Skip to content

Instantly share code, notes, and snippets.

@HuakunShen
Last active December 14, 2025 18:17
Show Gist options
  • Select an option

  • Save HuakunShen/c4bbc71ce1fc96594c0f7b44c3e6f684 to your computer and use it in GitHub Desktop.

Select an option

Save HuakunShen/c4bbc71ce1fc96594c0f7b44c3e6f684 to your computer and use it in GitHub Desktop.
nats client llms.txt
Selected Files Directory Structure:
└── ./
├── README.md
├── core
│ ├── README.md
│ ├── examples
│ │ └── snippets
│ │ ├── autounsub.ts
│ │ ├── basics.ts
│ │ ├── connect.ts
│ │ ├── headers.ts
│ │ ├── js-client.ts
│ │ ├── js.ts
│ │ ├── json.ts
│ │ ├── kv.ts
│ │ ├── no_responders.ts
│ │ ├── queuegroups.ts
│ │ ├── service.ts
│ │ ├── service_client.ts
│ │ ├── stream.ts
│ │ ├── sub_timeout.ts
│ │ ├── unsub.ts
│ │ └── wildcard_subscriptions.ts
│ └── tests
│ ├── auth_test.ts
│ ├── authenticator_test.ts
│ ├── autounsub_test.ts
│ ├── basics_test.ts
│ ├── bench_test.ts
│ ├── binary_test.ts
│ ├── buffer_test.ts
│ ├── clobber_test.ts
│ ├── connect.ts
│ ├── databuffer_test.ts
│ ├── disconnect_test.ts
│ ├── doublesubs_test.ts
│ ├── drain_test.ts
│ ├── encoders_test.ts
│ ├── events_test.ts
│ ├── headers_test.ts
│ ├── heartbeats_test.ts
│ ├── idleheartbeats_test.ts
│ ├── iterators_test.ts
│ ├── json_test.ts
│ ├── launcher_test.ts
│ ├── mrequest_test.ts
│ ├── parseip_test.ts
│ ├── parser_test.ts
│ ├── properties_test.ts
│ ├── protocol_test.ts
│ ├── queues_test.ts
│ ├── reconnect_test.ts
│ ├── resub_test.ts
│ ├── semver_test.ts
│ ├── servers_test.ts
│ ├── timeout_test.ts
│ ├── tls_test.ts
│ ├── token_test.ts
│ ├── types_test.ts
│ ├── util_test.ts
│ └── ws_test.ts
├── jetstream
│ ├── README.md
│ ├── examples
│ │ ├── 01_consumers.js
│ │ ├── 02_next.js
│ │ ├── 03_batch.js
│ │ ├── 04_consume.js
│ │ ├── 05_consume.js
│ │ ├── 06_heartbeats.js
│ │ ├── 07_consume_jobs.js
│ │ ├── 08_consume_process.js
│ │ ├── 09_replay.js
│ │ ├── js_readme_publish_examples.js
│ │ ├── jsm_readme_jsm_example.js
│ │ └── util.js
│ └── tests
│ ├── batch_publisher_test.ts
│ ├── consume_test.ts
│ ├── consumers_ordered_test.ts
│ ├── consumers_push_test.ts
│ ├── consumers_test.ts
│ ├── direct_consumer_test.ts
│ ├── fetch_test.ts
│ ├── jetstream409_test.ts
│ ├── jetstream_pullconsumer_test.ts
│ ├── jetstream_pushconsumer_test.ts
│ ├── jetstream_test.ts
│ ├── jscluster_test.ts
│ ├── jsm_direct_test.ts
│ ├── jsm_test.ts
│ ├── jsmsg_test.ts
│ ├── jstest_util.ts
│ ├── leak.ts
│ ├── next_test.ts
│ ├── pushconsumers_ordered_test.ts
│ ├── schedules_test.ts
│ ├── status_test.ts
│ ├── streams_test.ts
│ └── util.ts
├── kv
│ ├── README.md
│ └── tests
│ └── kv_test.ts
├── obj
│ ├── README.md
│ └── tests
│ ├── b64_test.ts
│ ├── objectstore_test.ts
│ └── sha_digest_test.ts
├── services
│ ├── README.md
│ ├── examples
│ │ ├── 01_services.ts
│ │ ├── 02_multiple_endpoints.ts
│ │ ├── 03_bigdata-client.ts
│ │ ├── 03_bigdata.ts
│ │ └── 03_util.ts
│ └── tests
│ ├── service-check.ts
│ └── service_test.ts
└── transport-node
├── README.md
├── examples
│ ├── bench.js
│ ├── nats-events.js
│ ├── nats-pub.js
│ ├── nats-rep.js
│ ├── nats-req.js
│ ├── nats-sub.js
│ └── util.js
└── tests
├── basics_test.js
├── jetstream_test.js
├── noiptls_test.js
├── reconnect_test.js
└── tls_test.js
--- core/examples/snippets/autounsub.ts ---
/*
* Copyright 2020 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
import type { Subscription } from "@nats-io/transport-deno";
// create a connection
const nc = await connect({ servers: "demo.nats.io:4222" });
// create a simple subscriber that listens for only one message
// and then auto unsubscribes, ending the async iterator
const sub = nc.subscribe("hello", { max: 3 });
const h1 = handler(sub);
const msub = nc.subscribe("hello");
const h2 = handler(msub);
for (let i = 1; i < 6; i++) {
nc.publish("hello", `hello-${i}`);
}
// ensure all the messages have been delivered to the server
// meaning that the subscription also processed them.
await nc.flush();
// unsub manually from the second subscription
msub.unsubscribe();
// await the handlers come back
await Promise.all([h1, h2]);
await nc.close();
async function handler(s: Subscription) {
const id = s.getID();
const max = s.getMax();
console.log(
`sub [${id}] listening to ${s.getSubject()} ${
max ? "and will unsubscribe after " + max + " msgs" : ""
}`,
);
for await (const m of s) {
console.log(`sub [${id}] #${s.getProcessed()}: ${m.string()}`);
}
console.log(`sub [${id}] is done.`);
}
--- core/examples/snippets/basics.ts ---
/*
* Copyright 2020 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "demo.nats.io:4222" });
// create a simple subscriber and iterate over messages
// matching the subscription
const sub = nc.subscribe("hello");
(async () => {
for await (const m of sub) {
console.log(`[${sub.getProcessed()}]: ${m.string()}`);
}
console.log("subscription closed");
})();
nc.publish("hello", "world");
nc.publish("hello", "again");
// we want to ensure that messages that are in flight
// get processed, so we are going to drain the
// connection. Drain is the same as close, but makes
// sure that all messages in flight get seen
// by the iterator. After calling drain,
// the connection closes.
await nc.drain();
--- core/examples/snippets/connect.ts ---
/*
* Copyright 2020-2021 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
const servers = [
{},
{ servers: ["demo.nats.io:4442", "demo.nats.io:4222"] },
{ servers: "demo.nats.io:4443" },
{ port: 4222 },
{ servers: "localhost" },
];
servers.forEach(async (v) => {
try {
const nc = await connect(v);
console.log(`connected to ${nc.getServer()}`);
// this promise indicates the client closed
const done = nc.closed();
// do something with the connection
// close the connection
await nc.close();
// check if the close was OK
const err = await done;
if (err) {
console.log(`error closing:`, err);
}
} catch (_err) {
console.log(`error connecting to ${JSON.stringify(v)}`);
}
});
--- core/examples/snippets/headers.ts ---
/*
* Copyright 2021-2024 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,
createInbox,
Empty,
headers,
nuid,
} from "@nats-io/transport-deno";
const nc = await connect(
{
servers: `demo.nats.io`,
},
);
// this function generates a random inbox subject
const subj = createInbox();
// subscribe to it
const sub = nc.subscribe(subj);
(async () => {
for await (const m of sub) {
if (m.headers) {
for (const [key, value] of m.headers) {
console.log(`${key}=${value}`);
}
console.log("ID", m.headers.get("ID"));
console.log("Id", m.headers.get("Id"));
console.log("id", m.headers.get("id"));
}
}
})().then();
// header names can be any printable ASCII character with the exception of `:`.
// header values can be any ASCII character except `\r` or `\n`.
// see https://www.ietf.org/rfc/rfc822.txt
const h = headers();
h.append("id", nuid.next());
h.append("unix_time", Date.now().toString());
nc.publish(subj, Empty, { headers: h });
await nc.flush();
await nc.close();
--- core/examples/snippets/js-client.ts ---
<Error reading file: Error: ENOENT: no such file or directory, open '/Users/hk/Dev/local-others/nats.js/core/examples/snippets/js-client.ts'>
--- core/examples/snippets/js.ts ---
import { connect, Empty } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "localhost:4222" });
import { AckPolicy, jetstream, jetstreamManager } from "@nats-io/jetstream";
const jsm = await jetstreamManager(nc);
for await (const si of jsm.streams.list()) {
console.log(si);
}
// add a stream - jetstream can capture nats core messages
const stream = "mystream";
const subj = `mystream.*`;
await jsm.streams.add({ name: stream, subjects: [subj] });
for (let i = 0; i < 100; i++) {
nc.publish(`${subj}.a`, Empty);
}
// find a stream that stores a specific subject:
const name = await jsm.streams.find("mystream.A");
// retrieve info about the stream by its name
const si = await jsm.streams.info(name);
// update a stream configuration
si.config.subjects?.push("a.b");
await jsm.streams.update(name, si.config);
// get a particular stored message in the stream by sequence
// this is not associated with a consumer
const sm = await jsm.streams.getMessage(stream, { seq: 1 });
console.log("sm?.seq", sm?.seq);
// delete the 5th message in the stream, securely erasing it
await jsm.streams.deleteMessage(stream, 5);
// purge all messages in the stream, the stream itself remains.
await jsm.streams.purge(stream);
// purge all messages with a specific subject (filter can be a wildcard)
await jsm.streams.purge(stream, { filter: "a.b" });
// purge messages with a specific subject keeping some messages
await jsm.streams.purge(stream, { filter: "a.c", keep: 5 });
// purge all messages with upto (not including seq)
await jsm.streams.purge(stream, { seq: 90 });
// purge all messages with upto sequence that have a matching subject
await jsm.streams.purge(stream, { filter: "a.d", seq: 100 });
// list all consumers for a stream:
const consumers = await jsm.consumers.list(stream).next();
consumers.forEach((ci) => {
console.log(ci);
});
// add a new durable consumer
await jsm.consumers.add(stream, {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
});
// retrieve a consumer's status and configuration
const ci = await jsm.consumers.info(stream, "me");
console.log(ci);
// delete a particular consumer
await jsm.consumers.delete(stream, "me");
// ```
// ```typescript
// // create the stream
// const jsm = await jetstreamManager(nc);
// await jsm.streams.add({ name: "a", subjects: ["a.*"] });
// // create a jetstream client:
// const js = jetstream(nc);
// // publish a message received by a stream
// let pa = await js.publish("a.b");
// // jetstream returns an acknowledgement with the
// // stream that captured the message, it's assigned sequence
// // and whether the message is a duplicate.
// const stream = pa.stream;
// const seq = pa.seq;
// const duplicate = pa.duplicate;
// // More interesting is the ability to prevent duplicates
// // on messages that are stored in the server. If
// // you assign a message ID, the server will keep looking
// // for the same ID for a configured amount of time (within a
// // configurable time window), and reject messages that
// // have the same ID:
// await js.publish("a.b", Empty, { msgID: "a" });
// // you can also specify constraints that should be satisfied.
// // For example, you can request the message to have as its
// // last sequence before accepting the new message:
// await js.publish("a.b", Empty, { expect: { lastMsgID: "a" } });
// await js.publish("a.b", Empty, { expect: { lastSequence: 3 } });
// // save the last sequence for this publish
// pa = await js.publish("a.b", Empty, { expect: { streamName: "a" } });
// // you can also mix the above combinations
// // this stream here accepts wildcards, you can assert that the
// // last message sequence recorded on a particular subject matches:
// const buf: Promise<PubAck>[] = [];
// for (let i = 0; i < 100; i++) {
// buf.push(js.publish("a.a", Empty));
// }
// await Promise.all(buf);
// // if additional "a.b" has been recorded, this will fail
// await js.publish("a.b", Empty, { expect: { lastSubjectSequence: pa.seq } });
--- core/examples/snippets/json.ts ---
/*
* Copyright 2021 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "demo.nats.io:4222" });
interface Person {
name: string;
}
// create a simple subscriber and iterate over messages
// matching the subscription
const sub = nc.subscribe("people");
(async () => {
for await (const m of sub) {
// typescript will see this as a Person
const p = m.json<Person>();
console.log(p);
}
})();
const p = { name: "Memo" } as Person;
nc.publish("people", JSON.stringify(p));
// finish
await nc.drain();
--- core/examples/snippets/kv.ts ---
import { connect } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "localhost:4222" });
import { Kvm } from "@nats-io/kv";
const kvm = new Kvm(nc);
const kv = await kvm.create("testing", { history: 5 });
// if the kv is expected to exist:
// const kv = await kvm.open("testing");
// create an entry - this is similar to a put, but will fail if the
// key exists
const hw = await kv.create("hello.world", "hi");
console.log('hw', hw)
// Values in KV are stored as KvEntries:
// {
// bucket: string,
// key: string,
// value: Uint8Array,
// created: Date,
// revision: number,
// delta?: number,
// operation: "PUT"|"DEL"|"PURGE"
// }
// The operation property specifies whether the value was
// updated (PUT), deleted (DEL) or purged (PURGE).
// you can monitor values modification in a KV by watching.
// You can watch specific key subset or everything.
// Watches start with the latest value for each key in the
// set of keys being watched - in this case all keys
const watch = await kv.watch();
(async () => {
for await (const e of watch) {
// do something with the change
console.log(`watch: ${e.key}: ${e.operation} ${e.value ? e.string() : ""}`);
}
})().then();
// update the entry
await kv.put("hello.world", "world");
// retrieve the KvEntry storing the value
// returns null if the value is not found
const e = await kv.get("hello.world");
// initial value of "hi" was overwritten above
console.log(`value for get ${e?.string()}`);
const buf: string[] = [];
const keys = await kv.keys();
await (async () => {
for await (const k of keys) {
buf.push(k);
}
})();
console.log(`keys contains hello.world: ${buf[0] === "hello.world"}`);
let h = await kv.history({ key: "hello.world" });
await (async () => {
for await (const e of h) {
// do something with the historical value
// you can test e.operation for "PUT", "DEL", or "PURGE"
// to know if the entry is a marker for a value set
// or for a deletion or purge.
console.log(
`history: ${e.key}: ${e.operation} ${e.value ? new TextDecoder().decode(e.value) : ""}`
);
}
})();
// deletes the key - the delete is recorded
await kv.delete("hello.world");
// purge is like delete, but all history values
// are dropped and only the purge remains.
await kv.purge("hello.world");
// stop the watch operation above
watch.stop();
// danger: destroys all values in the KV!
await kv.destroy();
--- core/examples/snippets/no_responders.ts ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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, errors } from "@nats-io/transport-deno";
const nc = await connect(
{
servers: `demo.nats.io`,
},
);
try {
const m = await nc.request("hello.world");
console.log(m.data);
} catch (err) {
if (err instanceof Error) {
if (err.cause instanceof errors.TimeoutError) {
console.log("someone is listening but didn't respond");
} else if (err.cause instanceof errors.NoRespondersError) {
console.log("no one is listening to 'hello.world'");
} else {
console.log(
`failed due to unknown error: ${(err.cause as Error)?.message}`,
);
}
} else {
console.log(`request failed: ${err}`);
}
}
await nc.close();
--- core/examples/snippets/queuegroups.ts ---
/*
* Copyright 2020-2021 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 } from "@nats-io/transport-deno";
import type { NatsConnection, Subscription } from "@nats-io/transport-deno";
async function createService(
name: string,
count = 1,
queue = "",
): Promise<NatsConnection[]> {
const conns: NatsConnection[] = [];
for (let i = 1; i <= count; i++) {
const n = queue ? `${name}-${i}` : name;
const nc = await connect(
{ servers: "demo.nats.io:4222", name: `${n}` },
);
nc.closed()
.then((err) => {
if (err) {
console.error(
`service ${n} exited because of error: ${err.message}`,
);
}
});
// create a subscription - note the option for a queue, if set
// any client with the same queue will be the queue group.
const sub = nc.subscribe("echo", { queue: queue });
const _ = handleRequest(n, sub);
console.log(`${n} is listening for 'echo' requests...`);
conns.push(nc);
}
return conns;
}
// simple handler for service requests
async function handleRequest(name: string, s: Subscription) {
const p = 12 - name.length;
const pad = "".padEnd(p);
for await (const m of s) {
// respond returns true if the message had a reply subject, thus it could respond
if (m.respond(m.data)) {
console.log(
`[${name}]:${pad} #${s.getProcessed()} echoed ${m.string()}`,
);
} else {
console.log(
`[${name}]:${pad} #${s.getProcessed()} ignoring request - no reply subject`,
);
}
}
}
// let's create two queue groups and a standalone subscriber
const conns: NatsConnection[] = [];
conns.push(...await createService("echo", 3, "echo"));
conns.push(...await createService("other-echo", 2, "other-echo"));
conns.push(...await createService("standalone"));
const a: Promise<void | Error>[] = [];
conns.forEach((c) => {
a.push(c.closed());
});
await Promise.all(a);
--- core/examples/snippets/service_client.ts ---
/*
* Copyright 2020 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 the connect function from a transport
import { connect, Empty } from "@nats-io/transport-deno";
// create a connection
const nc = await connect({ servers: "demo.nats.io:4222" });
// create an encoder
// the client makes a request and receives a promise for a message
// by default the request times out after 1s (1000 millis) and has
// no payload.
await nc.request("time", Empty, { timeout: 1000 })
.then((m) => {
console.log(`got response: ${m.string()}`);
})
.catch((err) => {
console.log(`problem with request: ${err.message}`);
});
await nc.close();
--- core/examples/snippets/service.ts ---
/*
* Copyright 2020-2021 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
import type { Subscription } from "@nats-io/transport-deno";
// create a connection
const nc = await connect({ servers: "demo.nats.io" });
// this subscription listens for `time` requests and returns the current time
const sub = nc.subscribe("time");
(async (sub: Subscription) => {
console.log(`listening for ${sub.getSubject()} requests...`);
for await (const m of sub) {
if (m.respond(new Date().toISOString())) {
console.info(`[time] handled #${sub.getProcessed()}`);
} else {
console.log(`[time] #${sub.getProcessed()} ignored - no reply subject`);
}
}
console.log(`subscription ${sub.getSubject()} drained.`);
})(sub);
// this subscription listens for admin.uptime and admin.stop
// requests to admin.uptime returns how long the service has been running
// requests to admin.stop gracefully stop the client by draining
// the connection
const started = Date.now();
const msub = nc.subscribe("admin.*");
(async (sub: Subscription) => {
console.log(`listening for ${sub.getSubject()} requests [uptime | stop]`);
// it would be very good to verify the origin of the request
// before implementing something that allows your service to be managed.
// NATS can limit which client can send or receive on what subjects.
for await (const m of sub) {
const chunks = m.subject.split(".");
console.info(`[admin] #${sub.getProcessed()} handling ${chunks[1]}`);
switch (chunks[1]) {
case "uptime":
// send the number of millis since up
m.respond(`${Date.now() - started}`);
break;
case "stop": {
m.respond(`[admin] #${sub.getProcessed()} stopping....`);
// gracefully shutdown
nc.drain()
.catch((err) => {
console.log("error draining", err);
});
break;
}
default:
console.log(
`[admin] #${sub.getProcessed()} ignoring request for ${m.subject}`,
);
}
}
// the iterator will stop when the subscription closes or drains
// so this line will print when that happens
console.log(`subscription ${sub.getSubject()} closed.`);
})(msub);
// wait for the client to close here.
await nc.closed().then((err?: void | Error) => {
let m = `connection to ${nc.getServer()} closed`;
if (err) {
m = `${m} with an error: ${err.message}`;
}
console.log(m);
});
--- core/examples/snippets/stream.ts ---
/*
* Copyright 2020 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "demo.nats.io" });
console.info(
"enter the following command from your transport module to get messages from the stream",
);
console.info(
"examples/nats-sub.ts stream.demo",
);
const start = Date.now();
let sequence = 0;
setInterval(() => {
sequence++;
const uptime = Date.now() - start;
console.info(`publishing #${sequence}`);
nc.publish("stream.demo", JSON.stringify({ sequence, uptime }));
}, 1000);
--- core/examples/snippets/sub_timeout.ts ---
/*
* Copyright 2020-2021 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 the connect function from a transport
import { connect, errors } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "demo.nats.io:4222" });
// create subscription with a timeout, if no message arrives
// within the timeout, the subscription throws a timeout error
const sub = nc.subscribe("hello", { timeout: 1000 });
(async () => {
for await (const _m of sub) {
// handle the messages
}
})().catch((err) => {
if (err instanceof errors.TimeoutError) {
console.log(`sub timed out!`);
} else {
console.log(`sub iterator got an error!`);
}
nc.close();
});
await nc.closed();
--- core/examples/snippets/unsub.ts ---
/*
* Copyright 2021 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "demo.nats.io:4222" });
// create a couple of subscriptions
// this subscription also illustrates the possible use of callbacks.
const auto = nc.subscribe("hello", {
callback: (err, msg) => {
if (err) {
console.error(err);
return;
}
console.log("auto", auto.getProcessed(), msg.string());
},
});
// unsubscribe after the first message,
auto.unsubscribe(1);
const manual = nc.subscribe("hello");
// wait for a message that says to stop
const done = (async () => {
console.log("waiting for a message on `hello` with a payload of `stop`");
for await (const m of manual) {
const payload = m.string();
console.log("manual", manual.getProcessed(), payload);
if (payload === "stop") {
manual.unsubscribe();
}
}
console.log("subscription iterator is done");
})();
await done;
await nc.close();
--- core/examples/snippets/wildcard_subscriptions.ts ---
/*
* Copyright 2020-2021 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 } from "@nats-io/transport-deno";
import type { Subscription } from "@nats-io/transport-deno";
const nc = await connect({ servers: "demo.nats.io:4222" });
// subscriptions can have wildcard subjects
// the '*' matches any string in the specified token position
const s1 = nc.subscribe("help.*.system");
const s2 = nc.subscribe("help.me.*");
// the '>' matches any tokens in that position or following
// '>' can only be specified at the end
const s3 = nc.subscribe("help.>");
async function printMsgs(s: Subscription) {
const subj = s.getSubject();
console.log(`listening for ${subj}`);
const c = 13 - subj.length;
const pad = "".padEnd(c);
for await (const m of s) {
console.log(
`[${subj}]${pad} #${s.getProcessed()} - ${m.subject} ${
m.data ? " " + m.string() : ""
}`,
);
}
}
printMsgs(s1);
printMsgs(s2);
printMsgs(s3);
// don't exit until the client closes
await nc.closed();
--- core/tests/auth_test.ts ---
/*
* Copyright 2018-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 { cleanup, connect, NatsServer, setup } from "test_helpers";
import {
assert,
assertArrayIncludes,
assertEquals,
assertExists,
assertRejects,
assertStringIncludes,
fail,
} from "@std/assert";
import { encodeAccount, encodeOperator, encodeUser } from "@nats-io/jwt";
import type {
MsgImpl,
NatsConnectionImpl,
NKeyAuth,
Status,
UserPass,
} from "../src/internal_mod.ts";
import {
createInbox,
credsAuthenticator,
DEFAULT_MAX_RECONNECT_ATTEMPTS,
deferred,
Empty,
jwtAuthenticator,
nkeyAuthenticator,
nkeys,
tokenAuthenticator,
usernamePasswordAuthenticator,
} from "../src/internal_mod.ts";
import { errors } from "../src/errors.ts";
const conf = {
authorization: {
users: [{
user: "derek",
password: "foobar",
permission: {
subscribe: "bar",
publish: "foo",
},
}],
},
};
Deno.test("auth - none", async () => {
const ns = await NatsServer.start(conf);
await assertRejects(
() => {
return ns.connect({ reconnect: false });
},
errors.AuthorizationError,
);
await ns.stop();
});
Deno.test("auth - bad", async () => {
const ns = await NatsServer.start(conf);
await assertRejects(
() => {
return ns.connect({ user: "me", pass: "hello" });
},
errors.AuthorizationError,
);
await ns.stop();
});
Deno.test("auth - weird chars", async () => {
const pass = "§12§12§12";
const ns = await NatsServer.start({
authorization: {
username: "admin",
password: pass,
},
});
const nc = await ns.connect({ user: "admin", pass: pass });
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - un/pw", async () => {
const ns = await NatsServer.start(conf);
const nc = await ns.connect(
{ user: "derek", pass: "foobar" },
);
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - un/pw authenticator", async () => {
const ns = await NatsServer.start(conf);
const nc = await ns.connect(
{
authenticator: usernamePasswordAuthenticator("derek", "foobar"),
},
);
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - sub no permissions keeps connection", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permissions: { subscribe: "foo" },
}],
},
}, { user: "a", pass: "a", reconnect: false });
const errStatus = deferred<Status>();
(async () => {
for await (const s of nc.status()) {
errStatus.resolve(s);
}
})().then();
const cbErr = deferred<Error | null>();
const sub = nc.subscribe("bar", {
callback: (err, _msg) => {
cbErr.resolve(err);
},
});
const v = await Promise.all([errStatus, cbErr, sub.closed]);
assertEquals(v[0].type, "error");
const ev = v[0] as ErrorEvent;
assertEquals(
ev.error.message,
`Permissions Violation for Subscription to "bar"`,
);
assertEquals(
v[1]?.message,
`Permissions Violation for Subscription to "bar"`,
);
assertEquals(nc.isClosed(), false);
await cleanup(ns, nc);
});
Deno.test("auth - sub iterator no permissions keeps connection", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permissions: { subscribe: "foo" },
}],
},
}, { user: "a", pass: "a", reconnect: false });
const errStatus = deferred<Status>();
(async () => {
for await (const s of nc.status()) {
errStatus.resolve(s);
}
})().then();
const iterErr = deferred<Error | null>();
const sub = nc.subscribe("bar");
(async () => {
for await (const _m of sub) {
// ignored
}
})().catch((err) => {
iterErr.resolve(err);
});
await nc.flush();
const v = await Promise.all([errStatus, iterErr, sub.closed]);
assertEquals(v[0].type, "error");
const ev = v[0] as ErrorEvent;
assertEquals(
ev.error.message,
`Permissions Violation for Subscription to "bar"`,
);
assertEquals(
v[1]?.message,
`Permissions Violation for Subscription to "bar"`,
);
assertEquals(sub.isClosed(), true);
assertEquals(nc.isClosed(), false);
await cleanup(ns, nc);
});
Deno.test("auth - pub permissions keep connection", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permissions: { publish: "foo" },
}],
},
}, { user: "a", pass: "a", reconnect: false });
const errStatus = deferred<Status>();
(async () => {
for await (const s of nc.status()) {
errStatus.resolve(s);
}
})().then();
nc.publish("bar");
const v = await errStatus;
assertEquals(v.type, "error");
const ev = v as ErrorEvent;
assertEquals(ev.error.message, `Permissions Violation for Publish to "bar"`);
assertEquals(nc.isClosed(), false);
await cleanup(ns, nc);
});
Deno.test("auth - req permissions keep connection", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permissions: { publish: "foo" },
}],
},
}, { user: "a", pass: "a", reconnect: false });
const errStatus = deferred<Status>();
(async () => {
for await (const s of nc.status()) {
errStatus.resolve(s);
}
})().then();
await assertRejects(
async () => {
await nc.request("bar");
},
errors.RequestError,
`Permissions Violation for Publish to "bar"`,
);
const v = await errStatus;
assertEquals(v.type, "error");
const ev = v as ErrorEvent;
assertEquals(ev.error.message, `Permissions Violation for Publish to "bar"`);
assertEquals(nc.isClosed(), false);
await cleanup(ns, nc);
});
Deno.test("auth - token", async () => {
const { ns, nc } = await setup({ authorization: { token: "foo" } }, {
token: "foo",
});
await nc.flush();
await cleanup(ns, nc);
});
Deno.test("auth - token authenticator", async () => {
const ns = await NatsServer.start({ authorization: { token: "foo" } });
const nc = await ns.connect({
authenticator: tokenAuthenticator("foo"),
});
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - nkey", async () => {
const kp = nkeys.createUser();
const pk = kp.getPublicKey();
const seed = kp.getSeed();
const conf = {
authorization: {
users: [
{ nkey: pk },
],
},
};
const ns = await NatsServer.start(conf);
const nc = await ns.connect(
{ authenticator: nkeyAuthenticator(seed) },
);
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - creds", async () => {
const creds = `-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN USER NKEY SEED-----
SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4
------END USER NKEY SEED------
`;
const conf = {
operator:
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw",
resolver: "MEMORY",
"resolver_preload": {
ACZSWBJ4SYILK7QVDELO64VX3EFWB6CXCPMEBUKA36MJJQRPXGEEQ2WJ:
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXVFdYVDNCT1JWSFNLQkc2T0pIVVdFQ01QRVdBNldZVEhNRzVEWkJBUUo1TUtGU1dHM1FRIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInN1YiI6IkFDWlNXQko0U1lJTEs3UVZERUxPNjRWWDNFRldCNkNYQ1BNRUJVS0EzNk1KSlFSUFhHRUVRMldKIiwidHlwZSI6ImFjY291bnQiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiY29ubiI6LTEsImltcG9ydHMiOi0xLCJleHBvcnRzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ3aWxkY2FyZHMiOnRydWV9fX0.q-E7bBGTU0uoTmM9Vn7WaEHDzCUrqvPDb9mPMQbry_PNzVAjf0RG9vd15lGxW5lu7CuGVqpj4CYKhNDHluIJAg",
},
};
const ns = await NatsServer.start(conf);
const nc = await ns.connect(
{
authenticator: credsAuthenticator(new TextEncoder().encode(creds)),
},
);
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - custom", async () => {
const jwt =
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw";
const useed = "SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4";
const conf = {
operator:
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw",
resolver: "MEMORY",
"resolver_preload": {
ACZSWBJ4SYILK7QVDELO64VX3EFWB6CXCPMEBUKA36MJJQRPXGEEQ2WJ:
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXVFdYVDNCT1JWSFNLQkc2T0pIVVdFQ01QRVdBNldZVEhNRzVEWkJBUUo1TUtGU1dHM1FRIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInN1YiI6IkFDWlNXQko0U1lJTEs3UVZERUxPNjRWWDNFRldCNkNYQ1BNRUJVS0EzNk1KSlFSUFhHRUVRMldKIiwidHlwZSI6ImFjY291bnQiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiY29ubiI6LTEsImltcG9ydHMiOi0xLCJleHBvcnRzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ3aWxkY2FyZHMiOnRydWV9fX0.q-E7bBGTU0uoTmM9Vn7WaEHDzCUrqvPDb9mPMQbry_PNzVAjf0RG9vd15lGxW5lu7CuGVqpj4CYKhNDHluIJAg",
},
};
const ns = await NatsServer.start(conf);
const authenticator = (nonce?: string) => {
const seed = nkeys.fromSeed(new TextEncoder().encode(useed));
const nkey = seed.getPublicKey();
const hash = seed.sign(new TextEncoder().encode(nonce));
const sig = nkeys.encode(hash);
return { nkey, sig, jwt };
};
const nc = await ns.connect(
{
authenticator: authenticator,
},
);
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - jwt", async () => {
const jwt =
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw";
const useed = "SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4";
const conf = {
operator:
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw",
resolver: "MEMORY",
"resolver_preload": {
ACZSWBJ4SYILK7QVDELO64VX3EFWB6CXCPMEBUKA36MJJQRPXGEEQ2WJ:
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXVFdYVDNCT1JWSFNLQkc2T0pIVVdFQ01QRVdBNldZVEhNRzVEWkJBUUo1TUtGU1dHM1FRIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInN1YiI6IkFDWlNXQko0U1lJTEs3UVZERUxPNjRWWDNFRldCNkNYQ1BNRUJVS0EzNk1KSlFSUFhHRUVRMldKIiwidHlwZSI6ImFjY291bnQiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiY29ubiI6LTEsImltcG9ydHMiOi0xLCJleHBvcnRzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ3aWxkY2FyZHMiOnRydWV9fX0.q-E7bBGTU0uoTmM9Vn7WaEHDzCUrqvPDb9mPMQbry_PNzVAjf0RG9vd15lGxW5lu7CuGVqpj4CYKhNDHluIJAg",
},
};
const ns = await NatsServer.start(conf);
let nc = await ns.connect(
{
authenticator: jwtAuthenticator(jwt, new TextEncoder().encode(useed)),
},
);
await nc.flush();
await nc.close();
nc = await ns.connect(
{
authenticator: jwtAuthenticator((): string => {
return jwt;
}, new TextEncoder().encode(useed)),
},
);
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - custom error", async () => {
const ns = await NatsServer.start(conf);
const authenticator = () => {
throw new Error("user code exploded");
};
await assertRejects(
() => {
return ns.connect(
{
maxReconnectAttempts: 1,
authenticator: authenticator,
},
);
},
Error,
"user code exploded",
);
await ns.stop();
});
Deno.test("basics - bad auth", async () => {
await assertRejects(
() => {
return connect(
{
servers: "connect.ngs.global",
reconnect: false,
user: "me",
pass: "you",
},
);
},
errors.AuthorizationError,
"Authorization Violation",
);
});
Deno.test("auth - nkey authentication", async () => {
const ukp = nkeys.createUser();
const conf = {
authorization: {
users: [{
nkey: ukp.getPublicKey(),
}],
},
};
// static
const ns = await NatsServer.start(conf);
let nc = await ns.connect({
authenticator: nkeyAuthenticator(ukp.getSeed()),
});
await nc.flush();
await nc.close();
// from function
nc = await ns.connect({
authenticator: nkeyAuthenticator((): Uint8Array => {
return ukp.getSeed();
}),
});
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - creds authenticator validation", () => {
const jwt =
`eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw`;
const ukp = nkeys.createUser();
const upk = ukp.getPublicKey();
const seed = new TextDecoder().decode(ukp.getSeed());
function creds(ajwt = "", aseed = ""): string {
return `-----BEGIN NATS USER JWT-----
${ajwt}
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN USER NKEY SEED-----
${aseed}
------END USER NKEY SEED------
`;
}
type test = [string, string, boolean, string];
const tests: test[] = [];
tests.push(["", "", false, "no jwt, no seed"]);
tests.push([jwt, "", false, "no seed"]);
tests.push(["", seed, false, "no jwt"]);
tests.push([jwt, seed, true, "jwt and seed"]);
tests.forEach((v) => {
const d = new TextEncoder().encode(creds(v[0], v[1]));
try {
const auth = credsAuthenticator(d);
if (!v[2]) {
fail(`should have failed: ${v[3]}`);
}
const { nkey, sig } = auth("helloworld") as unknown as NKeyAuth;
assertEquals(nkey, upk);
assert(sig.length > 0);
} catch (_err) {
if (v[2]) {
fail(`should have passed: ${v[3]}`);
}
}
});
});
Deno.test("auth - expiration is notified", async () => {
const O = nkeys.createOperator();
const A = nkeys.createAccount();
const resolver: Record<string, string> = {};
resolver[A.getPublicKey()] = await encodeAccount("A", A, {
limits: {
conn: -1,
subs: -1,
},
}, { signer: O });
const conf = {
operator: await encodeOperator("O", O),
resolver: "MEMORY",
"resolver_preload": resolver,
};
const ns = await NatsServer.start(conf);
const U = nkeys.createUser();
const ujwt = await encodeUser("U", U, A, { bearer_token: true }, {
exp: Math.round(Date.now() / 1000) + 5,
});
const nc = await ns.connect({
reconnect: false,
authenticator: jwtAuthenticator(ujwt),
});
let authErrors = 0;
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" &&
s.error instanceof errors.UserAuthenticationExpiredError
) {
authErrors++;
}
}
})().then();
const err = await nc.closed();
assert(authErrors >= 1);
assertExists(err);
assert(err instanceof errors.UserAuthenticationExpiredError, err?.message);
await cleanup(ns);
});
Deno.test("auth - expiration is notified and recovered", async () => {
const O = nkeys.createOperator();
const A = nkeys.createAccount();
const resolver: Record<string, string> = {};
resolver[A.getPublicKey()] = await encodeAccount("A", A, {
limits: {
conn: -1,
subs: -1,
},
}, { signer: O });
const conf = {
operator: await encodeOperator("O", O),
resolver: "MEMORY",
"resolver_preload": resolver,
};
const ns = await NatsServer.start(conf);
const U = nkeys.createUser();
let ujwt = await encodeUser("U", U, A, { bearer_token: true }, {
exp: Math.round(Date.now() / 1000) + 3,
});
const timer = setInterval(() => {
encodeUser("U", U, A, { bearer_token: true }, {
exp: Math.round(Date.now() / 1000) + 3,
}).then((token) => {
ujwt = token;
});
}, 250);
const nc = await ns.connect({
maxReconnectAttempts: -1,
authenticator: jwtAuthenticator(() => {
return ujwt;
}),
});
const d = deferred();
let reconnects = 0;
let authErrors = 0;
(async () => {
for await (const s of nc.status()) {
switch (s.type) {
case "reconnect":
reconnects++;
if (reconnects === 4) {
d.resolve();
}
break;
case "error":
if (s.error instanceof errors.UserAuthenticationExpiredError) {
authErrors++;
}
break;
default:
// ignored
}
}
})().then();
await d;
clearInterval(timer);
assert(authErrors >= 1);
assert(reconnects >= 4);
await cleanup(ns, nc);
});
Deno.test("auth - bad auth is notified", async () => {
const ns = await NatsServer.start(conf);
let count = 0;
// authenticator that works once
const authenticator = (): UserPass => {
const pass = count === 0 ? "foobar" : "bad";
count++;
return { user: "derek", pass };
};
const nc = await ns.connect(
{ authenticator },
);
let badAuths = 0;
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" && s.error instanceof errors.AuthorizationError
) {
badAuths++;
}
}
})().then();
await nc.reconnect();
const err = await nc.closed();
assert(badAuths > 1);
assert(err instanceof errors.AuthorizationError);
await ns.stop();
});
Deno.test("auth - perm request error", async () => {
const ns = await NatsServer.start({
authorization: {
users: [{
user: "a",
password: "b",
permission: {
publish: "r",
},
}, {
user: "s",
password: "s",
permission: {
subscribe: "q",
},
}],
},
});
const [nc, sc] = await Promise.all([
ns.connect(
{ user: "a", pass: "b" },
),
ns.connect(
{ user: "s", pass: "s" },
),
]);
sc.subscribe("q", {
callback: (err, msg) => {
if (err) {
return;
}
msg.respond();
},
});
const status = deferred<Status>();
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" && s.error instanceof errors.PermissionViolationError
) {
if (s.error.operation === "publish" && s.error.subject === "q") {
status.resolve(s);
}
}
}
})().then();
assertRejects(() => {
return nc.request("q");
}, errors.RequestError);
await status;
await cleanup(ns, nc, sc);
});
Deno.test("auth - perm request error no mux", async () => {
const ns = await NatsServer.start({
authorization: {
users: [{
user: "a",
password: "b",
permission: {
publish: "r",
},
}, {
user: "s",
password: "s",
permission: {
subscribe: "q",
},
}],
},
});
const [nc, sc] = await Promise.all([
ns.connect(
{ user: "a", pass: "b" },
),
ns.connect(
{ user: "s", pass: "s" },
),
]);
sc.subscribe("q", {
callback: (err, msg) => {
if (err) {
return;
}
msg.respond();
},
});
const status = deferred<Status>();
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" && s.error instanceof errors.PermissionViolationError
) {
if (s.error.operation === "publish" && s.error.subject === "q") {
status.resolve(s);
}
}
}
})().then();
await assertRejects(
() => {
return nc.request("q", Empty, { noMux: true, timeout: 1000 });
},
errors.RequestError,
"q",
);
await cleanup(ns, nc, sc);
});
Deno.test("auth - perm request error deliver to sub", async () => {
const ns = await NatsServer.start({
authorization: {
users: [{
user: "a",
password: "b",
permission: {
publish: "r",
},
}, {
user: "s",
password: "s",
permission: {
subscribe: "q",
},
}],
},
});
const [nc, sc] = await Promise.all([
ns.connect(
{ user: "a", pass: "b" },
),
ns.connect(
{ user: "s", pass: "s" },
),
]);
sc.subscribe("q", {
callback: (err, msg) => {
if (err) {
return;
}
msg.respond();
},
});
const status = deferred<Status>();
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" && s.error instanceof errors.PermissionViolationError
) {
if (s.error.subject === "q" && s.error.operation === "publish") {
status.resolve();
}
}
}
})().then();
const inbox = createInbox();
const sub = nc.subscribe(inbox, {
callback: () => {
},
});
await assertRejects(
() => {
return nc.request("q", Empty, {
noMux: true,
reply: inbox,
timeout: 1000,
});
},
errors.RequestError,
`Permissions Violation for Publish to "q"`,
);
assertEquals(sub.isClosed(), false);
await cleanup(ns, nc, sc);
});
Deno.test("auth - mux request perms", async () => {
const conf = {
authorization: {
users: [{
user: "a",
password: "a",
permission: {
subscribe: "q.>",
},
}],
},
};
const ns = await NatsServer.start(conf);
const nc = await ns.connect({ user: "a", pass: "a" });
await assertRejects(
() => {
return nc.request("q");
},
errors.RequestError,
"Permissions Violation for Subscription",
);
const nc2 = await ns.connect({ user: "a", pass: "a", inboxPrefix: "q" });
await assertRejects(
() => {
return nc2.request("q");
},
errors.RequestError,
"no responders: 'q'",
);
await cleanup(ns, nc, nc2);
});
Deno.test("auth - perm sub iterator error", async () => {
const ns = await NatsServer.start({
authorization: {
users: [{
user: "a",
password: "b",
permission: {
subscribe: "s",
},
}],
},
});
const nc = await ns.connect({ user: "a", pass: "b" });
const status = deferred<Status>();
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" && s.error instanceof errors.PermissionViolationError
) {
if (s.error.subject === "q" && s.error.operation === "publish") {
status.resolve(s);
}
}
}
})().then();
const sub = nc.subscribe("q");
await assertRejects(
async () => {
for await (const _m of sub) {
// ignored
}
},
errors.PermissionViolationError,
`Permissions Violation for Subscription to "q"`,
);
const err = await sub.closed;
assertEquals(err instanceof errors.PermissionViolationError, true);
await cleanup(ns, nc);
});
Deno.test("auth - perm error is not in lastError", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permission: {
subscribe: {
deny: "q",
},
},
}],
},
}, { user: "a", pass: "a" });
const nci = nc as NatsConnectionImpl;
assertEquals(nci.protocol.lastError, undefined);
const d = deferred<Error | null>();
const sub = nc.subscribe("q", {
callback: (err) => {
d.resolve(err);
},
});
const err = await d;
assert(err !== null);
assert(err instanceof errors.PermissionViolationError);
assert(nci.protocol.lastError === undefined);
const err2 = await sub.closed;
assert(err2 instanceof errors.PermissionViolationError);
await cleanup(ns, nc);
});
Deno.test("auth - ignore auth error abort", async () => {
const ns = await NatsServer.start({
authorization: {
users: [{
user: "a",
password: "a",
}],
},
});
async function t(ignoreAuthErrorAbort = false): Promise<number> {
let pass = "a";
const authenticator = (): UserPass => {
return { user: "a", pass };
};
const nc = await ns.connect({
authenticator,
ignoreAuthErrorAbort,
reconnectTimeWait: 150,
});
let count = 0;
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" && s.error instanceof errors.AuthorizationError
) {
count++;
}
}
})().then();
const nci = nc as NatsConnectionImpl;
pass = "b";
nci.protocol.transport.disconnect();
await nc.closed();
return count;
}
assertEquals(await t(), 2);
assertEquals(await t(true), DEFAULT_MAX_RECONNECT_ATTEMPTS);
await ns.stop();
});
Deno.test("auth - sub with permission error discards", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permission: {
subscribe: {
deny: "q",
},
},
}],
},
}, { user: "a", pass: "a" });
const nci = nc as NatsConnectionImpl;
let count = 0;
async function q() {
count++;
const d = deferred();
const sub = nc.subscribe("q", {
callback: (err) => {
d.resolve(err);
},
});
const err = await d;
assert(err);
assertEquals(nc.isClosed(), false);
await sub.closed;
const s = nci.protocol.subscriptions.get(count);
assertEquals(s, undefined);
}
await q();
await q();
await cleanup(ns, nc);
});
Deno.test("auth - creds and un and pw and token", async () => {
const creds = `-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN USER NKEY SEED-----
SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4
------END USER NKEY SEED------
`;
const conf = {
operator:
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw",
resolver: "MEMORY",
"resolver_preload": {
ACZSWBJ4SYILK7QVDELO64VX3EFWB6CXCPMEBUKA36MJJQRPXGEEQ2WJ:
"eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXVFdYVDNCT1JWSFNLQkc2T0pIVVdFQ01QRVdBNldZVEhNRzVEWkJBUUo1TUtGU1dHM1FRIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInN1YiI6IkFDWlNXQko0U1lJTEs3UVZERUxPNjRWWDNFRldCNkNYQ1BNRUJVS0EzNk1KSlFSUFhHRUVRMldKIiwidHlwZSI6ImFjY291bnQiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiY29ubiI6LTEsImltcG9ydHMiOi0xLCJleHBvcnRzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ3aWxkY2FyZHMiOnRydWV9fX0.q-E7bBGTU0uoTmM9Vn7WaEHDzCUrqvPDb9mPMQbry_PNzVAjf0RG9vd15lGxW5lu7CuGVqpj4CYKhNDHluIJAg",
},
};
const ns = await NatsServer.start(conf);
const te = new TextEncoder();
const nc = await ns.connect(
{
authenticator: [
credsAuthenticator(te.encode(creds)),
nkeyAuthenticator(
te.encode(
"SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4",
),
),
],
user: "a",
pass: "secret",
token: "mytoken",
},
);
await nc.flush();
await nc.close();
await ns.stop();
});
Deno.test("auth - request context", async () => {
const { ns, nc } = await setup({
accounts: {
S: {
users: [{
user: "s",
password: "s",
permission: {
subscribe: ["q.>", "_INBOX.>"],
publish: "$SYS.REQ.USER.INFO",
allow_responses: true,
},
}],
exports: [
{ service: "q.>" },
],
},
A: {
users: [{ user: "a", password: "a" }],
imports: [
{ service: { subject: "q.>", account: "S" } },
],
},
},
}, { user: "s", pass: "s" });
const srv = await (nc as NatsConnectionImpl).context();
assertEquals(srv.data.user, "s");
assertEquals(srv.data.account, "S");
assertArrayIncludes(srv.data.permissions?.publish?.allow || [], [
"$SYS.REQ.USER.INFO",
]);
assertArrayIncludes(srv.data.permissions?.subscribe?.allow || [], [
"q.>",
"_INBOX.>",
]);
assertEquals(srv.data.permissions?.responses?.max, 1);
nc.subscribe("q.>", {
callback(err, msg) {
if (err) {
fail(err.message);
}
const info = (msg as MsgImpl).requestInfo();
assertEquals(info?.acc, "A");
msg.respond();
},
});
const a = await ns.connect({ user: "a", pass: "a" });
await a.request("q.hello");
await cleanup(ns, nc, a);
});
Deno.test("auth - sub queue permission", async () => {
const conf = {
authorization: {
users: [{
user: "a",
password: "a",
permissions: { subscribe: ["q A"] },
}],
},
};
const { ns, nc } = await setup(conf, { user: "a", pass: "a" });
const qA = deferred();
nc.subscribe("q", {
queue: "A",
callback: (err, _msg) => {
if (err) {
qA.reject(err);
}
},
});
const qBad = deferred<Error>();
nc.subscribe("q", {
queue: "bad",
callback: (err, _msg) => {
if (err) {
qBad.resolve(err);
}
},
});
await nc.flush();
const err = await qBad;
qA.resolve();
await qA;
assert(err instanceof errors.PermissionViolationError);
assertStringIncludes(err.message, 'using queue "bad"');
await cleanup(ns, nc);
});
Deno.test("auth - account expired", async () => {
const O = nkeys.createOperator();
const A = nkeys.createAccount();
const resolver: Record<string, string> = {};
resolver[A.getPublicKey()] = await encodeAccount("A", A, {
limits: {
conn: -1,
subs: -1,
},
}, { signer: O, exp: Math.round(Date.now() / 1000) + 3 });
const conf = {
operator: await encodeOperator("O", O),
resolver: "MEMORY",
"resolver_preload": resolver,
};
const U = nkeys.createUser();
const ujwt = await encodeUser("U", U, A, { bearer_token: true });
const { ns, nc } = await setup(conf, {
reconnect: false,
authenticator: jwtAuthenticator(ujwt),
});
const d = deferred();
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" &&
s.error.message.includes("account authentication expired")
) {
d.resolve();
break;
}
}
})().catch(() => {});
const w = await nc.closed();
assertExists(w);
assert(w instanceof errors.AuthorizationError);
assertEquals(w.message, "Account Authentication Expired");
await cleanup(ns, nc);
});
Deno.test("env conn", async () => {
const ns = await NatsServer.start();
const nc = await ns.connect({ debug: true });
await nc.flush();
await cleanup(ns, nc);
});
--- core/tests/authenticator_test.ts ---
/*
* Copyright 2022-2024 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 { cleanup, setup } from "test_helpers";
import {
credsAuthenticator,
deadline,
deferred,
delay,
jwtAuthenticator,
nkeyAuthenticator,
nkeys,
tokenAuthenticator,
usernamePasswordAuthenticator,
} from "../src/internal_mod.ts";
import type {
Auth,
Authenticator,
NatsConnection,
NatsConnectionImpl,
} from "../src/internal_mod.ts";
import { assertEquals, assertThrows } from "@std/assert";
import {
encodeAccount,
encodeOperator,
encodeUser,
fmtCreds,
} from "@nats-io/jwt";
import { assertBetween } from "test_helpers";
function disconnectReconnect(nc: NatsConnection): Promise<void> {
const done = deferred<void>();
const disconnect = deferred();
const reconnect = deferred();
(async () => {
for await (const s of nc.status()) {
switch (s.type) {
case "disconnect":
disconnect.resolve();
break;
case "reconnect":
reconnect.resolve();
break;
}
}
})().then();
Promise.all([disconnect, reconnect])
.then(() => done.resolve()).catch((err) => done.reject(err));
return done;
}
async function testAuthenticatorFn(
fn: Authenticator,
conf: Record<string, unknown>,
debug = false,
): Promise<void> {
let called = 0;
const authenticator = (nonce?: string): Auth => {
called++;
return fn(nonce);
};
conf = Object.assign({}, conf, { debug });
const { ns, nc } = await setup(conf, {
authenticator,
});
const cycle = disconnectReconnect(nc);
await delay(2000);
called = 0;
const nci = nc as NatsConnectionImpl;
nci.reconnect();
await delay(1000);
await deadline(cycle, 4000);
assertBetween(called, 1, 10);
await nc.flush();
assertEquals(nc.isClosed(), false);
await cleanup(ns, nc);
}
Deno.test("authenticator - username password fns", async () => {
const user = "a";
const pass = "a";
const authenticator = usernamePasswordAuthenticator(() => {
return user;
}, () => {
return pass;
});
await testAuthenticatorFn(authenticator, {
authorization: {
users: [{
user: "a",
password: "a",
}],
},
});
});
Deno.test("authenticator - username string password fn", async () => {
const pass = "a";
const authenticator = usernamePasswordAuthenticator("a", () => {
return pass;
});
await testAuthenticatorFn(authenticator, {
authorization: {
users: [{
user: "a",
password: "a",
}],
},
});
});
Deno.test("authenticator - username fn password string", async () => {
const user = "a";
const authenticator = usernamePasswordAuthenticator(() => {
return user;
}, "a");
await testAuthenticatorFn(authenticator, {
authorization: {
users: [{
user: "a",
password: "a",
}],
},
});
});
Deno.test("authenticator - token fn", async () => {
const token = "tok";
const authenticator = tokenAuthenticator(() => {
return token;
});
await testAuthenticatorFn(authenticator, {
authorization: {
token,
},
});
});
Deno.test("authenticator - nkey fn", async () => {
const user = nkeys.createUser();
const seed = user.getSeed();
const nkey = user.getPublicKey();
const authenticator = nkeyAuthenticator(() => {
return seed;
});
await testAuthenticatorFn(authenticator, {
authorization: {
users: [
{ nkey },
],
},
});
});
Deno.test("authenticator - jwt bearer fn", async () => {
const O = nkeys.createOperator();
const A = nkeys.createAccount();
const U = nkeys.createUser();
const ujwt = await encodeUser("U", U, A, { bearer_token: true });
const authenticator = jwtAuthenticator(() => {
return ujwt;
});
const resolver: Record<string, string> = {};
resolver[A.getPublicKey()] = await encodeAccount("A", A, {
limits: {
conn: -1,
subs: -1,
},
}, { signer: O });
const conf = {
operator: await encodeOperator("O", O),
resolver: "MEMORY",
"resolver_preload": resolver,
};
await testAuthenticatorFn(authenticator, conf);
});
Deno.test("authenticator - jwt fn", async () => {
const O = nkeys.createOperator();
const A = nkeys.createAccount();
const U = nkeys.createUser();
const ujwt = await encodeUser("U", U, A, {});
const authenticator = jwtAuthenticator(() => {
return ujwt;
}, () => {
return U.getSeed();
});
const resolver: Record<string, string> = {};
resolver[A.getPublicKey()] = await encodeAccount("A", A, {
limits: {
conn: -1,
subs: -1,
},
}, { signer: O });
const conf = {
operator: await encodeOperator("O", O),
resolver: "MEMORY",
"resolver_preload": resolver,
};
await testAuthenticatorFn(authenticator, conf);
});
Deno.test("authenticator - creds fn", async () => {
const O = nkeys.createOperator();
const A = nkeys.createAccount();
const U = nkeys.createUser();
const ujwt = await encodeUser("U", U, A, {});
const creds = fmtCreds(ujwt, U);
const authenticator = credsAuthenticator(() => {
return creds;
});
const resolver: Record<string, string> = {};
resolver[A.getPublicKey()] = await encodeAccount("A", A, {
limits: {
conn: -1,
subs: -1,
},
}, { signer: O });
const conf = {
operator: await encodeOperator("O", O),
resolver: "MEMORY",
"resolver_preload": resolver,
};
await testAuthenticatorFn(authenticator, conf);
});
Deno.test("authenticator - bad creds", () => {
assertThrows(
() => {
credsAuthenticator(new TextEncoder().encode("hello"))();
},
Error,
"unable to parse credentials",
);
});
--- core/tests/autounsub_test.ts ---
/*
* Copyright 2018-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 { assertEquals, assertRejects } from "@std/assert";
import { createInbox, Empty, errors } from "../src/internal_mod.ts";
import type { NatsConnectionImpl, Subscription } from "../src/internal_mod.ts";
import { cleanup, Lock, setup } from "test_helpers";
import { TimeoutError } from "../src/errors.ts";
Deno.test("autounsub - max option", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 10 });
for (let i = 0; i < 20; i++) {
nc.publish(subj);
}
await nc.flush();
assertEquals(sub.getReceived(), 10);
await cleanup(ns, nc);
});
Deno.test("autounsub - unsubscribe", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 10 });
sub.unsubscribe(11);
for (let i = 0; i < 20; i++) {
nc.publish(subj);
}
await nc.flush();
assertEquals(sub.getReceived(), 11);
await cleanup(ns, nc);
});
Deno.test("autounsub - can unsub from auto-unsubscribed", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 1 });
for (let i = 0; i < 20; i++) {
nc.publish(subj);
}
await nc.flush();
assertEquals(sub.getReceived(), 1);
sub.unsubscribe();
await cleanup(ns, nc);
});
Deno.test("autounsub - can break to unsub", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 20 });
const iter = (async () => {
for await (const _m of sub) {
break;
}
})();
for (let i = 0; i < 20; i++) {
nc.publish(subj);
}
await nc.flush();
await iter;
assertEquals(sub.getProcessed(), 1);
await cleanup(ns, nc);
});
Deno.test("autounsub - can change auto-unsub to a higher value", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 1 });
sub.unsubscribe(10);
for (let i = 0; i < 20; i++) {
nc.publish(subj);
}
await nc.flush();
assertEquals(sub.getReceived(), 10);
await cleanup(ns, nc);
});
Deno.test("autounsub - request receives expected count with multiple helpers", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const fn = async (sub: Subscription) => {
for await (const m of sub) {
m.respond();
}
};
const subs: Subscription[] = [];
for (let i = 0; i < 5; i++) {
const sub = nc.subscribe(subj);
fn(sub).then();
subs.push(sub);
}
await nc.request(subj);
await nc.drain();
const counts = subs.map((s) => {
return s.getReceived();
});
const count = counts.reduce((a, v) => a + v);
assertEquals(count, 5);
await ns.stop();
});
Deno.test("autounsub - manual request receives expected count with multiple helpers", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const lock = Lock(5);
const fn = async (sub: Subscription) => {
for await (const m of sub) {
m.respond();
lock.unlock();
}
};
for (let i = 0; i < 5; i++) {
const sub = nc.subscribe(subj);
fn(sub).then();
}
const replySubj = createInbox();
const sub = nc.subscribe(replySubj);
nc.publish(subj, Empty, { reply: replySubj });
await lock;
await nc.drain();
assertEquals(sub.getReceived(), 5);
await ns.stop();
});
Deno.test("autounsub - check subscription leaks", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
const sub = nc.subscribe(subj);
sub.unsubscribe();
assertEquals(nci.protocol.subscriptions.size(), 0);
await cleanup(ns, nc);
});
Deno.test("autounsub - check request leaks", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
// should have no subscriptions
assertEquals(nci.protocol.subscriptions.size(), 0);
const sub = nc.subscribe(subj);
(async () => {
for await (const m of sub) {
m.respond();
}
})().then();
// should have one subscription
assertEquals(nci.protocol.subscriptions.size(), 1);
const msgs = [];
msgs.push(nc.request(subj));
msgs.push(nc.request(subj));
// should have 2 mux subscriptions, and 2 subscriptions
assertEquals(nci.protocol.subscriptions.size(), 2);
assertEquals(nci.protocol.muxSubscriptions.size(), 2);
await Promise.all(msgs);
// mux subs should have pruned
assertEquals(nci.protocol.muxSubscriptions.size(), 0);
sub.unsubscribe();
assertEquals(nci.protocol.subscriptions.size(), 1);
await cleanup(ns, nc);
});
Deno.test("autounsub - check cancelled request leaks", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
// should have no subscriptions
assertEquals(nci.protocol.subscriptions.size(), 0);
const rp = nc.request(subj, Empty, { timeout: 100 });
assertEquals(nci.protocol.subscriptions.size(), 1);
assertEquals(nci.protocol.muxSubscriptions.size(), 1);
await assertRejects(
() => {
return rp;
},
errors.RequestError,
subj,
);
// the rejection should be timeout
// mux subs should have pruned
assertEquals(nci.protocol.muxSubscriptions.size(), 0);
await cleanup(ns, nc);
});
Deno.test("autounsub - timeout cancelled request leaks", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
// should have no subscriptions
assertEquals(nci.protocol.subscriptions.size(), 0);
nci.subscribe(subj, {
callback: () => {
// ignored so it times out
},
});
const rp = nc.request(subj, Empty, { timeout: 250 });
assertEquals(nci.protocol.subscriptions.size(), 2);
assertEquals(nci.protocol.muxSubscriptions.size(), 1);
// the rejection should be timeout
await assertRejects(
() => {
return rp;
},
TimeoutError,
);
// mux subs should have pruned
assertEquals(nci.protocol.muxSubscriptions.size(), 0);
await cleanup(ns, nc);
});
--- core/tests/basics_test.ts ---
/*
* 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);
});
--- core/tests/bench_test.ts ---
/*
* Copyright 2018-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, assertThrows } from "@std/assert";
import { Bench, createInbox, Metric } from "../src/mod.ts";
import { connect } from "./connect.ts";
import type { BenchOpts } from "../src/mod.ts";
const u = "demo.nats.io:4222";
async function runBench(opts: BenchOpts): Promise<Metric[]> {
const nc = await connect({ servers: u });
const bench = new Bench(nc, opts);
const m = await bench.run();
await nc.close();
return m;
}
function pubSub(m: Metric[]) {
assertEquals(m.length, 3);
const pubsub = m.find((v) => {
return v.name === "pubsub";
});
assert(pubsub);
const sub = m.find((v) => {
return v.name === "sub";
});
assert(sub);
const pub = m.find((v) => {
return v.name === "pub";
});
assert(pub);
m.forEach((v) => {
assertEquals(v.payload, 8);
assert(v.lang);
assert(v.version);
assert(v.toString() !== "");
csv(v);
});
}
function reqRep(m: Metric[]) {
assertEquals(m.length, 3);
assertEquals(m[0].payload, 8);
assert(m[0].lang);
assert(m[0].version);
csv(m[0]);
}
function csv(m: Metric) {
assertEquals(Metric.header().split(",").length, 9);
const lines = m.toCsv().split("\n");
assertEquals(lines.length, 2);
const fields = lines[0].split(",");
assertEquals(fields.length, 9);
}
Deno.test("bench - no opts toss", async () => {
const nc = await connect({ servers: u });
assertThrows(
() => {
new Bench(nc, {});
},
Error,
"no options selected",
);
await nc.close();
});
Deno.test(`bench - pubsub`, async () => {
const m = await runBench({
pub: true,
sub: true,
msgs: 5,
size: 8,
callbacks: true,
subject: createInbox(),
});
pubSub(m);
});
Deno.test(`bench - pubsub async`, async () => {
const m = await runBench({
pub: true,
sub: true,
msgs: 5,
size: 8,
subject: createInbox(),
});
pubSub(m);
});
Deno.test(`bench - req`, async () => {
const m = await runBench({
req: true,
rep: true,
callbacks: true,
msgs: 5,
size: 8,
subject: createInbox(),
});
reqRep(m);
});
Deno.test(`bench - req async`, async () => {
const m = await runBench({
req: true,
rep: true,
asyncRequests: true,
callbacks: true,
msgs: 5,
size: 8,
subject: createInbox(),
});
reqRep(m);
});
--- core/tests/binary_test.ts ---
/*
* Copyright 2018-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 { assertEquals } from "@std/assert";
import { createInbox, deferred } from "../src/internal_mod.ts";
import type { Msg } from "../src/internal_mod.ts";
import { cleanup, setup } from "test_helpers";
function macro(input: Uint8Array) {
return async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const dm = deferred<Msg>();
const sub = nc.subscribe(subj, { max: 1 });
(async () => {
for await (const m of sub) {
dm.resolve(m);
}
})().then();
nc.publish(subj, input);
const msg = await dm;
assertEquals(msg.data, input);
await cleanup(ns, nc);
};
}
const invalid2octet = new Uint8Array([0xc3, 0x28]);
const invalidSequenceIdentifier = new Uint8Array([0xa0, 0xa1]);
const invalid3octet = new Uint8Array([0xe2, 0x28, 0xa1]);
const invalid4octet = new Uint8Array([0xf0, 0x90, 0x28, 0xbc]);
const embeddedNull = new Uint8Array(
[0x00, 0xf0, 0x00, 0x28, 0x00, 0x00, 0xf0, 0x9f, 0x92, 0xa9, 0x00],
);
Deno.test("binary - invalid2octet", macro(invalid2octet));
Deno.test(
"binary - invalidSequenceIdentifier",
macro(invalidSequenceIdentifier),
);
Deno.test("binary - invalid3octet", macro(invalid3octet));
Deno.test("binary - invalid4octet", macro(invalid4octet));
Deno.test("binary - embeddednull", macro(embeddedNull));
--- core/tests/buffer_test.ts ---
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
// This code has been ported almost directly from Go's src/bytes/buffer_test.go
// Copyright 2009 The Go Authors. All rights reserved. BSD license.
// https://github.com/golang/go/blob/master/LICENSE
// This code removes all Deno specific functionality to enable its use
// in a browser environment
import { assert, assertEquals, assertThrows } from "@std/assert";
import {
DenoBuffer,
MAX_SIZE,
readAll,
writeAll,
} from "../src/internal_mod.ts";
import {
append,
assert as buffer_assert,
AssertionError,
concat,
} from "../src/denobuffer.ts";
// N controls how many iterations of certain checks are performed.
const N = 100;
let testBytes: Uint8Array | null;
let testString: string | null;
function init(): void {
if (testBytes == null) {
testBytes = new Uint8Array(N);
for (let i = 0; i < N; i++) {
testBytes[i] = "a".charCodeAt(0) + (i % 26);
}
const decoder = new TextDecoder();
testString = decoder.decode(testBytes);
}
}
function check(buf: DenoBuffer, s: string): void {
const bytes = buf.bytes();
assertEquals(buf.length, bytes.byteLength);
const decoder = new TextDecoder();
const bytesStr = decoder.decode(bytes);
assertEquals(bytesStr, s);
assertEquals(buf.length, s.length);
}
// Fill buf through n writes of byte slice fub.
// The initial contents of buf corresponds to the string s;
// the result is the final contents of buf returned as a string.
function fillBytes(
buf: DenoBuffer,
s: string,
n: number,
fub: Uint8Array,
): string {
check(buf, s);
for (; n > 0; n--) {
const m = buf.write(fub);
assertEquals(m, fub.byteLength);
const decoder = new TextDecoder();
s += decoder.decode(fub);
check(buf, s);
}
return s;
}
// Empty buf through repeated reads into fub.
// The initial contents of buf corresponds to the string s.
function empty(
buf: DenoBuffer,
s: string,
fub: Uint8Array,
): void {
check(buf, s);
while (true) {
const r = buf.read(fub);
if (r === null) {
break;
}
s = s.slice(r);
check(buf, s);
}
check(buf, "");
}
function repeat(c: string, bytes: number): Uint8Array {
assertEquals(c.length, 1);
const ui8 = new Uint8Array(bytes);
ui8.fill(c.charCodeAt(0));
return ui8;
}
Deno.test("buffer - new buffer", () => {
init();
assert(testBytes);
assert(testString);
const buf = new DenoBuffer(testBytes.buffer as ArrayBuffer);
check(buf, testString);
});
Deno.test("buffer - basic operations", () => {
init();
assert(testBytes);
assert(testString);
const buf = new DenoBuffer();
for (let i = 0; i < 5; i++) {
check(buf, "");
buf.reset();
check(buf, "");
buf.truncate(0);
check(buf, "");
let n = buf.write(testBytes.subarray(0, 1));
assertEquals(n, 1);
check(buf, "a");
n = buf.write(testBytes.subarray(1, 2));
assertEquals(n, 1);
check(buf, "ab");
n = buf.write(testBytes.subarray(2, 26));
assertEquals(n, 24);
check(buf, testString.slice(0, 26));
buf.truncate(26);
check(buf, testString.slice(0, 26));
buf.truncate(20);
check(buf, testString.slice(0, 20));
empty(buf, testString.slice(0, 20), new Uint8Array(5));
empty(buf, "", new Uint8Array(100));
}
});
Deno.test("buffer - read/write byte", () => {
init();
assert(testBytes);
assert(testString);
const buf = new DenoBuffer();
buf.writeByte("a".charCodeAt(0));
const a = String.fromCharCode(buf.readByte()!);
assertEquals(a, "a");
});
Deno.test("buffer - write string", () => {
const buf = new DenoBuffer();
const s = "MSG a 1 b 6\r\nfoobar";
buf.writeString(s);
const rs = new TextDecoder().decode(buf.bytes());
assertEquals(rs, s);
});
Deno.test("buffer - write empty string", () => {
const buf = new DenoBuffer();
buf.writeString("");
assertEquals(buf.length, 0);
assertEquals(buf.capacity, 0);
});
Deno.test("buffer - read empty at EOF", () => {
// check that EOF of 'buf' is not reached (even though it's empty) if
// results are written to buffer that has 0 length (ie. it can't store any data)
const buf = new DenoBuffer();
const zeroLengthTmp = new Uint8Array(0);
const result = buf.read(zeroLengthTmp);
assertEquals(result, 0);
});
Deno.test("buffer - large byte writes", () => {
init();
const buf = new DenoBuffer();
const limit = 9;
for (let i = 3; i < limit; i += 3) {
const s = fillBytes(buf, "", 5, testBytes!);
empty(buf, s, new Uint8Array(Math.floor(testString!.length / i)));
}
check(buf, "");
});
Deno.test("buffer - too large byte writes", () => {
init();
const tmp = new Uint8Array(72);
const growLen = Number.MAX_VALUE;
const xBytes = repeat("x", 0);
const buf = new DenoBuffer(xBytes.buffer as ArrayBuffer);
buf.read(tmp);
assertThrows(
() => {
buf.grow(growLen);
},
Error,
"grown beyond the maximum size",
);
});
Deno.test("buffer - grow write max buffer", () => {
const bufSize = 16 * 1024;
const capacities = [MAX_SIZE, MAX_SIZE - 1];
for (const capacity of capacities) {
let written = 0;
const buf = new DenoBuffer();
const writes = Math.floor(capacity / bufSize);
for (let i = 0; i < writes; i++) {
written += buf.write(repeat("x", bufSize));
}
if (written < capacity) {
written += buf.write(repeat("x", capacity - written));
}
assertEquals(written, capacity);
}
});
Deno.test("buffer - grow read close max buffer plus 1", () => {
const reader = new DenoBuffer(new ArrayBuffer(MAX_SIZE + 1));
const buf = new DenoBuffer();
assertThrows(
() => {
buf.readFrom(reader);
},
Error,
"grown beyond the maximum size",
);
});
Deno.test("buffer - grow read close to max buffer", () => {
const capacities = [MAX_SIZE, MAX_SIZE - 1];
for (const capacity of capacities) {
const reader = new DenoBuffer(new ArrayBuffer(capacity));
const buf = new DenoBuffer();
buf.readFrom(reader);
assertEquals(buf.length, capacity);
}
});
Deno.test("buffer - read close to max buffer with initial grow", () => {
const capacities = [MAX_SIZE, MAX_SIZE - 1, MAX_SIZE - 512];
for (const capacity of capacities) {
const reader = new DenoBuffer(new ArrayBuffer(capacity));
const buf = new DenoBuffer();
buf.grow(MAX_SIZE);
buf.readFrom(reader);
assertEquals(buf.length, capacity);
}
});
Deno.test("buffer - large byte reads", () => {
init();
assert(testBytes);
assert(testString);
const buf = new DenoBuffer();
for (let i = 3; i < 30; i += 3) {
const n = Math.floor(testBytes.byteLength / i);
const s = fillBytes(buf, "", 5, testBytes.subarray(0, n));
empty(buf, s, new Uint8Array(testString.length));
}
check(buf, "");
});
Deno.test("buffer - cap with pre allocated slice", () => {
const buf = new DenoBuffer(new ArrayBuffer(10));
assertEquals(buf.capacity, 10);
});
Deno.test("buffer - read from sync", () => {
init();
assert(testBytes);
assert(testString);
const buf = new DenoBuffer();
for (let i = 3; i < 30; i += 3) {
const s = fillBytes(
buf,
"",
5,
testBytes.subarray(0, Math.floor(testBytes.byteLength / i)),
);
const b = new DenoBuffer();
b.readFrom(buf);
const fub = new Uint8Array(testString.length);
empty(b, s, fub);
}
assertThrows(() => {
new DenoBuffer().readFrom(null!);
});
});
Deno.test("buffer - test grow", () => {
const tmp = new Uint8Array(72);
for (const startLen of [0, 100, 1000, 10000, 100000]) {
const xBytes = repeat("x", startLen);
for (const growLen of [0, 100, 1000, 10000, 100000]) {
const buf = new DenoBuffer(xBytes.buffer as ArrayBuffer);
// If we read, this affects buf.off, which is good to test.
const nread = (buf.read(tmp)) ?? 0;
buf.grow(growLen);
const yBytes = repeat("y", growLen);
buf.write(yBytes);
// Check that buffer has correct data.
assertEquals(
buf.bytes().subarray(0, startLen - nread),
xBytes.subarray(nread),
);
assertEquals(
buf.bytes().subarray(startLen - nread, startLen - nread + growLen),
yBytes,
);
}
}
});
Deno.test("buffer - read all", () => {
init();
assert(testBytes);
const reader = new DenoBuffer(testBytes.buffer as ArrayBuffer);
const actualBytes = readAll(reader);
assertEquals(testBytes.byteLength, actualBytes.byteLength);
for (let i = 0; i < testBytes.length; ++i) {
assertEquals(testBytes[i], actualBytes[i]);
}
});
Deno.test("buffer - write all", () => {
init();
assert(testBytes);
const writer = new DenoBuffer();
writeAll(writer, testBytes);
const actualBytes = writer.bytes();
assertEquals(testBytes.byteLength, actualBytes.byteLength);
for (let i = 0; i < testBytes.length; ++i) {
assertEquals(testBytes[i], actualBytes[i]);
}
});
Deno.test("buffer - bytes array buffer length", () => {
// defaults to copy
const args = [{}, { copy: undefined }, undefined, { copy: true }];
for (const arg of args) {
const bufSize = 64 * 1024;
const bytes = new TextEncoder().encode("a".repeat(bufSize));
const reader = new DenoBuffer();
writeAll(reader, bytes);
const writer = new DenoBuffer();
writer.readFrom(reader);
const actualBytes = writer.bytes(arg);
assertEquals(actualBytes.byteLength, bufSize);
assert(actualBytes.buffer !== writer.bytes(arg).buffer);
assertEquals(actualBytes.byteLength, actualBytes.buffer.byteLength);
}
});
Deno.test("buffer - bytes copy false", () => {
const bufSize = 64 * 1024;
const bytes = new TextEncoder().encode("a".repeat(bufSize));
const reader = new DenoBuffer();
writeAll(reader, bytes);
const writer = new DenoBuffer();
writer.readFrom(reader);
const actualBytes = writer.bytes({ copy: false });
assertEquals(actualBytes.byteLength, bufSize);
assertEquals(actualBytes.buffer, writer.bytes({ copy: false }).buffer);
assert(actualBytes.buffer.byteLength > actualBytes.byteLength);
});
Deno.test("buffer - bytes copy false grow exact bytes", () => {
const bufSize = 64 * 1024;
const bytes = new TextEncoder().encode("a".repeat(bufSize));
const reader = new DenoBuffer();
writeAll(reader, bytes);
const writer = new DenoBuffer();
writer.grow(bufSize);
writer.readFrom(reader);
const actualBytes = writer.bytes({ copy: false });
assertEquals(actualBytes.byteLength, bufSize);
assertEquals(actualBytes.buffer.byteLength, actualBytes.byteLength);
});
Deno.test("buffer - concat", () => {
let buf = concat();
assertEquals(buf.length, 0);
const a = new Uint8Array([1]);
const b = new Uint8Array([2]);
buf = concat(undefined, b);
assertEquals(buf.length, 1);
assertEquals(buf[0], 2);
buf = concat(a);
assertEquals(buf.length, 1);
assertEquals(buf[0], 1);
buf = concat(a, b);
assertEquals(buf.length, 2);
assertEquals(buf[0], 1);
assertEquals(buf[1], 2);
});
Deno.test("buffer - append", () => {
let buf = append(new Uint8Array(0), 1);
assertEquals(buf.length, 1);
assertEquals(buf[0], 1);
buf = append(buf, 2);
assertEquals(buf.length, 2);
assertEquals(buf[0], 1);
assertEquals(buf[1], 2);
});
Deno.test("buffer - assert", () => {
assertThrows(
() => {
buffer_assert(false, "test");
},
AssertionError,
"test",
);
});
--- core/tests/clobber_test.ts ---
/*
* 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 { NatsServer } from "../../test_helpers/launcher.ts";
import { createInbox, DataBuffer } from "../src/internal_mod.ts";
import { connect } from "./connect.ts";
import { assertEquals } from "@std/assert";
function makeBuffer(N: number): Uint8Array {
const buf = new Uint8Array(N);
for (let i = 0; i < N; i++) {
buf[i] = "a".charCodeAt(0) + (i % 26);
}
return buf;
}
Deno.test("clobber - buffers don't clobber", async () => {
const iters = 250 * 1024;
const data = makeBuffer(iters * 1024);
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port });
const subj = createInbox();
const sub = nc.subscribe(subj, { max: iters });
const payloads = new DataBuffer();
const iter = (async () => {
for await (const m of sub) {
payloads.fill(m.data);
}
})();
let bytes = 0;
for (let i = 0; i < iters; i++) {
bytes += 1024;
const start = i * 1024;
nc.publish(subj, data.subarray(start, start + 1024));
}
await iter;
const td = new TextDecoder();
for (let i = 0; i < iters; i++) {
const start = i * 1024;
const r = td.decode(payloads.drain(1024));
const w = td.decode(data.subarray(start, start + 1024));
assertEquals(r, w);
}
await nc.close();
await ns.stop();
});
--- core/tests/connect.ts ---
export { connect } from "../../test_helpers/connect.ts";
--- core/tests/databuffer_test.ts ---
/*
* Copyright 2018-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 { assertEquals } from "@std/assert";
import { DataBuffer } from "../src/internal_mod.ts";
Deno.test("databuffer - empty", () => {
const buf = new DataBuffer();
assertEquals(0, buf.length());
assertEquals(0, buf.size());
assertEquals(0, buf.drain(1000).byteLength);
assertEquals(0, buf.peek().byteLength);
});
Deno.test("databuffer - simple", () => {
const buf = new DataBuffer();
buf.fill(DataBuffer.fromAscii("Hello"));
buf.fill(DataBuffer.fromAscii(" "));
buf.fill(DataBuffer.fromAscii("World"));
assertEquals(3, buf.length());
assertEquals(11, buf.size());
const p = buf.peek();
assertEquals(11, p.byteLength);
assertEquals("Hello World", DataBuffer.toAscii(p));
const d = buf.drain();
assertEquals(11, d.byteLength);
assertEquals("Hello World", DataBuffer.toAscii(d));
});
Deno.test("databuffer - from empty", () => {
//@ts-ignore: bad argument to fn
const a = DataBuffer.fromAscii(undefined);
assertEquals(0, a.byteLength);
});
Deno.test("databuffer - multi-fill", () => {
const buf = new DataBuffer();
const te = new TextEncoder();
buf.fill(te.encode("zero"));
assertEquals(buf.length(), 1);
assertEquals(buf.byteLength, 4);
buf.fill(te.encode("one"), te.encode("two"));
assertEquals(buf.length(), 3);
assertEquals(buf.byteLength, 10);
});
--- core/tests/disconnect_test.ts ---
/*
* Copyright 2018-2021 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 } from "./connect.ts";
import { Lock, NatsServer } from "test_helpers";
import type { NatsConnectionImpl } from "../src/internal_mod.ts";
Deno.test("disconnect - close handler is called on close", async () => {
const ns = await NatsServer.start();
const lock = Lock(1);
const nc = await connect(
{ port: ns.port, reconnect: false },
);
nc.closed().then(() => {
lock.unlock();
});
await ns.stop();
await lock;
});
Deno.test("disconnect - close process inbound ignores", async () => {
const ns = await NatsServer.start();
const lock = Lock(1);
const nc = await connect(
{ port: ns.port, reconnect: false },
) as NatsConnectionImpl;
nc.closed().then(() => {
lock.unlock();
});
await ns.stop();
await lock;
});
--- core/tests/doublesubs_test.ts ---
/*
* 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 { NatsServer } from "../../test_helpers/launcher.ts";
import { deferred, Empty, extend, headers } from "../src/internal_mod.ts";
import type { NatsConnectionImpl } from "../src/internal_mod.ts";
import { assertArrayIncludes, assertEquals } from "@std/assert";
import { connect } from "./connect.ts";
async function runDoubleSubsTest(tls: boolean) {
const tlsConfig = await NatsServer.tlsConfig();
let opts = { trace: true, host: "0.0.0.0" };
if (tls) {
opts = extend(opts, { tls: tlsConfig.tls });
}
let srv = await NatsServer.start(opts);
let connOpts = {
servers: `localhost:${srv.port}`,
reconnectTimeWait: 500,
maxReconnectAttempts: -1,
headers: true,
};
const cert = {
tls: {
caFile: tlsConfig.tls.ca_file,
},
};
if (tls) {
connOpts = extend(connOpts, cert);
}
const nc = await connect(connOpts) as NatsConnectionImpl;
const disconnected = deferred<void>();
const reconnected = deferred<void>();
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "disconnect":
disconnected.resolve();
break;
case "reconnect":
reconnected.resolve();
break;
}
}
})().then();
await nc.flush();
await srv.stop();
await disconnected;
const foo = nc.subscribe("foo");
const bar = nc.subscribe("bar");
const baz = nc.subscribe("baz");
nc.publish("foo", Empty);
nc.publish("bar", "hello");
const h = headers();
h.set("foo", "bar");
nc.publish("baz", Empty, { headers: h });
srv = await srv.restart();
await reconnected;
await nc.flush();
// pubs are stripped
assertEquals(foo.getReceived(), 0);
assertEquals(bar.getReceived(), 0);
assertEquals(baz.getReceived(), 0);
await nc.close();
await srv.stop();
const log = srv.getLog();
let count = 0;
const subs: string[] = [];
const sub = /\[SUB (\S+) \d]/;
log.split("\n").forEach((s) => {
const m = sub.exec(s);
if (m) {
count++;
subs.push(m[1]);
}
});
await Deno.remove(tlsConfig.certsDir, { recursive: true });
assertEquals(count, 3);
assertArrayIncludes(subs, ["foo", "bar", "baz"]);
}
Deno.test("doublesubs - standard", async () => {
await runDoubleSubsTest(false);
});
Deno.test("doublesubs - tls", async () => {
await runDoubleSubsTest(true);
});
--- core/tests/drain_test.ts ---
/*
* 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, assertRejects, assertThrows } from "@std/assert";
import { createInbox } from "../src/internal_mod.ts";
import { Lock } from "test_helpers";
import { cleanup, setup } from "test_helpers";
import { connect } from "./connect.ts";
import { errors } from "../src/errors.ts";
Deno.test("drain - connection drains when no subs", async () => {
const { ns, nc } = await setup();
await nc.drain();
await cleanup(ns);
});
Deno.test("drain - connection drain", async () => {
const { ns, nc } = await setup();
const nc2 = await connect({ port: ns.port });
const max = 1000;
const lock = Lock(max);
const subj = createInbox();
let first = true;
await nc.subscribe(subj, {
callback: () => {
lock.unlock();
if (first) {
first = false;
nc.drain();
}
},
queue: "q1",
});
let count = 0;
await nc2.subscribe(subj, {
callback: () => {
lock.unlock();
count++;
},
queue: "q1",
});
await nc.flush();
await nc2.flush();
for (let i = 0; i < max; i++) {
nc2.publish(subj);
}
await nc2.drain();
await lock;
await nc.closed();
assert(count > 0, "expected second connection to get some messages");
await ns.stop();
});
Deno.test("drain - subscription drain", async () => {
const { ns, nc } = await setup();
const lock = Lock();
const subj = createInbox();
let c1 = 0;
const s1 = nc.subscribe(subj, {
callback: () => {
c1++;
if (!s1.isDraining()) {
// resolve when done
s1.drain()
.then(() => {
lock.unlock();
});
}
},
queue: "q1",
});
let c2 = 0;
nc.subscribe(subj, {
callback: () => {
c2++;
},
queue: "q1",
});
for (let i = 0; i < 10000; i++) {
nc.publish(subj);
}
await nc.flush();
await lock;
assertEquals(c1 + c2, 10000);
assert(c1 >= 1, "s1 got more than one message");
assert(c2 >= 1, "s2 got more than one message");
assert(s1.isClosed());
await cleanup(ns, nc);
});
Deno.test("drain - publish after drain fails", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
nc.subscribe(subj);
await nc.drain();
try {
nc.publish(subj);
} catch (err) {
assert(
err instanceof errors.ClosedConnectionError ||
err instanceof errors.DrainingConnectionError,
);
}
await ns.stop();
});
Deno.test("drain - reject reqrep during connection drain", async () => {
const { ns, nc } = await setup();
const done = nc.drain();
await assertRejects(() => {
return nc.request("foo");
}, errors.DrainingConnectionError);
await done;
await cleanup(ns, nc);
});
Deno.test("drain - reject drain on closed", async () => {
const { ns, nc } = await setup();
await nc.close();
await assertRejects(() => {
return nc.drain();
}, errors.ClosedConnectionError);
await ns.stop();
});
Deno.test("drain - reject drain on draining", async () => {
const { ns, nc } = await setup();
const done = nc.drain();
await assertRejects(() => {
return nc.drain();
}, errors.DrainingConnectionError);
await done;
await ns.stop();
});
Deno.test("drain - reject subscribe on draining", async () => {
const { ns, nc } = await setup();
const done = nc.drain();
assertThrows(() => {
return nc.subscribe("foo");
}, errors.DrainingConnectionError);
await done;
await ns.stop();
});
Deno.test("drain - reject subscription drain on closed sub callback", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe("foo", { callback: () => {} });
sub.unsubscribe();
await assertRejects(
() => {
return sub.drain();
},
errors.InvalidOperationError,
"subscription is already closed",
);
await nc.close();
await ns.stop();
});
Deno.test("drain - reject subscription drain on closed sub iter", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe("foo");
const d = (async () => {
for await (const _ of sub) {
// nothing
}
})().then();
sub.unsubscribe();
await d;
await assertRejects(
() => {
return sub.drain();
},
errors.InvalidOperationError,
"subscription is already closed",
);
await nc.close();
await ns.stop();
});
Deno.test("drain - connection is closed after drain", async () => {
const { ns, nc } = await setup();
nc.subscribe("foo");
await nc.drain();
assert(nc.isClosed());
await ns.stop();
});
Deno.test("drain - reject subscription drain on closed", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe("foo");
await nc.close();
await assertRejects(() => {
return sub.drain();
}, errors.ClosedConnectionError);
await ns.stop();
});
Deno.test("drain - multiple sub drain returns same promise", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj);
const p1 = sub.drain();
const p2 = sub.drain();
assertEquals(p1, p2);
nc.publish(subj);
await nc.flush();
await p1;
await cleanup(ns, nc);
});
Deno.test("drain - publisher drain", async () => {
const { ns, nc } = await setup();
const nc1 = await connect({ port: ns.port });
const subj = createInbox();
const lock = Lock(10);
nc.subscribe(subj, {
callback: () => {
lock.unlock();
},
});
await nc.flush();
for (let i = 0; i < 10; i++) {
nc1.publish(subj);
}
await nc1.drain();
await lock;
await cleanup(ns, nc, nc1);
});
--- core/tests/encoders_test.ts ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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 { assertEquals } from "@std/assert";
import { decode, Empty, encode } from "../src/encoders.ts";
Deno.test("encoders - basics", () => {
let buf = encode();
assertEquals(buf, Empty);
buf = encode("1", "2", "3", "4");
assertEquals(buf, Uint8Array.from([49, 50, 51, 52]));
const s = decode(buf);
assertEquals(s, "1234");
assertEquals(decode(Empty), "");
});
--- core/tests/events_test.ts ---
/*
* Copyright 2021-2024 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 { Lock, NatsServer, ServerSignals } from "test_helpers";
import { connect } from "./connect.ts";
import { assertEquals } from "@std/assert";
import { deferred, delay } from "../src/internal_mod.ts";
import type { NatsConnectionImpl } from "../src/internal_mod.ts";
import { setup } from "test_helpers";
import { cleanup } from "../../test_helpers/mod.ts";
Deno.test("events - close on close", async () => {
const { ns, nc } = await setup();
nc.close().then();
const status = await nc.closed();
await ns.stop();
assertEquals(status, undefined);
});
Deno.test("events - disconnect and close", async () => {
const lock = Lock(2);
const { ns, nc } = await setup({}, { reconnect: false });
(async () => {
for await (const s of nc.status()) {
switch (s.type) {
case "disconnect":
lock.unlock();
break;
}
}
})().then();
nc.closed().then(() => {
lock.unlock();
});
await ns.stop();
await lock;
const v = await nc.closed();
assertEquals(v, undefined);
});
Deno.test("events - disreconnect", async () => {
const cluster = await NatsServer.cluster();
const nc = await connect(
{
servers: `${cluster[0].hostname}:${cluster[0].port}`,
maxReconnectAttempts: 1,
reconnectTimeWait: 0,
},
);
const disconnect = Lock();
const reconnect = Lock();
(async () => {
for await (const s of nc.status()) {
switch (s.type) {
case "disconnect":
disconnect.unlock();
break;
case "reconnect":
reconnect.unlock();
break;
}
}
})().then();
await cluster[0].stop();
await Promise.all([disconnect, reconnect]);
await nc.close();
await NatsServer.stopAll(cluster, true);
});
Deno.test("events - update", async () => {
const cluster = await NatsServer.cluster(1);
const nc = await connect(
{
servers: `127.0.0.1:${cluster[0].port}`,
},
);
const lock = Lock(1, 5000);
(async () => {
for await (const s of nc.status()) {
switch (s.type) {
case "update": {
assertEquals(s.added?.length, 1);
lock.unlock();
break;
}
}
}
})().then();
await delay(250);
const s = await NatsServer.addClusterMember(cluster[0]);
cluster.push(s);
await lock;
await nc.close();
await NatsServer.stopAll(cluster, true);
});
Deno.test("events - ldm", async () => {
const cluster = await NatsServer.cluster(2);
const nc = await connect(
{
servers: `127.0.0.1:${cluster[0].port}`,
},
);
const lock = Lock(1, 5000);
(async () => {
for await (const s of nc.status()) {
switch (s.type) {
case "ldm":
lock.unlock();
break;
}
}
})().then();
await cluster[0].signal(ServerSignals.LDM);
await lock;
await nc.close();
await NatsServer.stopAll(cluster, true);
});
Deno.test("events - ignore server updates", async () => {
const cluster = await NatsServer.cluster(1);
const nc = await connect(
{
servers: `127.0.0.1:${cluster[0].port}`,
ignoreClusterUpdates: true,
},
) as NatsConnectionImpl;
assertEquals(nc.protocol.servers.length(), 1);
const s = await NatsServer.addClusterMember(cluster[0]);
cluster.push(s);
await NatsServer.localClusterFormed(cluster);
await nc.flush();
assertEquals(nc.protocol.servers.length(), 1);
await nc.close();
await NatsServer.stopAll(cluster, true);
});
Deno.test("events - clean up", async () => {
const { ns, nc } = await setup();
const finished = deferred();
const done = (async () => {
for await (const _ of nc.status()) {
// nothing
}
// let's make sure the iter broke...
finished.resolve();
})();
await nc.reconnect();
await nc.close();
await finished;
await done;
await nc.closed();
await cleanup(ns, nc);
});
--- core/tests/headers_test.ts ---
/*
* 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 { connect } from "./connect.ts";
import {
canonicalMIMEHeaderKey,
createInbox,
Empty,
headers,
Match,
MsgHdrsImpl,
MsgImpl,
Parser,
} from "../src/internal_mod.ts";
import type {
NatsConnectionImpl,
Publisher,
RequestOptions,
} from "../src/internal_mod.ts";
import { NatsServer } from "../../test_helpers/launcher.ts";
import { assert, assertEquals, assertFalse, assertThrows } from "@std/assert";
import { TestDispatcher } from "./parser_test.ts";
import { cleanup, setup } from "test_helpers";
import { errors } from "../src/errors.ts";
Deno.test("headers - illegal key", () => {
const h = headers();
["bad:", "bad ", String.fromCharCode(127)].forEach((v) => {
assertThrows(
() => {
h.set(v, "aaa");
},
errors.InvalidArgumentError,
"is not a valid character in a header name",
);
});
["\r", "\n"].forEach((v) => {
assertThrows(
() => {
h.set("a", v);
},
errors.InvalidArgumentError,
"values cannot contain \\r or \\n",
);
});
});
Deno.test("headers - case sensitive", () => {
const h = headers() as MsgHdrsImpl;
h.set("a", "a");
assert(h.has("a"));
assert(!h.has("A"));
h.set("A", "A");
assert(h.has("A"));
assertEquals(h.size(), 2);
assertEquals(h.get("a"), "a");
assertEquals(h.values("a"), ["a"]);
assertEquals(h.get("A"), "A");
assertEquals(h.values("A"), ["A"]);
h.append("a", "aa");
h.append("A", "AA");
assertEquals(h.size(), 2);
assertEquals(h.values("a"), ["a", "aa"]);
assertEquals(h.values("A"), ["A", "AA"]);
h.delete("a");
assert(!h.has("a"));
assert(h.has("A"));
h.set("A", "AAA");
assertEquals(h.values("A"), ["AAA"]);
});
Deno.test("headers - case insensitive", () => {
const h = headers() as MsgHdrsImpl;
h.set("a", "a", Match.IgnoreCase);
// set replaces
h.set("A", "A", Match.IgnoreCase);
assertEquals(h.size(), 1);
assert(h.has("a", Match.IgnoreCase));
assert(h.has("A", Match.IgnoreCase));
assertEquals(h.values("a", Match.IgnoreCase), ["A"]);
assertEquals(h.values("A", Match.IgnoreCase), ["A"]);
h.append("a", "aa");
assertEquals(h.size(), 2);
const v = h.values("a", Match.IgnoreCase);
v.sort();
assertEquals(v, ["A", "aa"]);
h.delete("a", Match.IgnoreCase);
assertEquals(h.size(), 0);
});
Deno.test("headers - mime", () => {
const h = headers() as MsgHdrsImpl;
h.set("ab", "ab", Match.CanonicalMIME);
assert(!h.has("ab"));
assert(h.has("Ab"));
// set replaces
h.set("aB", "A", Match.CanonicalMIME);
assertEquals(h.size(), 1);
assert(h.has("Ab"));
h.append("ab", "aa", Match.CanonicalMIME);
assertEquals(h.size(), 1);
const v = h.values("ab", Match.CanonicalMIME);
v.sort();
assertEquals(v, ["A", "aa"]);
h.delete("ab", Match.CanonicalMIME);
assertEquals(h.size(), 0);
});
Deno.test("headers - publish has headers", async () => {
const srv = await NatsServer.start();
const nc = await connect(
{
port: srv.port,
},
);
const h = headers();
h.set("a", "aa");
h.set("b", "bb");
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 1 });
const done = (async () => {
for await (const m of sub) {
assert(m.headers);
const mh = m.headers as MsgHdrsImpl;
assertEquals(mh.size(), 2);
assert(mh.has("a"));
assert(mh.has("b"));
}
})();
nc.publish(subj, Empty, { headers: h });
await done;
await nc.close();
await srv.stop();
});
Deno.test("headers - request has headers", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
});
const s = createInbox();
const sub = nc.subscribe(s, { max: 1 });
const done = (async () => {
for await (const m of sub) {
m.respond(Empty, { headers: m.headers });
}
})();
const opts = {} as RequestOptions;
opts.headers = headers();
opts.headers.set("x", "X");
const msg = await nc.request(s, Empty, opts);
assert(msg.headers);
const mh = msg.headers;
assert(mh.has("x"));
await done;
await nc.close();
await srv.stop();
});
function status(code: number, description: string): Uint8Array {
const status = code
? `NATS/1.0 ${code.toString()} ${description}`.trim()
: "NATS/1.0";
const line = `${status}\r\n\r\n\r\n`;
return new TextEncoder().encode(line);
}
function checkStatus(code = 200, description = "") {
const h = MsgHdrsImpl.decode(status(code, description));
const isErrorCode = code > 0 && (code < 200 || code >= 300);
assertEquals(h.hasError, isErrorCode);
if (code > 0) {
assertEquals(h.code, code);
assertEquals(h.description, description);
assertEquals(h.status, `${code} ${description}`.trim());
}
assertEquals(h.description, description);
}
Deno.test("headers - status", () => {
checkStatus(0, "");
checkStatus(200, "");
checkStatus(200, "OK");
checkStatus(503, "No Responders");
checkStatus(404, "No Messages");
});
Deno.test("headers - equality", () => {
const a = headers() as MsgHdrsImpl;
const b = headers() as MsgHdrsImpl;
assert(a.equals(b));
a.set("a", "b");
b.set("a", "b");
assert(a.equals(b));
b.append("a", "bb");
assert(!a.equals(b));
a.append("a", "cc");
assert(!a.equals(b));
});
Deno.test("headers - canonical", () => {
assertEquals(canonicalMIMEHeaderKey("foo"), "Foo");
assertEquals(canonicalMIMEHeaderKey("foo-bar"), "Foo-Bar");
assertEquals(canonicalMIMEHeaderKey("foo-bar-baz"), "Foo-Bar-Baz");
});
Deno.test("headers - append ignore case", () => {
const h = headers() as MsgHdrsImpl;
h.set("a", "a");
h.append("A", "b", Match.IgnoreCase);
assertEquals(h.size(), 1);
assertEquals(h.values("a"), ["a", "b"]);
});
Deno.test("headers - append exact case", () => {
const h = headers() as MsgHdrsImpl;
h.set("a", "a");
h.append("A", "b");
assertEquals(h.size(), 2);
assertEquals(h.values("a"), ["a"]);
assertEquals(h.values("A"), ["b"]);
});
Deno.test("headers - append canonical", () => {
const h = headers() as MsgHdrsImpl;
h.set("a", "a");
h.append("A", "b", Match.CanonicalMIME);
assertEquals(h.size(), 2);
assertEquals(h.values("a"), ["a"]);
assertEquals(h.values("A"), ["b"]);
});
Deno.test("headers - malformed header are ignored", () => {
const te = new TextEncoder();
const d = new TestDispatcher();
const p = new Parser(d);
p.parse(
te.encode(`HMSG SUBJECT 1 REPLY 17 17\r\nNATS/1.0\r\nBAD\r\n\r\n\r\n`),
);
assertEquals(d.errs.length, 0);
assertEquals(d.msgs.length, 1);
const e = d.msgs[0];
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
const hi = m.headers as MsgHdrsImpl;
assertEquals(hi.size(), 0);
});
Deno.test("headers - handles no space", () => {
const te = new TextEncoder();
const d = new TestDispatcher();
const p = new Parser(d);
p.parse(
te.encode(`HMSG SUBJECT 1 REPLY 17 17\r\nNATS/1.0\r\nA:A\r\n\r\n\r\n`),
);
assertEquals(d.errs.length, 0);
assertEquals(d.msgs.length, 1);
const e = d.msgs[0];
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
assert(m.headers);
assertEquals(m.headers.get("A"), "A");
});
Deno.test("headers - trims values", () => {
const te = new TextEncoder();
const d = new TestDispatcher();
const p = new Parser(d);
p.parse(
te.encode(
`HMSG SUBJECT 1 REPLY 23 23\r\nNATS/1.0\r\nA: A \r\n\r\n\r\n`,
),
);
assertEquals(d.errs.length, 0);
assertEquals(d.msgs.length, 1);
const e = d.msgs[0];
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
assert(m.headers);
assertEquals(m.headers.get("A"), "A");
});
Deno.test("headers - error headers may have other entries", () => {
const te = new TextEncoder();
const d = new TestDispatcher();
const p = new Parser(d);
p.parse(
te.encode(
`HMSG _INBOX.DJ2IU18AMXPOZMG5R7NJVI 2 75 75\r\nNATS/1.0 100 Idle Heartbeat\r\nNats-Last-Consumer: 1\r\nNats-Last-Stream: 1\r\n\r\n\r\n`,
),
);
assertEquals(d.msgs.length, 1);
const e = d.msgs[0];
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
assert(m.headers);
assertEquals(m.headers.get("Nats-Last-Consumer"), "1");
assertEquals(m.headers.get("Nats-Last-Stream"), "1");
});
Deno.test("headers - code/description", () => {
assertThrows(
() => {
headers(500);
},
Error,
"'description' is required",
);
assertThrows(
() => {
headers(0, "some message");
},
Error,
"'description' is required",
);
});
Deno.test("headers - codec", async () => {
const { ns, nc } = await setup({}, {});
nc.subscribe("foo", {
callback: (_err, msg) => {
const h = headers(500, "custom status from client");
msg.respond(Empty, { headers: h });
},
});
const r = await nc.request("foo", Empty);
assertEquals(r.headers?.code, 500);
assertEquals(r.headers?.description, "custom status from client");
await cleanup(ns, nc);
});
Deno.test("headers - malformed headers", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
type t = {
proto: string;
expected: {
payload: string;
code?: number;
description?: string;
key?: string;
value?: string;
};
};
const h = headers(1, "h");
nc.publish("foo", Empty, { headers: h });
const tests: t[] = [
{
// extra spaces after subject, only new line after lengths
// trailing space after default status - this resulted in a
// NaN for the code but no crash
proto: "HPUB foo 13 15\nNATS/1.0 \r\n\r\nhi\r\n",
expected: {
payload: "hi",
code: 0,
description: "",
},
},
{
// extra spaces and pub lengths not followed by crlf
// status line followed by extra spaces etc
proto: "HPUB foo 17 19\nNATS/1.0 1 H\r\n\r\nhi\r\n",
expected: {
payload: "hi",
code: 1,
description: "H",
},
},
{
// server will convert this to msg, so no headers
proto: "HPUB foo 0 0\r\n\r\n",
expected: {
payload: "",
},
},
{
// this was the issue that broke java client
proto: "HPUB foo 12 12\r\nNATS/1.0\r\n\r\n\r\n",
expected: {
payload: "",
code: 0,
description: "",
},
},
];
const sub = nc.subscribe("foo", { max: tests.length });
let i = 0;
const done = (async () => {
for await (const m of sub) {
const t = tests[i++];
assertEquals(m.string(), t.expected.payload);
if (typeof t.expected.code === "number") {
assertEquals(m.headers?.code, t.expected.code);
assertEquals(m.headers?.description, t.expected.description);
if (t.expected.key) {
assertEquals(m.headers?.get(t.expected.key), t.expected.value);
}
}
}
})();
tests.forEach((v) => {
nci.protocol.sendCommand(v.proto);
});
await done;
await cleanup(ns, nc);
});
Deno.test("headers - iterator", () => {
const h = headers();
h.set("a", "aa");
h.set("b", "bb");
h.set("c", "cc");
let c = 0;
const abc = ["a", "b", "c"];
for (const tuple of h) {
const letter = abc[c];
assertEquals(tuple[0], letter);
assertEquals(tuple[1][0], `${letter}${letter}`);
c++;
}
assertEquals(c, 3);
});
Deno.test("headers - last", () => {
const h = headers();
h.set("a", "aa");
h.append("b", "bb");
h.append("b", "bbb");
assertEquals(h.last("foo"), "");
assertEquals(h.last("a"), "aa");
assertEquals(h.last("b"), "bbb");
});
Deno.test("headers - record", () => {
const h = headers();
h.set("a", "aa");
h.append("b", "bb");
h.append("b", "bbb");
const r = (h as MsgHdrsImpl).toRecord();
assertEquals(r, {
a: ["aa"],
b: ["bb", "bbb"],
});
const hr = MsgHdrsImpl.fromRecord(r) as MsgHdrsImpl;
assertEquals(hr.get("foo"), "");
assertEquals(h.get("a"), "aa");
assertEquals(h.get("b"), "bb");
assertEquals(h.last("b"), "bbb");
assert(hr.equals(h as MsgHdrsImpl));
});
Deno.test("headers - not equals", () => {
const h = headers() as MsgHdrsImpl;
h.set("a", "aa");
h.append("b", "bb");
h.append("b", "bbb");
assertFalse(h.equals(headers() as MsgHdrsImpl));
});
Deno.test("headers - empty", () => {
const h = headers() as MsgHdrsImpl;
assertEquals(h.toString(), "");
});
--- core/tests/heartbeats_test.ts ---
/*
* 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, fail } from "@std/assert";
import { deferred, delay, Heartbeat } from "../src/internal_mod.ts";
import type { PH, Status } from "../src/internal_mod.ts";
function pm(
lag: number,
disconnect: () => void,
statusHandler: (s: Status) => void,
skip?: number[],
): PH {
let counter = 0;
return {
flush(): Promise<void> {
counter++;
const d = deferred<void>();
if (skip && skip.indexOf(counter) !== -1) {
return d;
}
delay(lag)
.then(() => d.resolve());
return d;
},
disconnect(): void {
disconnect();
},
dispatchStatus(status: Status): void {
statusHandler(status);
},
};
}
Deno.test("heartbeat - timers fire", async () => {
const status: Status[] = [];
const ph = pm(25, () => {
fail("shouldn't have disconnected");
}, (s: Status): void => {
status.push(s);
});
const hb = new Heartbeat(ph, 100, 3);
hb._schedule();
await delay(400);
assert(hb.timer);
hb.cancel();
// we can have a timer still running here - we need to wait for lag
await delay(50);
assertEquals(hb.timer, undefined);
assert(status.length >= 2, `status ${status.length} >= 2`);
assertEquals(status[0].type, "ping");
});
Deno.test("heartbeat - errors fire on missed maxOut", async () => {
const disconnect = deferred<void>();
const status: Status[] = [];
const ph = pm(25, () => {
disconnect.resolve();
}, (s: Status): void => {
status.push(s);
}, [4, 5, 6]);
const hb = new Heartbeat(ph, 100, 3);
hb._schedule();
await disconnect;
assertEquals(hb.timer, undefined);
assert(status.length >= 7, `${status.length} >= 7`);
assertEquals(status[0].type, "ping");
});
Deno.test("heartbeat - recovers from missed", async () => {
let maxPending = 0;
const d = deferred<void>();
const ph = pm(25, () => {
fail("shouldn't have disconnected");
}, (s: Status): void => {
if (s.type === "ping") {
// increase it
if (s.pendingPings >= maxPending) {
maxPending = s.pendingPings;
} else {
// if lower it recovered
d.resolve();
}
}
}, [4, 5]);
const hb = new Heartbeat(ph, 100, 3);
hb._schedule();
await d;
hb.cancel();
// some resources in the test runner are not always cleaned unless we wait a bit
await delay(500);
});
--- core/tests/idleheartbeats_test.ts ---
/*
* Copyright 2022-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 { deferred, IdleHeartbeatMonitor } from "../src/internal_mod.ts";
import {
assert,
assertEquals,
assertExists,
assertNotEquals,
} from "@std/assert";
Deno.test("idleheartbeat - basic", async () => {
const d = deferred<number>();
const h = new IdleHeartbeatMonitor(250, () => {
d.reject(new Error("didn't expect to notify"));
return true;
});
let count = 0;
const timer = setInterval(() => {
count++;
h.work();
if (count === 8) {
clearInterval(timer);
h.cancel();
d.resolve();
}
}, 100);
await d;
});
Deno.test("idleheartbeat - timeout", async () => {
const d = deferred<number>();
new IdleHeartbeatMonitor(250, (v: number): boolean => {
d.resolve(v);
return true;
}, { maxOut: 1 });
assertEquals(await d, 1);
});
Deno.test("idleheartbeat - timeout maxOut", async () => {
const d = deferred<number>();
new IdleHeartbeatMonitor(250, (v: number): boolean => {
d.resolve(v);
return true;
}, { maxOut: 5 });
assertEquals(await d, 5);
});
Deno.test("idleheartbeat - timeout recover", async () => {
const d = deferred<void>();
const h = new IdleHeartbeatMonitor(250, (_v: number): boolean => {
d.reject(new Error("didn't expect to fail"));
return true;
}, { maxOut: 5 });
setTimeout(() => {
h.cancel();
d.resolve();
clearInterval(interval);
}, 1730);
const interval = setInterval(() => {
h.work();
}, 1000);
await d;
assert(4 >= h.missed, `4 >= ${h.missed}`);
});
Deno.test("idleheartbeat - timeout autocancel", async () => {
const d = deferred();
const h = new IdleHeartbeatMonitor(250, (_v: number): boolean => {
d.reject(new Error("didn't expect to fail"));
return true;
}, { maxOut: 4, cancelAfter: 2000 });
assert(h.autoCancelTimer);
let t = 0;
const timer = setInterval(() => {
h.work();
t++;
if (t === 20) {
clearInterval(timer);
d.resolve();
}
}, 100);
// we are not canceling the monitor, as the test will catch
// and resource leaks for a timer if not cleared.
await d;
assertEquals(h.cancelAfter, 2000);
assertEquals(h.timer, 0);
assertEquals(h.autoCancelTimer, 0);
assert(h.count >= 6, `${h.count} >= 6`);
});
Deno.test("idleheartbeat - change", () => {
const h = new IdleHeartbeatMonitor(2000, (_v: number): boolean => {
return true;
}, { maxOut: 2, cancelAfter: 2000 });
assertEquals(h.cancelAfter, 2000);
assertEquals(h.maxOut, 2);
assertEquals(h.interval, 2000);
assertExists(h.timer);
const old = h.timer;
h._change(3000, 3000, 4);
assertEquals(h.cancelAfter, 3000);
assertEquals(h.maxOut, 4);
assertEquals(h.interval, 3000);
assertNotEquals(old, h.timer);
h.cancel();
});
Deno.test("idleheartbeat - restart", () => {
const h = new IdleHeartbeatMonitor(2000, (_v: number): boolean => {
return true;
}, { maxOut: 2, cancelAfter: 2000 });
assertExists(h.timer);
const old = h.timer;
h.restart();
assertExists(h.timer);
assertNotEquals(old, h.timer);
h.cancel();
});
--- core/tests/iterators_test.ts ---
/*
* 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 { connect } from "./connect.ts";
import { assertEquals, assertRejects } from "@std/assert";
import { Lock, NatsServer } from "test_helpers";
import {
createInbox,
delay,
nuid,
QueuedIteratorImpl,
syncIterator,
} from "../src/internal_mod.ts";
import type { NatsConnectionImpl } from "../src/internal_mod.ts";
import { cleanup, setup } from "test_helpers";
import { errors } from "../src/errors.ts";
Deno.test("iterators - unsubscribe breaks and closes", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj);
const done = (async () => {
for await (const _m of sub) {
if (sub.getReceived() > 1) {
sub.unsubscribe();
}
}
})();
nc.publish(subj);
nc.publish(subj);
await done;
assertEquals(sub.getReceived(), 2);
await cleanup(ns, nc);
});
Deno.test("iterators - autounsub breaks and closes", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 2 });
const lock = Lock(2);
const done = (async () => {
for await (const _m of sub) {
lock.unlock();
}
})();
nc.publish(subj);
nc.publish(subj);
await done;
await lock;
assertEquals(sub.getReceived(), 2);
await cleanup(ns, nc);
});
Deno.test("iterators - permission error breaks and closes", async () => {
const conf = {
authorization: {
users: [{
user: "derek",
password: "foobar",
permission: {
subscribe: "bar",
publish: "foo",
},
}],
},
};
const ns = await NatsServer.start(conf);
const nc = await connect(
{ port: ns.port, user: "derek", pass: "foobar" },
);
const sub = nc.subscribe("foo");
const lock = Lock();
await (async () => {
for await (const _m of sub) {
// ignored
}
})().catch(() => {
lock.unlock();
});
await lock;
await cleanup(ns, nc);
});
Deno.test("iterators - unsubscribing closes", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj);
const lock = Lock();
const done = (async () => {
for await (const _m of sub) {
lock.unlock();
}
})();
nc.publish(subj);
await lock;
sub.unsubscribe();
await done;
await cleanup(ns, nc);
});
Deno.test("iterators - connection close closes", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj);
const lock = Lock();
const done = (async () => {
for await (const _m of sub) {
lock.unlock();
}
})();
nc.publish(subj);
await nc.flush();
await nc.close();
await lock;
await done;
await ns.stop();
});
Deno.test("iterators - cb subs fail iterator", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { callback: () => {} });
await assertRejects(
async () => {
for await (const _ of sub) {
// nothing
}
},
errors.InvalidOperationError,
"iterator cannot be used when a callback is registered",
);
nc.publish(subj);
await nc.flush();
await cleanup(ns, nc);
});
Deno.test("iterators - cb message counts", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const lock = Lock(3);
const sub = nc.subscribe(subj, {
callback: () => {
lock.unlock();
},
});
nc.publish(subj);
nc.publish(subj);
nc.publish(subj);
await lock;
assertEquals(sub.getReceived(), 3);
assertEquals(sub.getProcessed(), 3);
assertEquals(sub.getPending(), 0);
await cleanup(ns, nc);
});
Deno.test("iterators - push on done is noop", async () => {
const qi = new QueuedIteratorImpl<string>();
const buf: string[] = [];
const done = (async () => {
for await (const s of qi) {
buf.push(s);
}
})();
qi.push("a");
qi.push("b");
qi.push("c");
qi.stop();
await done;
assertEquals(buf.length, 3);
assertEquals("a,b,c", buf.join(","));
qi.push("d");
assertEquals(buf.length, 3);
assertEquals("a,b,c", buf.join(","));
});
Deno.test("iterators - break cleans up", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
const sub = nc.subscribe(subj);
const done = (async () => {
for await (const _m of sub) {
break;
}
})();
nc.publish(subj);
await done;
assertEquals(sub.isClosed(), true);
assertEquals(nci.protocol.subscriptions.subs.size, 0);
await cleanup(ns, nc);
});
Deno.test("iterators - sync iterator", async () => {
const { ns, nc } = await setup();
const subj = nuid.next();
const sub = nc.subscribe(subj);
const sync = syncIterator(sub);
nc.publish(subj, "a");
let m = await sync.next();
assertEquals(m?.string(), "a");
nc.publish(subj, "b");
m = await sync.next();
assertEquals(m?.string(), "b");
const p = sync.next();
// blocks until next message
const v = await Promise.race([
delay(250).then(() => {
return "timer";
}),
p,
]);
assertEquals(v, "timer");
await assertRejects(
async () => {
for await (const _m of sub) {
// should fail
}
},
Error,
"already yielding",
);
const sub2 = nc.subscribe("foo", {
callback: () => {
},
});
await assertRejects(
async () => {
for await (const _m of sub2) {
// should fail
}
},
Error,
"iterator cannot be used when a callback is registered",
);
await cleanup(ns, nc);
});
--- core/tests/json_test.ts ---
/*
* Copyright 2018-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 { assertEquals } from "@std/assert";
import { createInbox } from "../src/internal_mod.ts";
import type { Msg } from "../src/internal_mod.ts";
import { Lock } from "test_helpers";
import { cleanup, setup } from "test_helpers";
function macro(input: unknown) {
return async () => {
const { ns, nc } = await setup();
const lock = Lock();
const subj = createInbox();
nc.subscribe(subj, {
callback: (err: Error | null, msg: Msg) => {
assertEquals(null, err);
// in JSON undefined is translated to null
if (input === undefined) {
input = null;
}
assertEquals(msg.json(), input);
lock.unlock();
},
max: 1,
});
nc.publish(subj, JSON.stringify(input));
await nc.flush();
await lock;
await cleanup(ns, nc);
};
}
Deno.test("json - string", macro("helloworld"));
Deno.test("json - empty", macro(""));
Deno.test("json - null", macro(null));
Deno.test("json - number", macro(10));
Deno.test("json - false", macro(false));
Deno.test("json - true", macro(true));
Deno.test("json - empty array", macro([]));
Deno.test("json - any array", macro([1, "a", false, 3.1416]));
Deno.test("json - empty object", macro({}));
Deno.test("json - object", macro({ a: 1, b: false, c: "name", d: 3.1416 }));
--- core/tests/launcher_test.ts ---
/*
* Copyright 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 { NatsServer } from "../../test_helpers/launcher.ts";
Deno.test("cmdlauncher - test", async () => {
const ns = await NatsServer.start({ debug: true, trace: true }, true);
console.log(ns.config);
await ns.stop();
});
--- core/tests/mrequest_test.ts ---
/*
* Copyright 2022-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 { cleanup, setup } from "test_helpers";
import type {
Msg,
NatsConnectionImpl,
QueuedIteratorImpl,
} from "../src/internal_mod.ts";
import { createInbox, deferred, delay, Empty } from "../src/internal_mod.ts";
import { assert, assertEquals, assertRejects, fail } from "@std/assert";
import { errors } from "../src/errors.ts";
async function requestManyCount(noMux = false): Promise<void> {
const { ns, nc } = await setup({});
const nci = nc as NatsConnectionImpl;
let payload = "";
const subj = createInbox();
nc.subscribe(subj, {
callback: (_err, msg) => {
if (payload === "") {
payload = msg.string();
}
for (let i = 0; i < 5; i++) {
msg.respond();
}
},
});
const iter = await nci.requestMany(subj, "hello", {
strategy: "count",
maxWait: 2000,
maxMessages: 5,
noMux,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
}
assertEquals(nci.protocol.subscriptions.size(), noMux ? 1 : 2);
assertEquals(payload, "hello");
assertEquals(iter.getProcessed(), 5);
await cleanup(ns, nc);
}
Deno.test("mreq - request many count", async () => {
await requestManyCount();
});
Deno.test("mreq - request many count noMux", async () => {
await requestManyCount(true);
});
async function requestManyJitter(noMux = false): Promise<void> {
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();
}
},
});
const start = Date.now();
const iter = await nci.requestMany(subj, Empty, {
strategy: "stall",
maxWait: 5000,
noMux,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
}
const time = Date.now() - start;
assert(1000 > time);
assertEquals(iter.getProcessed(), 10);
await cleanup(ns, nc);
}
Deno.test("mreq - request many jitter", async () => {
await requestManyJitter();
});
Deno.test("mreq - request many jitter noMux", async () => {
await requestManyJitter(true);
});
async function requestManySentinel(
noMux = false,
partial = false,
): Promise<void> {
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");
}
if (!partial) {
msg.respond();
}
},
});
const start = Date.now();
const iter = await nci.requestMany(subj, Empty, {
strategy: "sentinel",
maxWait: 2000,
noMux,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
}
const time = Date.now() - start;
// partial will timeout
assert(partial ? time > 500 : 500 > time);
// partial will not have the empty message
assertEquals(iter.getProcessed(), partial ? 10 : 11);
await cleanup(ns, nc);
}
Deno.test("mreq - nomux request many sentinel", async () => {
await requestManySentinel();
});
Deno.test("mreq - nomux request many sentinel noMux", async () => {
await requestManySentinel(true);
});
Deno.test("mreq - nomux request many sentinel partial", async () => {
await requestManySentinel(false, true);
});
Deno.test("mreq - nomux request many sentinel partial noMux", async () => {
await requestManySentinel(true, true);
});
async function requestManyTimerNoResponse(noMux = false): Promise<void> {
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,
noMux,
});
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("mreq - request many wait for timer - no response", async () => {
await requestManyTimerNoResponse();
});
Deno.test("mreq - request many wait for timer noMux - no response", async () => {
await requestManyTimerNoResponse(true);
});
async function requestTimerLateResponse(noMux = false): Promise<void> {
const { ns, nc } = await setup({});
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
nc.subscribe(subj, {
callback: (_err, msg) => {
delay(1750).then(() => {
msg.respond();
});
},
});
let count = 0;
const start = Date.now();
const iter = await nci.requestMany(subj, Empty, {
strategy: "timer",
maxWait: 2000,
noMux,
});
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("mreq - request many waits for timer late response", async () => {
await requestTimerLateResponse();
});
Deno.test("mreq - request many waits for timer late response noMux", async () => {
await requestTimerLateResponse(true);
});
async function requestManyStopsOnError(noMux = false): Promise<void> {
const { ns, nc } = await setup({});
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
const iter = await nci.requestMany(subj, Empty, {
strategy: "timer",
maxWait: 2000,
noMux,
});
await assertRejects(
async () => {
for await (const _mer of iter) {
// do nothing
}
},
errors.NoRespondersError,
subj,
);
await cleanup(ns, nc);
}
Deno.test("mreq - request many stops on error", async () => {
await requestManyStopsOnError();
});
Deno.test("mreq - request many stops on error noMux", async () => {
await requestManyStopsOnError(true);
});
Deno.test("mreq - pub permission error", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permissions: { publish: { deny: "q" } },
}],
},
}, { user: "a", pass: "a" });
const d = deferred();
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" &&
s.error instanceof errors.PermissionViolationError &&
s.error.operation === "publish" && s.error.subject === "q"
) {
d.resolve();
}
}
})().then();
const iter = await nc.requestMany("q", Empty, {
strategy: "count",
maxMessages: 3,
maxWait: 2000,
});
await assertRejects(
async () => {
for await (const _m of iter) {
// nothing
}
},
Error,
"Permissions Violation for Publish",
);
await d;
await cleanup(ns, nc);
});
Deno.test("mreq - sub permission error", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permissions: { subscribe: { deny: "_INBOX.>" } },
}],
},
}, { user: "a", pass: "a" });
nc.subscribe("q", {
callback: (_err, msg) => {
msg?.respond();
},
});
const d = deferred();
(async () => {
for await (const s of nc.status()) {
if (
s.type === "error" &&
s.error instanceof errors.PermissionViolationError &&
s.error.operation === "subscription" &&
s.error.subject.startsWith("_INBOX.")
) {
d.resolve();
}
}
})().then();
await assertRejects(
async () => {
const iter = await nc.requestMany("q", Empty, {
strategy: "count",
maxMessages: 3,
maxWait: 2000,
noMux: true,
});
for await (const _m of iter) {
// nothing;
}
},
errors.PermissionViolationError,
"Permissions Violation for Subscription",
);
await d;
await cleanup(ns, nc);
});
Deno.test("mreq - lost sub permission", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
}],
},
}, { user: "a", pass: "a" });
let reloaded = false;
nc.subscribe("q", {
callback: (_err, msg) => {
msg?.respond();
if (!reloaded) {
reloaded = true;
ns.reload({
authorization: {
users: [{
user: "a",
password: "a",
permissions: { subscribe: { deny: "_INBOX.>" } },
}],
},
});
}
},
});
const d = deferred();
(async () => {
for await (const s of nc.status()) {
if (s.type === "error") {
if (
s.error instanceof errors.PermissionViolationError &&
s.error.operation === "subscription" &&
s.error.subject.startsWith("_INBOX.")
) {
d.resolve();
}
}
}
})().then();
const iter = await nc.requestMany("q", Empty, {
strategy: "count",
maxMessages: 100,
stall: 2000,
maxWait: 2000,
noMux: true,
}) as QueuedIteratorImpl<Msg>;
await assertRejects(
() => {
return (async () => {
for await (const _m of iter) {
// nothing;
}
})();
},
errors.PermissionViolationError,
"Permissions Violation for Subscription",
);
await iter.iterClosed;
await cleanup(ns, nc);
});
Deno.test("mreq - 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);
// there's no error here - the empty response is the timeout
const iter = await nc.requestMany("q", Empty, {
maxWait: 1000,
maxMessages: 10,
noMux: true,
});
for await (const _ of iter) {
// nothing
}
assertEquals(nci.protocol.subscriptions.size(), 1);
await cleanup(ns, nc);
});
Deno.test("mreq - no responder doesn't leak subs", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
assertEquals(nci.protocol.subscriptions.size(), 0);
await assertRejects(
async () => {
const iter = await nc.requestMany("q", Empty, {
noMux: true,
maxWait: 1000,
maxMessages: 10,
});
for await (const _ of iter) {
// nothing
}
},
errors.NoRespondersError,
"no responders: 'q'",
);
// the mux subscription
assertEquals(nci.protocol.subscriptions.size(), 0);
await cleanup(ns, nc);
});
Deno.test("mreq - 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: ">",
allow_responses: true,
},
}],
},
}, { user: "s", pass: "s" });
const nci = nc as NatsConnectionImpl;
assertEquals(nci.protocol.subscriptions.size(), 0);
await assertRejects(
async () => {
const iter = await nc.requestMany("qq", Empty, {
noMux: true,
maxWait: 1000,
});
for await (const _ of iter) {
// nothing
}
},
Error,
"Permissions Violation for Publish",
);
await cleanup(ns, nc);
});
Deno.test("basics - request many tracing", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe("foo", {
callback: (_, m) => {
m.respond();
m.respond();
},
});
const traces = nc.subscribe("traces", {
callback: () => {},
max: 2,
});
nc.flush();
let iter = await nc.requestMany("foo", Empty, {
strategy: "stall",
maxWait: 2_000,
traceDestination: "traces",
});
let count = 0;
for await (const _ of iter) {
count++;
}
assertEquals(count, 2);
iter = await nc.requestMany("foo", Empty, {
strategy: "stall",
maxWait: 2_000,
traceDestination: "traces",
traceOnly: true,
});
count = 0;
for await (const _ of iter) {
count++;
}
assertEquals(count, 0);
await traces.closed;
assertEquals(sub.getReceived(), 1);
assertEquals(traces.getReceived(), 2);
await cleanup(ns, nc);
});
--- core/tests/parseip_test.ts ---
/*
* 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 { ipV4, isIP, parseIP } from "../src/internal_mod.ts";
import { assert, assertEquals, assertFalse } from "@std/assert";
Deno.test("ipparser", () => {
const tests = [
{ t: "127.0.1.2", e: ipV4(127, 0, 1, 2) },
{ t: "127.0.0.1", e: ipV4(127, 0, 0, 1) },
{ t: "127.001.002.003", e: ipV4(127, 1, 2, 3) },
{ t: "::ffff:127.1.2.3", e: ipV4(127, 1, 2, 3) },
{ t: "::ffff:127.001.002.003", e: ipV4(127, 1, 2, 3) },
{ t: "::ffff:7f01:0203", e: ipV4(127, 1, 2, 3) },
{ t: "0:0:0:0:0000:ffff:127.1.2.3", e: ipV4(127, 1, 2, 3) },
{ t: "0:0:0:0:000000:ffff:127.1.2.3", e: ipV4(127, 1, 2, 3) },
{ t: "0:0:0:0::ffff:127.1.2.3", e: ipV4(127, 1, 2, 3) },
{
t: "2001:4860:0:2001::68",
e: new Uint8Array(
[
0x20,
0x01,
0x48,
0x60,
0,
0,
0x20,
0x01,
0,
0,
0,
0,
0,
0,
0x00,
0x68,
],
),
},
{
t: "2001:4860:0000:2001:0000:0000:0000:0068",
e: new Uint8Array(
[
0x20,
0x01,
0x48,
0x60,
0,
0,
0x20,
0x01,
0,
0,
0,
0,
0,
0,
0x00,
0x68,
],
),
},
{ t: "-0.0.0.0", e: undefined },
{ t: "0.-1.0.0", e: undefined },
{ t: "0.0.-2.0", e: undefined },
{ t: "0.0.0.-3", e: undefined },
{ t: "127.0.0.256", e: undefined },
{ t: "abc", e: undefined },
{ t: "123:", e: undefined },
{ t: "fe80::1%lo0", e: undefined },
{ t: "fe80::1%911", e: undefined },
{ t: "", e: undefined },
{ t: "a1:a2:a3:a4::b1:b2:b3:b4", e: undefined }, // Issue 6628
];
tests.forEach((tc) => {
assertEquals(parseIP(tc.t), tc.e, tc.t);
});
});
Deno.test("ipparser - isIP", () => {
assert(isIP("127.0.0.1"));
assert(isIP("192.168.1.1"));
assert(isIP("2001:4860:0:2001::68"));
assert(isIP("::1"));
assert(isIP("::"));
assertFalse(isIP(""));
assertFalse(isIP("invalid"));
assertFalse(isIP("256.0.0.1"));
});
Deno.test("ipparser - IPv6 edge cases", () => {
// Empty IPv6 (all zeros)
const emptyV6 = parseIP("::");
assert(emptyV6 !== undefined);
assertEquals(emptyV6, new Uint8Array(16));
// Loopback
const loopback = parseIP("::1");
assert(loopback !== undefined);
const expected = new Uint8Array(16);
expected[15] = 1;
assertEquals(loopback, expected);
// IPv6 with hex number > 0xFFFF should fail
assertEquals(parseIP("10000::1"), undefined);
// IPv6 with invalid hex digits should fail
assertEquals(parseIP("gggg::1"), undefined);
// IPv6 with trailing data should fail
assertEquals(parseIP("::1:extra"), undefined);
// IPv6 with double ellipsis should fail
assertEquals(parseIP("::1::2"), undefined);
// IPv6 ending with single colon should fail
assertEquals(parseIP("::1:"), undefined);
// IPv6 with too many groups should fail
assertEquals(parseIP("1:2:3:4:5:6:7:8:9"), undefined);
// IPv6 mixed case hex (should work)
assert(parseIP("fe80::1A2b:3C4d") !== undefined);
});
Deno.test("ipparser - IPv4 edge cases", () => {
// Missing octets
assertEquals(parseIP("127.0.0"), undefined);
assertEquals(parseIP("127.0"), undefined);
assertEquals(parseIP("127"), undefined);
// Double dots
assertEquals(parseIP("1.2..4"), undefined);
// Octet starting with invalid character
assertEquals(parseIP("127.a.0.1"), undefined);
// Valid edge cases
assertEquals(parseIP("0.0.0.0"), ipV4(0, 0, 0, 0));
assertEquals(parseIP("255.255.255.255"), ipV4(255, 255, 255, 255));
// Boundary: exactly 255 is valid
assertEquals(parseIP("10.0.0.255"), ipV4(10, 0, 0, 255));
});
--- core/tests/parser_test.ts ---
/*
* 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 {
DenoBuffer,
describe,
Empty,
headers,
Kind,
MsgHdrsImpl,
MsgImpl,
Parser,
State,
} from "../src/internal_mod.ts";
import type {
Dispatcher,
Msg,
ParserEvent,
Publisher,
} from "../src/internal_mod.ts";
import { assert, assertEquals, assertThrows } from "@std/assert";
const te = new TextEncoder();
const td = new TextDecoder();
class NoopDispatcher implements Dispatcher<ParserEvent> {
push(_a: ParserEvent): void {}
}
export class TestDispatcher implements Dispatcher<ParserEvent> {
count = 0;
pings = 0;
pongs = 0;
ok = 0;
errs: ParserEvent[] = [];
infos: ParserEvent[] = [];
msgs: ParserEvent[] = [];
push(a: ParserEvent): void {
this.count++;
switch (a.kind) {
case Kind.OK:
this.ok++;
break;
case Kind.ERR:
this.errs.push(a);
break;
case Kind.MSG:
this.msgs.push(a);
break;
case Kind.INFO:
this.infos.push(a);
break;
case Kind.PING:
this.pings++;
break;
case Kind.PONG:
this.pongs++;
break;
default:
throw new Error(`unknown parser event ${JSON.stringify(a)}`);
}
}
}
// These are almost verbatim ports of the NATS parser tests
function byByteTest(
data: Uint8Array,
): { states: State[]; dispatcher: TestDispatcher } {
const e = new TestDispatcher();
const p = new Parser(e);
const states: State[] = [];
assertEquals(p.state, State.OP_START);
for (let i = 0; i < data.length; i++) {
states.push(p.state);
p.parse(Uint8Array.of(data[i]));
}
states.push(p.state);
return { states, dispatcher: e };
}
Deno.test("parser - ping", () => {
const states = [
State.OP_START,
State.OP_P,
State.OP_PI,
State.OP_PIN,
State.OP_PING,
State.OP_PING,
State.OP_START,
];
const results = byByteTest(te.encode("PING\r\n"));
assertEquals(results.states, states);
assertEquals(results.dispatcher.pings, 1);
assertEquals(results.dispatcher.count, 1);
const events = new TestDispatcher();
const p = new Parser(events);
p.parse(te.encode("PING\r\n"));
assertEquals(p.state, State.OP_START);
p.parse(te.encode("PING \r"));
assertEquals(p.state, State.OP_PING);
p.parse(te.encode("PING \r \n"));
assertEquals(p.state, State.OP_START);
assertEquals(events.pings, 2);
assertEquals(events.count, 2);
});
Deno.test("parser - err", () => {
const states = [
State.OP_START,
State.OP_MINUS,
State.OP_MINUS_E,
State.OP_MINUS_ER,
State.OP_MINUS_ERR,
State.OP_MINUS_ERR_SPC,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.MINUS_ERR_ARG,
State.OP_START,
];
const results = byByteTest(te.encode(`-ERR '234 6789'\r\n`));
assertEquals(results.states, states);
assertEquals(results.dispatcher.errs.length, 1);
assertEquals(results.dispatcher.count, 1);
assertEquals(td.decode(results.dispatcher.errs[0].data), `'234 6789'`);
const events = new TestDispatcher();
const p = new Parser(events);
p.parse(te.encode("-ERR 'Any error'\r\n"));
assertEquals(p.state, State.OP_START);
assertEquals(events.errs.length, 1);
assertEquals(events.count, 1);
assertEquals(td.decode(events.errs[0].data), `'Any error'`);
});
Deno.test("parser - ok", () => {
let states = [
State.OP_START,
State.OP_PLUS,
State.OP_PLUS_O,
State.OP_PLUS_OK,
State.OP_PLUS_OK,
State.OP_START,
];
let result = byByteTest(te.encode("+OK\r\n"));
assertEquals(result.states, states);
states = [
State.OP_START,
State.OP_PLUS,
State.OP_PLUS_O,
State.OP_PLUS_OK,
State.OP_PLUS_OK,
State.OP_PLUS_OK,
State.OP_PLUS_OK,
State.OP_START,
];
result = byByteTest(te.encode("+OKay\r\n"));
assertEquals(result.states, states);
});
Deno.test("parser - info", () => {
const states = [
State.OP_START,
State.OP_I,
State.OP_IN,
State.OP_INF,
State.OP_INFO,
State.OP_INFO_SPC,
State.INFO_ARG,
State.INFO_ARG,
State.INFO_ARG,
State.OP_START,
];
const results = byByteTest(te.encode(`INFO {}\r\n`));
assertEquals(results.states, states);
assertEquals(results.dispatcher.infos.length, 1);
assertEquals(results.dispatcher.count, 1);
});
Deno.test("parser - errors", () => {
const bad = [
" PING",
"POO",
"Px",
"PIx",
"PINx",
"PONx",
"ZOO",
"Mx\r\n",
"MSx\r\n",
"MSGx\r\n",
"MSG foo\r\n",
"MSG \r\n",
"MSG foo 1\r\n",
"MSG foo bar 1\r\n",
"MSG foo bar 1 baz\r\n",
"MSG foo 1 bar baz\r\n",
"+x\r\n",
"+0x\r\n",
"-x\r\n",
"-Ex\r\n",
"-ERx\r\n",
"-ERRx\r\n",
];
let count = 0;
bad.forEach((s) => {
assertThrows(() => {
count++;
const p = new Parser(new NoopDispatcher());
p.parse(te.encode(s));
});
});
assertEquals(count, bad.length);
});
Deno.test("parser - split msg", () => {
assertThrows(() => {
const p = new Parser(new NoopDispatcher());
p.parse(te.encode("MSG a\r\n"));
});
assertThrows(() => {
const p = new Parser(new NoopDispatcher());
p.parse(te.encode("MSG a b c\r\n"));
});
const d = new TestDispatcher();
const p = new Parser(d);
p.parse(te.encode("MSG a"));
assert(p.argBuf);
p.parse(te.encode(" 1 3\r\nf"));
assertEquals(p.ma.size, 3, "size");
assertEquals(p.ma.sid, 1, "sid");
assertEquals(p.ma.subject, te.encode("a"), "subject");
assert(p.msgBuf, "should message buffer");
p.parse(te.encode("oo\r\n"));
assertEquals(d.count, 1);
assertEquals(d.msgs.length, 1);
assertEquals(td.decode(d.msgs[0].msg?.subject), "a");
assertEquals(td.decode(d.msgs[0].data), "foo");
assertEquals(p.msgBuf, undefined);
p.parse(te.encode("MSG a 1 3\r\nba"));
assertEquals(p.ma.size, 3, "size");
assertEquals(p.ma.sid, 1, "sid");
assertEquals(p.ma.subject, te.encode("a"), "subject");
assert(p.msgBuf, "should message buffer");
p.parse(te.encode("r\r\n"));
assertEquals(d.msgs.length, 2);
assertEquals(td.decode(d.msgs[1].data), "bar");
assertEquals(p.msgBuf, undefined);
p.parse(te.encode("MSG a 1 6\r\nfo"));
assertEquals(p.ma.size, 6, "size");
assertEquals(p.ma.sid, 1, "sid");
assertEquals(p.ma.subject, te.encode("a"), "subject");
assert(p.msgBuf, "should message buffer");
p.parse(te.encode("ob"));
p.parse(te.encode("ar\r\n"));
assertEquals(d.msgs.length, 3);
assertEquals(td.decode(d.msgs[2].data), "foobar");
assertEquals(p.msgBuf, undefined);
const payload = new Uint8Array(100);
const buf = te.encode("MSG a 1 b 103\r\nfoo");
p.parse(buf);
assertEquals(p.ma.size, 103);
assertEquals(p.ma.sid, 1);
assertEquals(td.decode(p.ma.subject), "a");
assertEquals(td.decode(p.ma.reply), "b");
assert(p.argBuf);
const a = "a".charCodeAt(0); //97
for (let i = 0; i < payload.length; i++) {
payload[i] = a + (i % 26);
}
p.parse(payload);
assertEquals(p.state, State.MSG_PAYLOAD);
assertEquals(p.ma.size, 103);
assertEquals(p.msgBuf.length, 103);
p.parse(te.encode("\r\n"));
assertEquals(p.state, State.OP_START);
assertEquals(p.msgBuf, undefined);
assertEquals(d.msgs.length, 4);
const db = d.msgs[3].data!;
assertEquals(td.decode(db.subarray(0, 3)), "foo");
const gen = db.subarray(3);
for (let k = 0; k < 100; k++) {
assertEquals(gen[k], a + (k % 26));
}
});
Deno.test("parser - info arg", () => {
const arg = {
"server_id": "test",
host: "localhost",
port: 4222,
version: "1.2.3",
"auth_required": true,
"tls_required": true,
"max_payload": 2 * 1024 * 1024,
"connect_urls": [
"localhost:5222",
"localhost:6222",
],
};
const d = new TestDispatcher();
const p = new Parser(d);
const info = te.encode(`INFO ${JSON.stringify(arg)}\r\n`);
p.parse(info.subarray(0, 9));
assertEquals(p.state, State.INFO_ARG);
assert(p.argBuf);
p.parse(info.subarray(9, 11));
assertEquals(p.state, State.INFO_ARG);
assert(p.argBuf);
p.parse(info.subarray(11));
assertEquals(p.state, State.OP_START);
assertEquals(p.argBuf, undefined);
assertEquals(d.infos.length, 1);
assertEquals(d.count, 1);
const arg2 = JSON.parse(td.decode(d.infos[0]?.data));
assertEquals(arg2, arg);
const good = [
"INFO {}\r\n",
"INFO {}\r\n",
"INFO {} \r\n",
'INFO { "server_id": "test" } \r\n',
'INFO {"connect_urls":[]}\r\n',
];
good.forEach((info) => {
p.parse(te.encode(info));
});
assertEquals(d.infos.length, good.length + 1);
const bad = [
"IxNFO {}\r\n",
"INxFO {}\r\n",
"INFxO {}\r\n",
"INFOx {}\r\n",
"INFO{}\r\n",
"INFO {}",
];
let count = 0;
bad.forEach((info) => {
assertThrows(() => {
count++;
p.parse(te.encode(info));
});
});
assertEquals(count, bad.length);
});
Deno.test("parser - header", () => {
const d = new TestDispatcher();
const p = new Parser(d);
const h = headers() as MsgHdrsImpl;
h.set("x", "y");
const hdr = h.encode();
p.parse(te.encode(`HMSG a 1 ${hdr.length} ${hdr.length + 3}\r\n`));
p.parse(hdr);
assertEquals(p.ma.size, hdr.length + 3, "size");
assertEquals(p.ma.sid, 1, "sid");
assertEquals(p.ma.subject, te.encode("a"), "subject");
assert(p.msgBuf, "should message buffer");
p.parse(te.encode("bar\r\n"));
assertEquals(d.msgs.length, 1);
const payload = d.msgs[0].data;
const h2 = MsgHdrsImpl.decode(payload!.subarray(0, d.msgs[0].msg?.hdr));
assert(h2.equals(h));
assertEquals(td.decode(payload!.subarray(d.msgs[0].msg?.hdr)), "bar");
});
Deno.test("parser - subject", () => {
const d = new TestDispatcher();
const p = new Parser(d);
p.parse(
te.encode(
`MSG foo 1 _INBOX.4E66Z7UREYUY9VKDNFBT1A.4E66Z7UREYUY9VKDNFBT72.4E66Z7UREYUY9VKDNFBSVI 102400\r\n`,
),
);
for (let i = 0; i < 100; i++) {
p.parse(new Uint8Array(1024));
}
assertEquals(p.ma.size, 102400, `size ${p.ma.size}`);
assertEquals(p.ma.sid, 1, "sid");
assertEquals(p.ma.subject, te.encode("foo"), "subject");
assertEquals(
p.ma.reply,
te.encode(
"_INBOX.4E66Z7UREYUY9VKDNFBT1A.4E66Z7UREYUY9VKDNFBT72.4E66Z7UREYUY9VKDNFBSVI",
),
"reply",
);
});
Deno.test("parser - msg buffers don't clobber", () => {
parserClobberTest(false);
});
Deno.test("parser - hmsg buffers don't clobber", () => {
parserClobberTest(true);
});
function parserClobberTest(hdrs = false): void {
const d = new TestDispatcher();
const p = new Parser(d);
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 code = (n: number): number => {
return n % 26 + a;
};
const subj = new Uint8Array(26);
const reply = new Uint8Array(26);
const payload = new Uint8Array(1024 * 1024);
const kv = new Uint8Array(12);
const check = (n: number, m: Msg) => {
const s = code(n + 1);
assertEquals(m.subject.length, subj.length);
te.encode(m.subject).forEach((c) => {
assertEquals(c, s, "subject char");
});
const r = code(n + 2);
assertEquals(m.reply?.length, reply.length);
te.encode(m.reply).forEach((c) => {
assertEquals(r, c, "reply char");
});
assertEquals(m.data.length, payload.length);
const pc = code(n);
m.data.forEach((c) => {
assertEquals(c, pc);
}, "payload");
if (hdrs) {
assert(m.headers);
fill(n + 3, kv);
const hv = td.decode(kv);
assertEquals(m.headers.get(hv), hv);
}
};
const buf = new DenoBuffer();
const N = 100;
for (let i = 0; i < N; i++) {
buf.reset();
fill(i, payload);
fill(i + 1, subj);
fill(i + 2, reply);
let hdrdata = Empty;
if (hdrs) {
fill(i + 3, kv);
const h = headers() as MsgHdrsImpl;
const kvs = td.decode(kv);
h.set(kvs, kvs);
hdrdata = h.encode();
}
const len = hdrdata.length + payload.length;
if (hdrs) {
buf.writeString("HMSG ");
} else {
buf.writeString("MSG ");
}
buf.write(subj);
buf.writeString(` 1`);
if (reply) {
buf.writeString(" ");
buf.write(reply);
}
if (hdrs) {
buf.writeString(` ${hdrdata.length}`);
}
buf.writeString(` ${len}\r\n`);
if (hdrs) {
buf.write(hdrdata);
}
buf.write(payload);
buf.writeString(`\r\n`);
p.parse(buf.bytes());
}
assertEquals(d.msgs.length, 100);
for (let i = 0; i < 100; i++) {
const e = d.msgs[i];
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
check(i, m);
}
}
Deno.test("parser - describe", () => {
assertEquals(describe({ kind: Kind.MSG }), "MSG: ");
assertEquals(describe({ kind: Kind.OK }), "OK: ");
assertEquals(describe({ kind: Kind.PING }), "PING: ");
assertEquals(describe({ kind: Kind.PONG }), "PONG: ");
assertEquals(
describe({ kind: Kind.ERR, data: te.encode("error message") }),
"ERR: error message",
);
assertEquals(
describe({ kind: Kind.INFO, data: te.encode('{"server_id":"test"}') }),
'INFO: {"server_id":"test"}',
);
});
--- core/tests/properties_test.ts ---
/*
* Copyright 2018-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 } from "./connect.ts";
import {
assert,
assertEquals,
assertExists,
assertFalse,
assertMatch,
} from "@std/assert";
import type {
Authenticator,
ConnectionOptions,
NatsConnectionImpl,
} from "../src/internal_mod.ts";
import {
Connect,
credsAuthenticator,
defaultOptions,
extend,
parseOptions,
Servers,
} from "../src/internal_mod.ts";
import { NatsServer } from "../../test_helpers/launcher.ts";
import { hasWsProtocol } from "../src/options.ts";
Deno.test("properties - VERSION is semver", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port }) as NatsConnectionImpl;
const { version } = nc.protocol.transport;
assertMatch(version, /[0-9]+\.[0-9]+\.[0-9]+/);
await nc.close();
await ns.stop();
});
Deno.test("properties - connect is a function", () => {
assert(typeof connect === "function");
});
Deno.test("properties - default connect properties", () => {
const opts = defaultOptions();
opts.servers = "127.0.0.1:4222";
const c = new Connect(
{ version: "1.0.0", lang: "lang" },
opts,
);
const cc = JSON.parse(JSON.stringify(c));
assertEquals(cc.lang, "lang");
assert(cc.version);
assertEquals(cc.verbose, false, "verbose");
assertEquals(cc.pedantic, false, "pedantic");
assertEquals(cc.protocol, 1, "protocol");
assertEquals(cc.user, undefined, "user");
assertEquals(cc.pass, undefined, "pass");
assertEquals(cc.auth_token, undefined, "auth_token");
assertEquals(cc.name, undefined, "name");
});
Deno.test("properties - configured token options", () => {
let opts = defaultOptions();
opts.servers = "127.0.0.1:4222";
opts.name = "test";
opts.token = "abc";
opts.pedantic = true;
opts.verbose = true;
opts = parseOptions(opts);
assertExists(opts.authenticator);
const fn = opts.authenticator as Authenticator;
const auth = fn();
opts = extend(opts, auth);
const c = new Connect({ version: "1.0.0", lang: "lang" }, opts);
const cc = JSON.parse(JSON.stringify(c));
assertEquals(cc.verbose, opts.verbose);
assertEquals(cc.pedantic, opts.pedantic);
assertEquals(cc.name, opts.name);
assertEquals(cc.user, undefined);
assertEquals(cc.pass, undefined);
assertEquals(cc.auth_token, opts.token);
});
Deno.test("properties - configured user/pass options", () => {
let opts = defaultOptions();
opts.servers = "127.0.0.1:4222";
opts.user = "test";
opts.pass = "secret";
opts = parseOptions(opts);
assertExists(opts.authenticator);
const fn = opts.authenticator as Authenticator;
const auth = fn();
opts = extend(opts, auth);
const c = new Connect({ version: "1.0.0", lang: "lang" }, opts);
const cc = JSON.parse(JSON.stringify(c));
assertEquals(cc.user, opts.user);
assertEquals(cc.pass, opts.pass);
assertEquals(cc.auth_token, undefined);
});
Deno.test("properties - tls doesn't leak options", () => {
const tlsOptions = {
keyFile: "keyFile",
certFile: "certFile",
caFile: "caFile",
key: "key",
cert: "cert",
ca: "ca",
};
const opts = { tls: tlsOptions, cert: "another" };
let po = parseOptions(opts);
assertExists(po.authenticator);
const fn = po.authenticator as Authenticator;
const auth = fn() || {};
po = extend(po, auth);
const c = new Connect({ version: "1.2.3", lang: "test" }, po);
const cc = JSON.parse(JSON.stringify(c));
assertEquals(cc.tls_required, true);
assertEquals(cc.cert, undefined);
assertEquals(cc.keyFile, undefined);
assertEquals(cc.certFile, undefined);
assertEquals(cc.caFile, undefined);
assertEquals(cc.tls, undefined);
});
Deno.test("properties - port is only honored if no servers provided", () => {
type test = { opts?: ConnectionOptions; expected: string };
const buf: test[] = [];
buf.push({ expected: "127.0.0.1:4222" });
buf.push({ opts: {}, expected: "127.0.0.1:4222" });
buf.push({ opts: { port: 9999 }, expected: "127.0.0.1:9999" });
buf.forEach((t) => {
const opts = parseOptions(t.opts);
const servers = new Servers(opts.servers as string[], {});
const cs = servers.getCurrentServer();
assertEquals(cs.listen, t.expected);
});
});
Deno.test("properties - multi", () => {
const creds = `-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN USER NKEY SEED-----
SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4
------END USER NKEY SEED------
`;
const authenticator = credsAuthenticator(new TextEncoder().encode(creds));
let opts = defaultOptions();
opts.servers = "127.0.0.1:4222";
opts.user = "test";
opts.pass = "secret";
opts.token = "mytoken";
opts.authenticator = authenticator;
opts = parseOptions(opts);
assertExists(opts.authenticator);
const c = new Connect({ version: "1.0.0", lang: "lang" }, opts, "hello");
const cc = JSON.parse(JSON.stringify(c));
assertEquals(cc.user, opts.user);
assertEquals(cc.pass, opts.pass);
assertEquals(cc.auth_token, opts.token);
assertExists(cc.jwt);
assertExists(cc.sig);
assert(cc.sig.length > 0);
assertExists(cc.nkey);
});
Deno.test("properties - hasWsProtocol", () => {
assertFalse(hasWsProtocol({ servers: "127.0.0.1:4222" }));
assert(hasWsProtocol({ servers: "WS://127.0.0.1:4222" }));
assert(hasWsProtocol({ servers: "ws://127.0.0.1:4222" }));
assert(hasWsProtocol({ servers: "WSS://127.0.0.1:4222" }));
assert(hasWsProtocol({ servers: "ws://127.0.0.1:4222" }));
assert(
hasWsProtocol({
servers: ["nats://127.0.0.1:4222", "ws://127.0.0.1:4222"],
}),
);
});
--- core/tests/protocol_test.ts ---
/*
* Copyright 2018-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 {
Empty,
extractProtocolMessage,
MuxSubscription,
protoLen,
RequestOne,
SubscriptionImpl,
Subscriptions,
} from "../src/internal_mod.ts";
import type { Msg, ProtocolHandler } from "../src/internal_mod.ts";
import { assertEquals, assertRejects, equal } from "@std/assert";
import { errors } from "../src/errors.ts";
Deno.test("protocol - mux subscription cancel", async () => {
const mux = new MuxSubscription();
mux.init();
const r = new RequestOne(mux, "");
r.token = "alberto";
mux.add(r);
assertEquals(mux.size(), 1);
assertEquals(mux.get("alberto"), r);
assertEquals(mux.getToken({ subject: "" } as Msg), null);
const check = assertRejects(
() => {
return Promise.race([r.deferred, r.timer]);
},
errors.RequestError,
"cancelled",
);
r.cancel();
await check;
assertEquals(mux.size(), 0);
});
Deno.test("protocol - bad dispatch is noop", () => {
const mux = new MuxSubscription();
mux.init();
mux.dispatcher()(null, { subject: "foo" } as Msg);
});
Deno.test("protocol - subs all", () => {
const subs = new Subscriptions();
const s = new SubscriptionImpl({
unsubscribe(_s: SubscriptionImpl, _max?: number) {},
} as ProtocolHandler, "hello");
subs.add(s);
assertEquals(subs.size(), 1);
assertEquals(s.sid, 1);
assertEquals(subs.sidCounter, 1);
equal(subs.get(0), s);
const a = subs.all();
assertEquals(a.length, 1);
subs.cancel(a[0]);
assertEquals(subs.size(), 0);
});
Deno.test("protocol - cancel unknown sub", () => {
const subs = new Subscriptions();
const s = new SubscriptionImpl({
unsubscribe(_s: SubscriptionImpl, _max?: number) {},
} as ProtocolHandler, "hello");
assertEquals(subs.size(), 0);
subs.add(s);
assertEquals(subs.size(), 1);
subs.cancel(s);
assertEquals(subs.size(), 0);
});
Deno.test("protocol - protolen -1 on empty", () => {
assertEquals(protoLen(Empty), 0);
assertEquals(extractProtocolMessage(Empty), "");
});
--- core/tests/queues_test.ts ---
/*
* Copyright 2018-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 { createInbox } from "../src/core.ts";
import type { Subscription } from "../src/core.ts";
import { assertEquals } from "@std/assert";
import { cleanup, setup } from "test_helpers";
Deno.test("queues - deliver to single queue", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const subs = [];
for (let i = 0; i < 5; i++) {
const s = nc.subscribe(subj, { queue: "a" });
subs.push(s);
}
nc.publish(subj);
await nc.flush();
const received = subs.map((s) => s.getReceived());
const sum = received.reduce((p, c) => p + c);
assertEquals(sum, 1);
await cleanup(ns, nc);
});
Deno.test("queues - deliver to multiple queues", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const fn = (queue: string) => {
const subs = [];
for (let i = 0; i < 5; i++) {
const s = nc.subscribe(subj, { queue: queue });
subs.push(s);
}
return subs;
};
const subsa = fn("a");
const subsb = fn("b");
nc.publish(subj);
await nc.flush();
const mc = (subs: Subscription[]): number => {
const received = subs.map((s) => s.getReceived());
return received.reduce((p, c) => p + c);
};
assertEquals(mc(subsa), 1);
assertEquals(mc(subsb), 1);
await cleanup(ns, nc);
});
Deno.test("queues - queues and subs independent", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const subs = [];
let queueCount = 0;
for (let i = 0; i < 5; i++) {
const s = nc.subscribe(subj, {
callback: () => {
queueCount++;
},
queue: "a",
});
subs.push(s);
}
let count = 0;
subs.push(nc.subscribe(subj, {
callback: () => {
count++;
},
}));
await Promise.all(subs);
nc.publish(subj);
await nc.flush();
assertEquals(queueCount, 1);
assertEquals(count, 1);
await cleanup(ns, nc);
});
--- core/tests/reconnect_test.ts ---
/*
* Copyright 2018-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, assertInstanceOf, fail } from "@std/assert";
import { connect } from "./connect.ts";
import { Lock, NatsServer } from "test_helpers";
import {
createInbox,
DataBuffer,
deadline,
deferred,
delay,
tokenAuthenticator,
} from "../src/internal_mod.ts";
import type { NatsConnectionImpl } from "../src/nats.ts";
import { cleanup, setup } from "test_helpers";
import { ConnectionError } from "../src/errors.ts";
Deno.test("reconnect - should receive when some servers are invalid", async () => {
const lock = Lock(1);
const servers = ["127.0.0.1:7", "demo.nats.io:4222"];
const nc = await connect(
{ servers: servers, noRandomize: true },
) as NatsConnectionImpl;
const subj = createInbox();
await nc.subscribe(subj, {
callback: () => {
lock.unlock();
},
});
nc.publish(subj);
await lock;
await nc.close();
const a = nc.protocol.servers.getServers();
assertEquals(a.length, 1);
assert(a[0].didConnect);
});
Deno.test("reconnect - events", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
waitOnFirstConnect: true,
reconnectTimeWait: 100,
maxReconnectAttempts: 10,
timeout: 1000,
});
let disconnects = 0;
let reconnecting = 0;
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "disconnect":
disconnects++;
break;
case "reconnecting":
reconnecting++;
break;
}
}
})().then();
await srv.stop();
const err = await nc.closed();
assertInstanceOf(err, ConnectionError, "connection closed");
assertEquals(disconnects, 1);
assertEquals(reconnecting, 10);
});
Deno.test("reconnect - reconnect not emitted if suppressed", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
reconnect: false,
});
let disconnects = 0;
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "disconnect":
disconnects++;
break;
case "reconnecting":
fail("shouldn't have emitted reconnecting");
break;
}
}
})().then();
await srv.stop();
await nc.closed();
});
Deno.test("reconnect - reconnecting after proper delay", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
reconnectTimeWait: 500,
maxReconnectAttempts: 1,
}) as NatsConnectionImpl;
const first = nc.protocol.servers.getCurrentServer().lastConnect;
const dt = deferred<number>();
(async () => {
for await (const e of nc.status()) {
switch (e.type as string) {
case "reconnecting": {
const last = nc.protocol.servers.getCurrentServer().lastConnect;
dt.resolve(last - first);
break;
}
}
}
})().then();
await srv.stop();
const elapsed = await dt;
assert(elapsed >= 500 && elapsed <= 800, `elapsed was ${elapsed}`);
await nc.closed();
});
Deno.test("reconnect - indefinite reconnects", async () => {
let srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
reconnectTimeWait: 100,
maxReconnectAttempts: -1,
});
let disconnects = 0;
let reconnects = 0;
let reconnect = false;
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "disconnect":
disconnects++;
break;
case "reconnect":
reconnect = true;
nc.close().then().catch();
break;
case "reconnecting":
reconnects++;
break;
}
}
})().then();
await srv.stop();
const lock = Lock(1);
setTimeout(async () => {
srv = await srv.restart();
lock.unlock();
}, 1000);
await nc.closed();
await srv.stop();
await lock;
await srv.stop();
assert(reconnects >= 5, `${reconnects} >= 5`);
assert(reconnect);
assertEquals(disconnects, 1);
});
Deno.test("reconnect - jitter", async () => {
const srv = await NatsServer.start();
let called = false;
const h = () => {
called = true;
return 15;
};
const dc = await connect({
port: srv.port,
reconnect: false,
}) as NatsConnectionImpl;
const hasDefaultFn = typeof dc.options.reconnectDelayHandler === "function";
const nc = await connect({
port: srv.port,
maxReconnectAttempts: 1,
reconnectDelayHandler: h,
});
await srv.stop();
await nc.closed();
await dc.closed();
assert(called);
assert(hasDefaultFn);
});
Deno.test("reconnect - internal disconnect forces reconnect", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
reconnect: true,
reconnectTimeWait: 200,
}) as NatsConnectionImpl;
let stale = false;
let disconnect = false;
const lock = Lock();
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "staleConnection":
stale = true;
break;
case "disconnect":
disconnect = true;
break;
case "reconnect":
lock.unlock();
break;
}
}
})().then();
nc.protocol.disconnect();
await lock;
assert(disconnect, "disconnect");
assert(stale, "stale");
await nc.close();
await srv.stop();
});
Deno.test("reconnect - wait on first connect", async () => {
let srv = await NatsServer.start({});
const port = srv.port;
await delay(500);
await srv.stop();
await delay(1000);
const pnc = connect({
port: port,
waitOnFirstConnect: true,
reconnectTimeWait: 100,
maxReconnectAttempts: 10,
});
await delay(3000);
srv = await srv.restart();
const nc = await pnc;
const subj = createInbox();
nc.subscribe(subj, {
callback: (_err, msg) => {
msg.respond();
},
});
await nc.request(subj);
// stop the server
await srv.stop();
// no re will quit the client
const err = await nc.closed();
assertInstanceOf(err, ConnectionError, "connection refused");
});
Deno.test("reconnect - close stops reconnects", async () => {
const { ns, nc } = await setup({}, {
maxReconnectAttempts: -1,
reconnectTimeWait: 500,
});
const reconnects = deferred();
(async () => {
let c = 0;
for await (const s of nc.status()) {
if (s.type === "reconnecting") {
c++;
if (c === 5) {
reconnects.resolve();
}
}
}
})().then();
setTimeout(() => {
ns.stop();
}, 1000);
await reconnects;
await deadline(nc.close(), 5000)
.catch((err) => {
// the promise will reject if deadline exceeds
fail(err);
});
// await some more, because the close could have a timer pending that
// didn't complete flapping the test on resource leak
await delay(1000);
});
Deno.test("reconnect - stale connections don't close", async () => {
const listener = Deno.listen({ port: 0, transport: "tcp" });
const { port } = listener.addr as Deno.NetAddr;
const connections: Deno.Conn[] = [];
const TE = new TextEncoder();
const INFO = TE.encode(
"INFO " + JSON.stringify({
server_id: "TEST",
version: "0.0.0",
host: "127.0.0.1",
port: port,
}) + "\r\n",
);
const PING = { re: /^PING\r\n/im, out: TE.encode("PONG\r\n") };
const CONNECT = { re: /^CONNECT\s+([^\r\n]+)\r\n/im, out: TE.encode("") };
const CMDS = [PING, CONNECT];
const startReading = (conn: Deno.Conn) => {
const buf = new Uint8Array(1024 * 8);
const inbound = new DataBuffer();
(async () => {
while (true) {
const count = await conn.read(buf);
if (count === null) {
break;
}
if (count) {
inbound.fill(DataBuffer.concat(buf.subarray(0, count)));
const lines = DataBuffer.toAscii(inbound.peek());
for (let i = 0; i < CMDS.length; i++) {
const m = CMDS[i].re.exec(lines);
if (m) {
const len = m[0].length;
if (len) {
inbound.drain(len);
await conn.write(CMDS[i].out);
}
if (i === 0) {
// sent the PONG we are done.
return;
}
}
}
}
}
})();
};
(async () => {
for await (const conn of listener) {
connections.push(conn);
try {
await conn.write(INFO);
startReading(conn);
} catch (_err) {
console.log(_err);
return;
}
}
})().then();
const nc = await connect({
port,
maxReconnectAttempts: -1,
pingInterval: 2000,
reconnectTimeWait: 500,
ignoreAuthErrorAbort: true,
// need a timeout, otherwise the default is 20s and we leak resources
timeout: 2000,
});
let stales = 0;
(async () => {
for await (const s of nc.status()) {
if (s.type === "staleConnection") {
stales++;
if (stales === 3) {
await nc.close();
}
}
}
})().then();
await nc.closed();
listener.close();
connections.forEach((c) => {
return c.close();
});
assert(stales >= 3, `stales ${stales}`);
// there could be a redial timer waiting here for 2s
await delay(2000);
});
Deno.test("reconnect - protocol errors don't close client", async () => {
const { ns, nc } = await setup({}, {
maxReconnectAttempts: -1,
reconnectTimeWait: 500,
});
const nci = nc as NatsConnectionImpl;
let reconnects = 0;
(async () => {
for await (const s of nc.status()) {
if (s.type === "reconnect") {
reconnects++;
if (reconnects < 3) {
setTimeout(() => {
nci.protocol.sendCommand(`X\r\n`);
});
}
if (reconnects === 3) {
await nc.close();
}
}
}
})().then();
nci.protocol.sendCommand(`X\r\n`);
const err = await nc.closed();
assertEquals(err, undefined);
await cleanup(ns, nc);
});
Deno.test("reconnect - authentication timeout reconnects", async () => {
const ns = await NatsServer.start({
authorization: {
timeout: 0.001,
token: "hello",
},
});
let counter = 3;
const authenticator = tokenAuthenticator(() => {
if (counter-- <= 0) {
return "hello";
}
const start = Date.now();
while (true) {
if (Date.now() > start + 150) {
break;
}
}
return "hello";
});
const nc = await connect({
port: ns.port,
token: "hello",
waitOnFirstConnect: true,
ignoreAuthErrorAbort: true,
authenticator,
});
assert(!nc.isClosed());
await cleanup(ns, nc);
});
--- core/tests/resub_test.ts ---
/*
* 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 { cleanup, setup } from "test_helpers";
import { createInbox } from "../src/internal_mod.ts";
import type {
Msg,
NatsConnection,
NatsConnectionImpl,
} from "../src/internal_mod.ts";
import { assert, assertEquals, assertExists, fail } from "@std/assert";
import type { NatsServer } from "../../test_helpers/launcher.ts";
Deno.test("resub - iter", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subja = createInbox();
const sub = nc.subscribe(subja, { max: 2 });
const buf: Msg[] = [];
(async () => {
for await (const m of sub) {
buf.push(m);
m.respond();
}
})().then();
await nc.request(subja);
const subjb = createInbox();
nci._resub(sub, subjb);
assertEquals(sub.getSubject(), subjb);
await nc.request(subjb);
assertEquals(sub.getProcessed(), 2);
assertEquals(buf.length, 2);
assertEquals(buf[0].subject, subja);
assertEquals(buf[1].subject, subjb);
await cleanup(ns, nc);
});
Deno.test("resub - callback", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subja = createInbox();
const buf: Msg[] = [];
const sub = nc.subscribe(subja, {
max: 2,
callback: (_err, msg) => {
buf.push(msg);
msg.respond();
},
});
await nc.request(subja);
const subjb = createInbox();
nci._resub(sub, subjb);
assertEquals(sub.getSubject(), subjb);
await nc.request(subjb);
assertEquals(sub.getProcessed(), 2);
assertEquals(buf.length, 2);
assertEquals(buf[0].subject, subja);
assertEquals(buf[1].subject, subjb);
await cleanup(ns, nc);
});
async function assertEqualSubs(
ns: NatsServer,
nc: NatsConnection,
): Promise<void> {
const nci = nc as NatsConnectionImpl;
const cid = nc.info?.client_id || -1;
if (cid === -1) {
fail("client_id not found");
}
const connz = await ns.connz(cid, "detail");
const conn = connz.connections.find((c) => {
return c.cid === cid;
});
assertExists(conn);
assertExists(conn.subscriptions_list_detail);
const subs = nci.protocol.subscriptions.all();
subs.forEach((sub) => {
const ssub = conn.subscriptions_list_detail?.find((d) => {
return d.sid === `${sub.sid}`;
});
assertExists(ssub);
assertEquals(ssub.subject, sub.subject);
});
}
Deno.test("resub - removes server interest", async () => {
const { ns, nc } = await setup();
nc.subscribe("a", {
callback() {
// nothing
},
});
await nc.flush();
const nci = nc as NatsConnectionImpl;
let sub = nci.protocol.subscriptions.all().find((s) => {
return s.subject === "a";
});
assertExists(sub);
// assert the server sees the same subscriptions
await assertEqualSubs(ns, nc);
// change it
nci._resub(sub, "b");
await nc.flush();
// make sure we don't find a
sub = nci.protocol.subscriptions.all().find((s) => {
return s.subject === "a";
});
assert(sub === undefined);
// make sure we find b
sub = nci.protocol.subscriptions.all().find((s) => {
return s.subject === "b";
});
assertExists(sub);
// assert server thinks the same thing
await assertEqualSubs(ns, nc);
await cleanup(ns, nc);
});
--- core/tests/semver_test.ts ---
/*
* 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 { compare, Feature, Features, parseSemVer } from "../src/semver.ts";
import { assert, assertEquals, assertFalse, assertThrows } from "@std/assert";
Deno.test("semver", () => {
const pt: { a: string; b: string; r: number }[] = [
{ a: "1.0.0", b: "1.0.0", r: 0 },
{ a: "1.0.1", b: "1.0.0", r: 1 },
{ a: "1.1.0", b: "1.0.1", r: 1 },
{ a: "1.1.1", b: "1.1.0", r: 1 },
{ a: "1.1.1", b: "1.1.1", r: 0 },
{ a: "1.1.100", b: "1.1.099", r: 1 },
{ a: "2.0.0", b: "1.500.500", r: 1 },
];
pt.forEach((t) => {
assertEquals(
compare(parseSemVer(t.a), parseSemVer(t.b)),
t.r,
`compare(${t.a}, ${t.b})`,
);
});
});
Deno.test("semver - feature basics", () => {
// sanity version check
const f = new Features(parseSemVer("4.0.0"));
assert(f.require("4.0.0"));
assertFalse(f.require("5.0.0"));
// change the version to 2.8.3
f.update(parseSemVer("2.8.3"));
let info = f.get(Feature.JS_PULL_MAX_BYTES);
assertEquals(info.ok, true);
assertEquals(info.min, "2.8.3");
assert(f.supports(Feature.JS_PULL_MAX_BYTES));
assertFalse(f.isDisabled(Feature.JS_PULL_MAX_BYTES));
// disable the feature
f.disable(Feature.JS_PULL_MAX_BYTES);
assert(f.isDisabled(Feature.JS_PULL_MAX_BYTES));
assertFalse(f.supports(Feature.JS_PULL_MAX_BYTES));
info = f.get(Feature.JS_PULL_MAX_BYTES);
assertEquals(info.ok, false);
assertEquals(info.min, "unknown");
// remove all disablements
f.resetDisabled();
assert(f.supports(Feature.JS_PULL_MAX_BYTES));
f.update(parseSemVer("2.8.2"));
assertFalse(f.supports(Feature.JS_PULL_MAX_BYTES));
});
Deno.test("semver - parse", () => {
assertThrows(() => parseSemVer(), Error, "'' is not a semver value");
assertThrows(() => parseSemVer(" "), Error, "' ' is not a semver value");
assertThrows(
() => parseSemVer("a.1.2"),
Error,
"'a.1.2' is not a semver value",
);
assertThrows(
() => parseSemVer("1.a.2"),
Error,
"'1.a.2' is not a semver value",
);
assertThrows(
() => parseSemVer("1.2.a"),
Error,
"'1.2.a' is not a semver value",
);
let sv = parseSemVer("v1.2.3");
assertEquals(sv.major, 1);
assertEquals(sv.minor, 2);
assertEquals(sv.micro, 3);
sv = parseSemVer("1.2.3-this-is-a-tag");
assertEquals(sv.major, 1);
assertEquals(sv.minor, 2);
assertEquals(sv.micro, 3);
});
--- core/tests/servers_test.ts ---
/*
* Copyright 2018-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 {
isIPV4OrHostname,
Servers,
setTransportFactory,
} from "../src/internal_mod.ts";
import type { ServerInfo } from "../src/internal_mod.ts";
import { assertEquals } from "@std/assert";
Deno.test("servers - single", () => {
const servers = new Servers(["127.0.0.1:4222"], { randomize: false });
assertEquals(servers.length(), 1);
assertEquals(servers.getServers().length, 1);
assertEquals(servers.getCurrentServer().listen, "127.0.0.1:4222");
let ni = 0;
servers.getServers().forEach((s) => {
if (s.gossiped) {
ni++;
}
});
assertEquals(ni, 0);
});
Deno.test("servers - multiples", () => {
const servers = new Servers(["h:1", "h:2"], { randomize: false });
assertEquals(servers.length(), 2);
assertEquals(servers.getServers().length, 2);
assertEquals(servers.getCurrentServer().listen, "h:1");
let ni = 0;
servers.getServers().forEach((s) => {
if (s.gossiped) {
ni++;
}
});
assertEquals(ni, 0);
});
function servInfo(): ServerInfo {
return {
max_payload: 1,
client_id: 1,
proto: 1,
version: "1",
} as ServerInfo;
}
Deno.test("servers - add/delete", () => {
const servers = new Servers(["127.0.0.1:4222"], { randomize: false });
assertEquals(servers.length(), 1);
let ce = servers.update(Object.assign(servInfo(), { connect_urls: ["h:1"] }));
assertEquals(ce.added.length, 1);
assertEquals(ce.deleted.length, 0);
assertEquals(servers.length(), 2);
let gossiped = servers.getServers().filter((s) => {
return s.gossiped;
});
assertEquals(gossiped.length, 1);
ce = servers.update(Object.assign(servInfo(), { connect_urls: [] }));
assertEquals(ce.added.length, 0);
assertEquals(ce.deleted.length, 1);
assertEquals(servers.length(), 1);
gossiped = servers.getServers().filter((s) => {
return s.gossiped;
});
assertEquals(gossiped.length, 0);
});
Deno.test("servers - url parse fn", () => {
const fn = (s: string): string => {
return `x://${s}`;
};
setTransportFactory({ urlParseFn: fn });
const s = new Servers(["127.0.0.1:4222"], { randomize: false });
s.update(Object.assign(servInfo(), { connect_urls: ["h:1", "j:2/path"] }));
const servers = s.getServers();
assertEquals(servers[0].src, "x://127.0.0.1:4222");
assertEquals(servers[1].src, "x://h:1");
assertEquals(servers[2].src, "x://j:2/path");
setTransportFactory({ urlParseFn: undefined });
});
Deno.test("servers - save tls name", () => {
const servers = new Servers(["h:1", "h:2"], { randomize: false });
servers.addServer("127.1.0.0", true);
servers.addServer("127.1.2.0", true);
servers.updateTLSName();
assertEquals(servers.length(), 4);
assertEquals(servers.getServers().length, 4);
assertEquals(servers.getCurrentServer().listen, "h:1");
const gossiped = servers.getServers().filter((s) => {
return s.gossiped;
});
assertEquals(gossiped.length, 2);
gossiped.forEach((sn) => {
assertEquals(sn.tlsName, "h");
});
});
Deno.test("servers - port 80", () => {
function t(hp: string, port: number) {
const servers = new Servers([hp]);
const s = servers.getCurrentServer();
assertEquals(s.port, port);
}
t("localhost:80", 80);
t("localhost:443", 443);
t("localhost:201", 201);
t("localhost", 4222);
t("localhost/foo", 4222);
t("localhost:2342/foo", 2342);
t("[2001:db8:4006:812::200e]:8080", 8080);
t("::1", 4222);
});
Deno.test("servers - hostname only", () => {
assertEquals(isIPV4OrHostname("hostname"), true);
assertEquals(isIPV4OrHostname("hostname:40"), true);
assertEquals(isIPV4OrHostname("::ffff:35.234.43.228"), false);
});
--- core/tests/timeout_test.ts ---
/*
* Copyright 2021-2024 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 {
assertInstanceOf,
assertRejects,
assertStringIncludes,
} from "@std/assert";
import { createInbox, Empty, errors } from "../src/internal_mod.ts";
import { cleanup, setup } from "../../test_helpers/mod.ts";
Deno.test("timeout - request noMux stack is useful", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const err = await assertRejects(() => {
return nc.request(subj, Empty, { noMux: true, timeout: 250 });
}, errors.RequestError);
assertInstanceOf(err.cause, errors.NoRespondersError);
assertStringIncludes((err as Error).stack || "", "timeout_test");
await cleanup(ns, nc);
});
Deno.test("timeout - request stack is useful", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const err = await assertRejects(() => {
return nc.request(subj, Empty, { timeout: 250 });
}, errors.RequestError);
assertInstanceOf(err.cause, errors.NoRespondersError);
assertStringIncludes((err as Error).stack || "", "timeout_test");
await cleanup(ns, nc);
});
--- core/tests/tls_test.ts ---
/*
* 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 { assertEquals, assertRejects } from "@std/assert";
import { connect } from "./connect.ts";
import { errors } from "../src/internal_mod.ts";
import type { NatsConnectionImpl } from "../src/internal_mod.ts";
import { cleanup, NatsServer } from "test_helpers";
Deno.test("tls - fail if server doesn't support TLS", async () => {
const ns = await NatsServer.start();
await assertRejects(
() => {
return connect({ port: ns.port, tls: {}, reconnect: false });
},
errors.ConnectionError,
"server does not support 'tls'",
);
await ns.stop();
});
Deno.test("tls - connects to tls without option", async () => {
const nc = await connect({ servers: "demo.nats.io" }) as NatsConnectionImpl;
await nc.flush();
assertEquals(nc.protocol.transport.isEncrypted(), true);
await nc.close();
});
Deno.test("tls - custom ca fails without root", async () => {
const tlsConfig = await NatsServer.tlsConfig();
const config = {
host: "0.0.0.0",
tls: tlsConfig.tls,
};
const ns = await NatsServer.start(config);
await assertRejects(
() => {
return connect({ servers: `localhost:${ns.port}`, reconnect: false });
},
errors.ConnectionError,
"invalid peer certificate: unknownissuer",
);
await ns.stop();
await Deno.remove(tlsConfig.certsDir, { recursive: true });
});
Deno.test("tls - custom ca with root connects", async () => {
const tlsConfig = await NatsServer.tlsConfig();
const config = {
host: "0.0.0.0",
tls: tlsConfig.tls,
};
const ns = await NatsServer.start(config);
const nc = await connect({
servers: `localhost:${ns.port}`,
tls: {
caFile: config.tls.ca_file,
},
});
await nc.flush();
await nc.close();
await ns.stop();
await Deno.remove(tlsConfig.certsDir, { recursive: true });
});
Deno.test("tls - available connects with or without", async () => {
const tlsConfig = await NatsServer.tlsConfig();
const config = {
host: "0.0.0.0",
allow_non_tls: true,
tls: tlsConfig.tls,
};
const ns = await NatsServer.start(config);
// will upgrade to tls but fail in the test because the
// certificate will not be trusted
await assertRejects(async () => {
await connect({
servers: `localhost:${ns.port}`,
});
});
// will upgrade to tls as tls is required
const a = connect({
servers: `localhost:${ns.port}`,
tls: {
caFile: config.tls.ca_file,
},
});
// will NOT upgrade to tls
const b = connect({
servers: `localhost:${ns.port}`,
tls: null,
});
const conns = await Promise.all([a, b]) as NatsConnectionImpl[];
await conns[0].flush();
await conns[1].flush();
assertEquals(conns[0].protocol.transport.isEncrypted(), true);
assertEquals(conns[1].protocol.transport.isEncrypted(), false);
await cleanup(ns, ...conns);
await Deno.remove(tlsConfig.certsDir, { recursive: true });
});
--- core/tests/token_test.ts ---
/*
* Copyright 2018-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 { assertRejects } from "@std/assert";
import { NatsServer } from "test_helpers";
import { connect } from "./connect.ts";
import { errors } from "../src/errors.ts";
const conf = { authorization: { token: "tokenxxxx" } };
Deno.test("token - empty", async () => {
const ns = await NatsServer.start(conf);
await assertRejects(() => {
return connect({ port: ns.port, reconnect: false, debug: true });
}, errors.AuthorizationError);
await ns.stop();
});
Deno.test("token - bad", async () => {
const ns = await NatsServer.start(conf);
await assertRejects(() => {
return connect(
{ port: ns.port, token: "bad", reconnect: false },
);
}, errors.AuthorizationError);
await ns.stop();
});
Deno.test("token - ok", async () => {
const ns = await NatsServer.start(conf);
const nc = await connect(
{ port: ns.port, token: "tokenxxxx" },
);
await nc.close();
await ns.stop();
});
--- core/tests/types_test.ts ---
/*
* Copyright 2018-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 } from "./connect.ts";
import type { Msg, NatsConnection } from "../src/internal_mod.ts";
import { createInbox, DataBuffer, deferred } from "../src/internal_mod.ts";
import { assert, assertEquals } from "@std/assert";
import { NatsServer } from "../../test_helpers/launcher.ts";
function mh(nc: NatsConnection, subj: string): Promise<Msg> {
const dm = deferred<Msg>();
const sub = nc.subscribe(subj, { max: 1 });
(async () => {
for await (const m of sub) {
dm.resolve(m);
}
})().then();
return dm;
}
Deno.test("types - json types", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port });
const subj = createInbox();
const dm = mh(nc, subj);
nc.publish(subj, JSON.stringify(6691));
const msg = await dm;
assertEquals(typeof msg.json(), "number");
assertEquals(msg.json<number>(), 6691);
await nc.close();
await ns.stop();
});
Deno.test("types - string types", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port });
const subj = createInbox();
const dm = mh(nc, subj);
nc.publish(subj, "hello world");
const msg = await dm;
assertEquals(msg.string(), "hello world");
await nc.close();
await ns.stop();
});
Deno.test("types - binary types", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port });
const subj = createInbox();
const dm = mh(nc, subj);
const payload = DataBuffer.fromAscii("hello world");
nc.publish(subj, payload);
const msg = await dm;
assert(msg.data instanceof Uint8Array);
assertEquals(msg.data, payload);
await nc.close();
await ns.stop();
});
--- core/tests/util_test.ts ---
/*
* Copyright 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, assertRejects } from "@std/assert";
import {
backoff,
deadline,
debugDeferred,
deferred,
millis,
nanos,
SimpleMutex,
} from "../src/util.ts";
import { TimeoutError } from "../src/errors.ts";
Deno.test("util - simple mutex", () => {
const r = new SimpleMutex(1);
assertEquals(r.max, 1);
assertEquals(r.current, 0);
r.lock().catch();
assertEquals(r.current, 1);
r.lock().catch();
assertEquals(r.current, 2);
assertEquals(r.waiting.length, 1);
r.unlock();
assertEquals(r.current, 1);
assertEquals(r.waiting.length, 0);
});
Deno.test("util - backoff", () => {
const b = backoff([0, 100, 200]);
assertEquals(b.backoff(0), 0);
let n = b.backoff(1);
assert(n >= 50 && 150 >= n, `${n} >= 50 && 150 >= ${n}`);
n = b.backoff(2);
assert(n >= 100 && 300 >= n, `${n} >= 100 && 300 >= ${n}`);
n = b.backoff(3);
assert(n >= 100 && 300 >= n, `${n} >= 100 && 300 >= ${n}`);
});
Deno.test("util - deadline", async () => {
await assertRejects(() => {
return deadline(deferred(), 100);
}, TimeoutError);
});
Deno.test("util - nanos", () => {
assertEquals(nanos(1000), 1000000000);
assertEquals(millis(1000000000), 1000);
});
Deno.test("util - backoff bad arg", () => {
//@ts-expect-error: test
const b = backoff("hello");
assert(b.backoff(1) > 0);
});
Deno.test("util - debug deferred", async () => {
await assertRejects(() => {
const d = debugDeferred();
d.reject("hello world");
return d;
});
});
--- core/tests/ws_test.ts ---
/*
* Copyright 2024 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,
assertFalse,
assertRejects,
} from "@std/assert";
import {
createInbox,
errors,
wsconnect,
wsUrlParseFn,
} from "../src/internal_mod.ts";
import type { NatsConnectionImpl } from "../src/nats.ts";
import {
assertBetween,
cleanup,
Lock,
NatsServer,
wsServerConf,
} from "test_helpers";
Deno.test("ws - connect", async () => {
const ns = await NatsServer.start(wsServerConf());
const nc = await wsconnect({ servers: `ws://127.0.0.1:${ns.websocket}` });
await nc.flush();
await cleanup(ns, nc);
});
// Fixme: allow sanitizer once ws transport closes cleanly.
Deno.test("ws - wss connection", async () => {
const ns = await NatsServer.start(wsServerConf());
const nc = await wsconnect({
servers: `wss://demo.nats.io:8443`,
});
assertEquals(
(nc as NatsConnectionImpl).protocol.transport?.isEncrypted(),
true,
);
await nc.flush();
await cleanup(ns, nc);
});
Deno.test(
"ws - pubsub",
async () => {
const ns = await NatsServer.start(wsServerConf());
const nc = await wsconnect({ servers: `ws://127.0.0.1:${ns.websocket}` });
const sub = nc.subscribe(createInbox());
const done = (async () => {
for await (const m of sub) {
return m;
}
})().then();
nc.publish(sub.getSubject(), "hello world");
const r = await done;
assertExists(r);
assertEquals(r.subject, sub.getSubject());
assertEquals(r.string(), "hello world");
await cleanup(ns, nc);
},
);
Deno.test(
"ws - disconnect reconnects",
async () => {
const ns = await NatsServer.start(wsServerConf());
const nc = await wsconnect({ servers: `ws://127.0.0.1:${ns.websocket}` });
const status = nc.status();
const done = (async () => {
for await (const s of status) {
switch (s.type) {
case "reconnect":
return;
default:
}
}
})();
await nc.reconnect();
await done;
await cleanup(ns, nc);
},
);
Deno.test("ws - tls options are not supported", async () => {
await assertRejects(
() => {
return wsconnect({ servers: "wss://demo.nats.io:8443", tls: {} });
},
errors.InvalidArgumentError,
"'tls' is not configurable on w3c websocket connections",
);
});
Deno.test(
"ws - indefinite reconnects",
async () => {
let ns = await NatsServer.start(wsServerConf());
const nc = await wsconnect({
servers: `ws://127.0.0.1:${ns.websocket}`,
reconnectTimeWait: 100,
maxReconnectAttempts: -1,
});
let disconnects = 0;
let reconnects = 0;
let reconnect = false;
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "disconnect":
disconnects++;
break;
case "reconnect":
reconnect = true;
nc.close();
break;
case "reconnecting":
reconnects++;
break;
}
}
})().then();
await ns.stop();
const lock = Lock(1);
setTimeout(async () => {
ns = await ns.restart();
lock.unlock();
}, 1000);
await nc.closed();
await ns.stop();
await lock;
await ns.stop();
assertBetween(reconnects, 4, 10);
assert(reconnect);
assert(disconnects >= 1);
},
);
Deno.test("ws - basics", async () => {
const ns = await NatsServer.start(wsServerConf());
const nc = await wsconnect({
servers: `ws://127.0.0.1:${ns.websocket}`,
}) as NatsConnectionImpl;
const t = nc.protocol.transport;
assertFalse(t.isClosed);
await nc.close();
assertEquals(t.isClosed, true);
await ns.stop();
});
Deno.test("ws - url parse", () => {
const u = [
{ in: "foo", expect: "wss://foo:443/" },
{ in: "foo:100", expect: "wss://foo:100/" },
{ in: "foo/", expect: "wss://foo:443/" },
{ in: "foo/hello", expect: "wss://foo:443/hello" },
{ in: "foo:100/hello", expect: "wss://foo:100/hello" },
{ in: "foo/hello?one=two", expect: "wss://foo:443/hello?one=two" },
{ in: "foo:100/hello?one=two", expect: "wss://foo:100/hello?one=two" },
{ in: "nats://foo", expect: "ws://foo:80/" },
{ in: "tls://foo", expect: "wss://foo:443/" },
{ in: "ws://foo", expect: "ws://foo:80/" },
{ in: "ws://foo:100", expect: "ws://foo:100/" },
{
in: "[2001:db8:1f70::999:de8:7648:6e8]",
expect: "wss://[2001:db8:1f70:0:999:de8:7648:6e8]:443/",
},
{
in: "[2001:db8:1f70::999:de8:7648:6e8]:100",
expect: "wss://[2001:db8:1f70:0:999:de8:7648:6e8]:100/",
},
];
u.forEach((tc) => {
const out = wsUrlParseFn(tc.in);
assertEquals(out, tc.expect, `test ${tc.in}`);
});
});
Deno.test("ws - wsURLParseFn", () => {
assertEquals(wsUrlParseFn("localhost", true), "wss://localhost:443/");
assertEquals(wsUrlParseFn("localhost", false), "ws://localhost:80/");
assertEquals(wsUrlParseFn("http://localhost"), "ws://localhost:80/");
assertEquals(wsUrlParseFn("https://localhost"), "wss://localhost:443/");
});
--- core/README.md ---
[![License](https://img.shields.io/badge/Licence-Apache%202.0-blue.svg)](./LICENSE)
![nats-core](https://github.com/nats-io/nats.js/actions/workflows/test.yml/badge.svg)
[![JSDoc](https://img.shields.io/badge/JSDoc-reference-blue)](https://nats-io.github.io/nats.js/core/index.html)
[![JSR](https://jsr.io/badges/@nats-io/nats-core)](https://jsr.io/@nats-io/nats-core)
[![JSR](https://jsr.io/badges/@nats-io/nats-core/score)](https://jsr.io/@nats-io/nats-core)
[![NPM Version](https://img.shields.io/npm/v/%40nats-io%2Fnats-core)](https://www.npmjs.com/package/@nats-io/nats-core)
![NPM Downloads](https://img.shields.io/npm/dt/%40nats-io%2Fnats-core)
![NPM Downloads](https://img.shields.io/npm/dm/%40nats-io%2Fnats-core)
# Core
The _core_ module implements the _core_ functionality for JavaScript clients:
- Connection, authentication, connection lifecycle
- NATS protocol handling - messaging functionality (publish, subscribe and
request reply)
A native transports (node, deno) modules are a peer module that export a
`connect` function which returns a concrete instance of a `NatsConnection`. The
transport library re-exports all the functionality in this module, to make it
the entry point into the NATS JavaScript ecosystem.
You can use this module as a runtime agnostic dependency and implement
functionality that uses a NATS client connection without binding your code to a
particular runtime. For example, the @nats-io/jetstream library depends on
@nats-io/nats-core to implement all of its JetStream protocol.
## WebSocket Support
The _core_ module also offers a for W3C Websocket transport (aka browser, Deno,
and Node v22) via the exported `wsconnect` function. This function is
semantically equivalent to the traditional `connect`, but returns a
`NatsConnection` that is backed by a W3C WebSocket.
Note that wsconnect assumes `wss://` connections. If you provide a port, it
likewise resolve to `wss://localhost:443`. If you specify a `ws://` URL, the
client assumes port 80, which is likely not the port. Check your server
configuration as the port for WebSocket protocol is NOT 4222.
# Installation
For a quick overview of the libraries and how to install them, see
[runtimes.md](../runtimes.md).
If you are not implementing a NATS client compatible module, you can use this
repository to view the documentation of the NATS core functionality. Your NATS
client instance already uses and re-exports the module implemented here, so
there's no need for you to directly depend on this library.
Note that this module is distributed in two different registries:
- npm a node-specific library supporting CJS (`require`) and ESM (`import`) for
node specific projects
- jsr a node and other ESM (`import`) compatible runtimes (deno, browser, node)
If your application doesn't use `require`, you can simply depend on the JSR
version.
## NPM
The NPM registry hosts a node-only compatible version of the library
[@nats-io/nats-core](https://www.npmjs.com/package/@nats-io/nats-core)
supporting both CJS and ESM:
```bash
npm install @nats-io/nats-core
```
## JSR
The JSR registry hosts the ESM-only
[@nats-io/nats-core](https://jsr.io/@nats-io/nats-core) version of the library.
```bash
deno add jsr:@nats-io/nats-core
```
```bash
npx jsr add @nats-io/nats-core
```
```bash
yarn dlx jsr add @nats-io/nats-core
```
```bash
bunx jsr add @nats-io/nats-core
```
## Referencing the library
Once you import the library, you can reference in your code as:
```javascript
import * as nats_core from "@nats-io/nats-core";
// or in node (only when using CJS)
const nats_core = require("@nats-io/nats-core");
```
The main entry point for this library is the `NatsConnection`.
## Basics
### Connecting to a nats-server
To connect to a server you use the `connect()` exposed by your selected
transport. The connect function returns a connection which implements the
`NatsConnection` that you can use to interact with the server.
The `connect()` function will accept a `ConnectOptions` which customizes some of
the client behaviors. In general all options apply to all transports where
possible. Options that are non-sensical on a particular runtime will be
documented by your transport module.
By default, `connect()` will attempt a connection on `127.0.0.1:4222`. If the
connection is dropped, the client will attempt to reconnect. You can customize
the server you want to connect to by specifying `port` (for local connections),
or full hostport on the `servers` option. Note that the `servers` option can be
a single hostport (a string) or an array of hostports.
The example below will attempt to connect to different servers by specifying
different `ConnectionOptions`. At least two of them should work if your internet
is working.
```typescript
// import the connect function from a transport
import { connect } from "@nats-io/transport-deno";
const servers = [
{},
{ servers: ["demo.nats.io:4442", "demo.nats.io:4222"] },
{ servers: "demo.nats.io:4443" },
{ port: 4222 },
{ servers: "localhost" },
];
servers.forEach(async (v) => {
try {
const nc = await connect(v);
console.log(`connected to ${nc.getServer()}`);
// this promise indicates the client closed
const done = nc.closed();
// do something with the connection
// close the connection
await nc.close();
// check if the close was OK
const err = await done;
if (err) {
console.log(`error closing:`, err);
}
} catch (_err) {
console.log(`error connecting to ${JSON.stringify(v)}`);
}
});
```
To disconnect from the nats-server, call `close()` on the connection. A
connection can also be terminated when an unexpected error happens. For example,
the server returns a run-time error. In those cases, the client will re-initiate
a connection, it the connection options allow it.
By default, the client will always attempt to reconnect if the connection is
disrupted for a reason other than calling `close()`. To get notified when the
connection is closed, await the resolution of the Promise returned by
`closed()`. If closed resolves to a value, the value is a `NatsError` indicating
why the connection closed.
### Publish and Subscribe
The basic NATS client operations are `publish` to send messages and `subscribe`
to receive messages.
Messages are published to a _subject_. A _subject_ is like a URL with the
exception that it doesn't specify an actual endpoint. Subjects can be any
string, but until you learn more about NATS stick to the simple rule that
subjects that are just simple ASCII printable letters and number characters. All
recipients that have expressed interest in a subject will receive messages
addressed to that subject (provided they have access and permissions to get it).
To express interest in a subject, you create a `subscription`.
In JavaScript clients subscriptions work as an async iterator - clients simply
loop to process messages as they happen.
NATS messages are payload agnostic. Payloads are `Uint8Arrays` or `string`.
Messages also provide a `string()` and `json()` that allows you to convert the
underlying `Uint8Array` into a string or parse by using `JSON.parse()` (which
can fail to parse if the payload is not the expected format).
To cancel a subscription and terminate your interest, you call `unsubscribe()`
or `drain()` on a subscription. Unsubscribe will typically terminate regardless
of whether there are messages in flight for the client. Drain ensures that all
messages that are inflight are processed before canceling the subscription.
Connections can also be drained as well. Draining a connection closes it, after
all subscriptions have been drained and all outbound messages have been sent to
the server.
```typescript
// import the connect function from a transport
import { connect } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "demo.nats.io:4222" });
// create a simple subscriber and iterate over messages
// matching the subscription
const sub = nc.subscribe("hello");
(async () => {
for await (const m of sub) {
console.log(`[${sub.getProcessed()}]: ${m.string()}`);
}
console.log("subscription closed");
})();
nc.publish("hello", "world");
nc.publish("hello", "again");
// we want to ensure that messages that are in flight
// get processed, so we are going to drain the
// connection. Drain is the same as close, but makes
// sure that all messages in flight get seen
// by the iterator. After calling drain,
// the connection closes.
await nc.drain();
```
```typescript
interface Person {
name: string;
}
// create a simple subscriber and iterate over messages
// matching the subscription
const sub = nc.subscribe("people");
(async () => {
for await (const m of sub) {
// typescript will see this as a Person
const p = m.json<Person>();
console.log(`[${sub.getProcessed()}]: ${p.name}`);
}
})();
const p = { name: "Memo" } as Person;
nc.publish("people", JSON.stringify(p));
```
### Wildcard Subscriptions
Subjects can be used to organize messages into hierarchies. For example, a
subject may contain additional information that can be useful in providing a
context to the message, such as the ID of the client that sent the message, or
the region where a message originated.
Instead of subscribing to each specific subject, you can create subscriptions
that have subjects with wildcards. Wildcards match one or more tokens in a
subject. A token is a string following a period (`.`).
All subscriptions are independent. If two different subscriptions match a
subject, both will get to process the message:
```typescript
import { connect } from "@nats-io/transport-deno";
import type { Subscription } from "@nats-io/transport-deno";
const nc = await connect({ servers: "demo.nats.io:4222" });
// subscriptions can have wildcard subjects
// the '*' matches any string in the specified token position
const s1 = nc.subscribe("help.*.system");
const s2 = nc.subscribe("help.me.*");
// the '>' matches any tokens in that position or following
// '>' can only be specified at the end
const s3 = nc.subscribe("help.>");
async function printMsgs(s: Subscription) {
const subj = s.getSubject();
console.log(`listening for ${subj}`);
const c = 13 - subj.length;
const pad = "".padEnd(c);
for await (const m of s) {
console.log(
`[${subj}]${pad} #${s.getProcessed()} - ${m.subject} ${
m.data ? " " + m.string() : ""
}`,
);
}
}
printMsgs(s1);
printMsgs(s2);
printMsgs(s3);
// don't exit until the client closes
await nc.closed();
```
### Services: Request/Reply
Request/Reply is NATS equivalent to an HTTP request. To make requests you
publish messages as you did before, but also specify a `reply` subject. The
`reply` subject is where a service will publish (send) your response.
NATS provides syntactic sugar, for publishing requests. The `request()` API will
generate a reply subject and manage the creation of a subscription under the
covers to receive the reply. It will also start a timer to ensure that if a
response is not received within your specified time, the request fails. The
example also illustrates a graceful shutdown.
#### Services
Here's an example of a service. It is a bit more complicated than expected
simply to illustrate not only how to create responses, but how the subject
itself is used to dispatch different behaviors.
```typescript
import { connect, Subscription } from "@nats-io/nats-deno";
// create a connection
const nc = await connect({ servers: "demo.nats.io" });
// this subscription listens for `time` requests and returns the current time
const sub = nc.subscribe("time");
(async (sub: Subscription) => {
console.log(`listening for ${sub.getSubject()} requests...`);
for await (const m of sub) {
if (m.respond(new Date().toISOString())) {
console.info(`[time] handled #${sub.getProcessed()}`);
} else {
console.log(`[time] #${sub.getProcessed()} ignored - no reply subject`);
}
}
console.log(`subscription ${sub.getSubject()} drained.`);
})(sub);
// this subscription listens for admin.uptime and admin.stop
// requests to admin.uptime returns how long the service has been running
// requests to admin.stop gracefully stop the client by draining
// the connection
const started = Date.now();
const msub = nc.subscribe("admin.*");
(async (sub: Subscription) => {
console.log(`listening for ${sub.getSubject()} requests [uptime | stop]`);
// it would be very good to verify the origin of the request
// before implementing something that allows your service to be managed.
// NATS can limit which client can send or receive on what subjects.
for await (const m of sub) {
const chunks = m.subject.split(".");
console.info(`[admin] #${sub.getProcessed()} handling ${chunks[1]}`);
switch (chunks[1]) {
case "uptime":
// send the number of millis since up
m.respond(`${Date.now() - started}`);
break;
case "stop": {
m.respond(`[admin] #${sub.getProcessed()} stopping....`);
// gracefully shutdown
nc.drain()
.catch((err) => {
console.log("error draining", err);
});
break;
}
default:
console.log(
`[admin] #${sub.getProcessed()} ignoring request for ${m.subject}`,
);
}
}
console.log(`subscription ${sub.getSubject()} drained.`);
})(msub);
// wait for the client to close here.
await nc.closed().then((err?: void | Error) => {
let m = `connection to ${nc.getServer()} closed`;
if (err) {
m = `${m} with an error: ${err.message}`;
}
console.log(m);
});
```
#### Making Requests
Here's a simple example of a client making a simple request from the service
above:
```typescript
import { connect, Empty } from "../../src/types.ts";
// create a connection
const nc = await connect({ servers: "demo.nats.io:4222" });
// the client makes a request and receives a promise for a message
// by default the request times out after 1s (1000 millis) and has
// no payload.
await nc.request("time", Empty, { timeout: 1000 })
.then((m) => {
console.log(`got response: ${m.string()}`);
})
.catch((err) => {
console.log(`problem with request: ${err.message}`);
});
await nc.close();
```
Of course you can also use a tool like the nats cli:
```bash
> nats -s demo.nats.io req time ""
11:39:59 Sending request on "time"
11:39:59 Received with rtt 97.814458ms
2024-06-26T16:39:59.710Z
> nats -s demo.nats.io req admin.uptime ""
11:38:41 Sending request on "admin.uptime"
11:38:41 Received with rtt 99.065458ms
61688
>nats -s demo.nats.io req admin.stop ""
11:39:08 Sending request on "admin.stop"
11:39:08 Received with rtt 100.004959ms
[admin] #5 stopping....
```
### Queue Groups
Queue groups allow scaling of services horizontally. Subscriptions for members
of a queue group are treated as a single service. When you send a message to a
queue group subscription, only a single client in a queue group will receive it.
There can be any number of queue groups. Each group is treated as its own
independent unit. Note that non-queue subscriptions are also independent of
subscriptions in a queue group.
```typescript
import { connect } from "@nats-io/transport-deno";
import type { NatsConnection, Subscription } from "@nats-io/transport-deno";
async function createService(
name: string,
count = 1,
queue = "",
): Promise<NatsConnection[]> {
const conns: NatsConnection[] = [];
for (let i = 1; i <= count; i++) {
const n = queue ? `${name}-${i}` : name;
const nc = await connect(
{ servers: "demo.nats.io:4222", name: `${n}` },
);
nc.closed()
.then((err) => {
if (err) {
console.error(
`service ${n} exited because of error: ${err.message}`,
);
}
});
// create a subscription - note the option for a queue, if set
// any client with the same queue will be the queue group.
const sub = nc.subscribe("echo", { queue: queue });
const _ = handleRequest(n, sub);
console.log(`${n} is listening for 'echo' requests...`);
conns.push(nc);
}
return conns;
}
// simple handler for service requests
async function handleRequest(name: string, s: Subscription) {
const p = 12 - name.length;
const pad = "".padEnd(p);
for await (const m of s) {
// respond returns true if the message had a reply subject, thus it could respond
if (m.respond(m.data)) {
console.log(
`[${name}]:${pad} #${s.getProcessed()} echoed ${m.string()}`,
);
} else {
console.log(
`[${name}]:${pad} #${s.getProcessed()} ignoring request - no reply subject`,
);
}
}
}
// let's create two queue groups and a standalone subscriber
const conns: NatsConnection[] = [];
conns.push(...await createService("echo", 3, "echo"));
conns.push(...await createService("other-echo", 2, "other-echo"));
conns.push(...await createService("standalone"));
const a: Promise<void | Error>[] = [];
conns.forEach((c) => {
a.push(c.closed());
});
await Promise.all(a);
```
Run it and publish a request to the subject `echo` to see what happens.
## Advanced Usage
### Headers
NATS headers are similar to HTTP headers. Headers are enabled automatically if
the server supports them. Note that if you publish a message using headers but
the server doesn't support them, an Error is thrown. Also note that even if you
are publishing a message with a header, it is possible for the recipient to not
support them.
```typescript
import { connect, createInbox, Empty, headers } from "../../src/types.ts";
import { nuid } from "../../nats-base-client/nuid.ts";
const nc = await connect(
{
servers: `demo.nats.io`,
},
);
const subj = createInbox();
const sub = nc.subscribe(subj);
(async () => {
for await (const m of sub) {
if (m.headers) {
for (const [key, value] of m.headers) {
console.log(`${key}=${value}`);
}
// reading a header is not case sensitive
console.log("id", m.headers.get("id"));
}
}
})().then();
// header names can be any printable ASCII character with the exception of `:`.
// header values can be any ASCII character except `\r` or `\n`.
// see https://www.ietf.org/rfc/rfc822.txt
const h = headers();
h.append("id", nuid.next());
h.append("unix_time", Date.now().toString());
nc.publish(subj, Empty, { headers: h });
await nc.flush();
await nc.close();
```
### No Responders
Requests can fail for many reasons. A common reason for a failure is the lack of
interest in the subject. Typically these surface as a timeout error. If the
server is enabled to use headers, it will also enable a `no responders` feature.
If you send a request for which there's no interest, the request will be
immediately rejected:
```typescript
import { connect } from "@nats-io/transport-deno";
import {
NoRespondersError,
RequestError,
TimeoutError,
} from "@nats-io/transport-deno";
const nc = await connect({
servers: `demo.nats.io`,
});
try {
const m = await nc.request("hello.world");
console.log(m.data);
} catch (err) {
if (err instanceof RequestError) {
if (err.cause instanceof TimeoutError) {
console.log("someone is listening but didn't respond");
} else if (err.cause instanceof NoRespondersError) {
console.log("no one is listening to 'hello.world'");
} else {
console.log(
`failed due to unknown error: ${(err.cause as Error)?.message}`,
);
}
} else {
console.log(`request failed: ${(err as Error).message}`);
}
}
await nc.close();
```
### Authentication
NATS supports many different forms of credentials:
- username/password
- token
- NKEYS
- client certificates
- JWTs
For user/password and token authentication, you can simply provide them as
`ConnectionOptions` - see `user`, `pass`, `token`. Internally these mechanisms
are implemented as an `Authenticator`. An `Authenticator` is simply a function
that handles the type of authentication specified.
Setting the `user`/`pass` or `token` options, simply initializes an
`Authenticator` and sets the username/password.
```typescript
// if the connection requires authentication, provide `user` and `pass` or
// `token` options in the NatsConnectionOptions
import { connect } from "@nats-io/transport-deno";
const nc1 = await connect({
servers: "127.0.0.1:4222",
user: "jenny",
pass: "867-5309",
});
const nc2 = await connect({ port: 4222, token: "t0pS3cret!" });
```
#### Authenticators
NKEYs and JWT authentication are more complex, as they cryptographically respond
to a server challenge.
Because NKEY and JWT authentication may require reading data from a file or an
HTTP cookie, these forms of authentication will require a bit more from the
developer to activate them. The work is related to accessing these resources
varies depending on the platform.
After the credential artifacts are read, you can use one of these functions to
create the authenticator. You then simply assign it to the `authenticator`
property of the `ConnectionOptions`:
- `nkeyAuthenticator(seed?: Uint8Array | (() => Uint8Array)): Authenticator`
- `jwtAuthenticator(jwt: string | (() => string), seed?: Uint8Array | (()=> Uint8Array)): Authenticator`
- `credsAuthenticator(creds: Uint8Array | (() => Uint8Array)): Authenticator`
Note that the authenticators provide the ability to specify functions that
return the desired value. This enables dynamic environments such as a browser
where values accessed by fetching a value from a cookie.
Here's an example:
```javascript
// read the creds file as necessary, in the case it
// is part of the code for illustration purposes (this a partial creds)
const creds = `-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJqdSDJB....
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN USER NKEY SEED-----
SUAIBDPBAUTW....
------END USER NKEY SEED------
`;
const nc = await connect(
{
port: 4222,
authenticator: credsAuthenticator(new TextEncoder().encode(creds)),
},
);
```
### Flush
Flush sends a `PING` protocol message to the server. When the server responds
with `PONG` you are guaranteed that all pending data was sent and received by
the server. Note `ping()` effectively adds a server round-trip. All NATS clients
handle their buffering optimally, so `ping(): Promise<void>` shouldn't be used
except in cases where you are writing some sort of test, as you may be degrading
the performance of the client.
```javascript
nc.publish("foo");
nc.publish("bar");
await nc.flush();
```
### `PublishOptions`
When you publish a message you can specify some options:
- `reply` - this is a subject to receive a reply (you must set up a subscription
on the reply subject) before you publish.
- `headers` - a set of headers to decorate the message.
### `SubscriptionOptions`
You can specify several options when creating a subscription:
- `max`: maximum number of messages to receive - auto unsubscribe
- `timeout`: how long to wait for the first message
- `queue`: the [queue group](#queue-groups) name the subscriber belongs to
- `callback`: a function with the signature
`(err: Error|null, msg: Msg) => void;` that should be used for handling the
message. Subscriptions with callbacks are NOT iterators.
#### Auto Unsubscribe
```javascript
// subscriptions can auto unsubscribe after a certain number of messages
nc.subscribe("foo", { max: 10 });
```
#### Timeout Subscriptions
```javascript
// create subscription with a timeout, if no message arrives
// within the timeout, the subscription throws a timeout error
const sub = nc.subscribe("hello", { timeout: 1000 });
(async () => {
for await (const _m of sub) {
// handle the messages
}
})().catch((err) => {
if (err instanceof TimeoutError) {
console.log(`sub timed out!`);
} else {
console.log(`sub iterator got an error!`);
}
nc.close();
});
```
### `RequestOptions`
When making a request, there are several options you can pass:
- `timeout`: how long to wait for the response
- `headers`: optional headers to include with the message
- `noMux`: create a new subscription to handle the request. Normally a shared
subscription is used to receive response messages.
- `reply`: optional subject where the reply should be sent.
#### `noMux` and `reply`
Under the hood, the request API simply uses a wildcard subscription to handle
all requests you send.
In some cases, the default subscription strategy doesn't work correctly. For
example, a client may be constrained by the subjects where it can receive
replies.
When `noMux` is set to `true`, the client will create a normal subscription for
receiving the response to a generated inbox subject before the request is
published. The `reply` option can be used to override the generated inbox
subject with an application provided one. Note that setting `reply` requires
`noMux` to be `true`:
```typescript
const m = await nc.request(
"q",
Empty,
{ reply: "bar", noMux: true, timeout: 1000 },
);
```
### Draining Connections and Subscriptions
Draining provides for a graceful way to unsubscribe or close a connection
without losing messages that have already been dispatched to the client.
You can drain a subscription or all subscriptions in a connection.
When you drain a subscription, the client sends an `unsubscribe` protocol
message to the server followed by a `flush`. The subscription handler is only
removed after the server responds. Thus all pending messages for the
subscription have been processed.
Draining a connection, drains all subscriptions. However when you drain the
connection it becomes impossible to make new subscriptions or send new requests.
After the last subscription is drained it also becomes impossible to publish a
message. These restrictions do not exist when just draining a subscription.
### Lifecycle and Informational Events
Clients can get notification on various event types by calling
`status(): AsyncIterable<Status>` on the connection, the currently included
status `type`s include:
- `disconnect` - the client disconnected from the specified `server`
- `reconnect` - the client reconnected to the specified `server`
- `reconnecting` - the client is in its reconnect loop
- `update` - the cluster configuration has been updated, if servers were added
the `added` list will specify them, if servers were deleted servers the
`deleted` list will specify them.
- `ldm` - the server has started its lame duck mode and will evict clients
- `error` - an async error (such as a permission violation) was received, the
error is specified in the `error` property. Note that permission errors for
subscriptions are also notified to the subscription.
- `ping` - the server has not received a response for client pings, the number
of outstanding pings are notified in the `pendingPings` property. Note that
this should onlyl be `1` under normal operations.
- `staleConnection` - the connection is stale (client will reconnect)
- `forceReconnect` - the client has been instructed to reconnect because of
user-code (`reconnect()`)
- `close` - the client has closed, no further reconnects will be attempted.
```javascript
const nc = await connect(opts);
(async () => {
console.info(`connected ${nc.getServer()}`);
for await (const s of nc.status()) {
switch (s.type) {
case "disconnect":
case "reconnect":
console.log(s);
break;
default:
// ignored
}
}
})().then();
nc.closed()
.then((err) => {
console.log(
`connection closed ${err ? " with error: " + err.message : ""}`,
);
});
```
Be aware that when a client closes, you will need to wait for the `closed()`
promise to resolve. When it resolves, the client is done and will not reconnect.
### Async vs. Callbacks
Previous versions of the JavaScript NATS clients specified callbacks for message
processing. This required complex handling logic when a service required
coordination of operations. Callbacks are an inversion of control anti-pattern.
The async APIs trivialize complex coordination and makes your code easier to
maintain. With that said, there are some implications:
- Async subscriptions buffer inbound messages.
- Subscription processing delays until the runtime executes the promise related
microtasks at the end of an event loop.
In a traditional callback-based library, I/O happens after all data yielded by a
read in the current event loop completes processing. This means that callbacks
are invoked as part of processing. With async, the processing is queued in a
microtask queue. At the end of the event loop, the runtime processes the
microtasks, which in turn resumes your functions. As expected, this increases
latency, but also provides additional liveliness.
To reduce async latency, the NATS client allows processing a subscription in the
same event loop that dispatched the message. Simply specify a `callback` in the
subscription options. The signature for a callback is
`(err: (NatsError|null), msg: Msg) => void`. When specified, the subscription
iterator will never yield a message, as the callback will intercept all
messages.
Note that `callback` likely shouldn't even be documented, as likely it is a
workaround to an underlying application problem where you should be considering
a different strategy to horizontally scale your application, or reduce pressure
on the clients, such as using queue workers, or more explicitly targeting
messages. With that said, there are many situations where using callbacks can be
more performant or appropriate.
## Connection Options
The following is the list of connection options and default values.
| Option | Default | Description |
| ----------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authenticator` | none | Specifies the authenticator function that sets the client credentials. |
| `debug` | `false` | If `true`, the client prints protocol interactions to the console. Useful for debugging. |
| `ignoreClusterUpdates` | `false` | If `true` the client will ignore any cluster updates provided by the server. |
| `ignoreAuthErrorAbort` | `false` | Prevents client connection aborts if the client fails more than twice in a row with an authentication error |
| `inboxPrefix` | `"_INBOX"` | Sets de prefix for automatically created inboxes - `createInbox(prefix)` |
| `maxPingOut` | `2` | Max number of pings the client will allow unanswered before raising a stale connection error. |
| `maxReconnectAttempts` | `10` | Sets the maximum number of reconnect attempts. The value of `-1` specifies no limit. |
| `name` | | Optional client name - recommended to be set to a unique client name. |
| `noAsyncTraces` | `false` | When `true` the client will not add additional context to errors associated with request operations. Setting this option to `true` will greatly improve performance of request/reply and JetStream publishers. |
| `noEcho` | `false` | Subscriptions receive messages published by the client. Requires server support (1.2.0). If set to true, and the server does not support the feature, an error with code `NO_ECHO_NOT_SUPPORTED` is emitted, and the connection is aborted. Note that it is possible for this error to be emitted on reconnect when the server reconnects to a server that does not support the feature. |
| `noRandomize` | `false` | If set, the order of user-specified servers is randomized. |
| `noResolve` | none | If true, client will not resolve host names. |
| `pass` | | Sets the password for a connection. |
| `pedantic` | `false` | Turns on strict subject format checks. |
| `pingInterval` | `120000` | Number of milliseconds between client-sent pings. |
| `port` | `4222` | Port to connect to (only used if `servers` is not specified). |
| `reconnect` | `true` | If false, client will not attempt reconnecting. |
| `reconnectDelayHandler` | Generated function | A function that returns the number of millis to wait before the next connection to a server it connected to `()=>number`. |
| `reconnectJitter` | `100` | Number of millis to randomize after `reconnectTimeWait`. |
| `reconnectJitterTLS` | `1000` | Number of millis to randomize after `reconnectTimeWait` when TLS options are specified. |
| `reconnectTimeWait` | `2000` | If disconnected, the client will wait the specified number of milliseconds between reconnect attempts. |
| `servers` | `"localhost:4222"` | String or Array of hostport for servers. |
| `timeout` | 20000 | Number of milliseconds the client will wait for a connection to be established. If it fails it will emit a `connection_timeout` event with a NatsError that provides the hostport of the server where the connection was attempted. |
| `tls` | TlsOptions | A configuration object for requiring a TLS connection (not applicable to nats.ws). |
| `token` | | Sets a authorization token for a connection. |
| `user` | | Sets the username for a connection. |
| `verbose` | `false` | Turns on `+OK` protocol acknowledgements. |
| `waitOnFirstConnect` | `false` | If `true` the client will fall back to a reconnect mode if it fails its first connection attempt. |
### TlsOptions
| Option | Default | Description |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `ca` | N/A | CA certificate |
| `caFile` | | CA certificate filepath |
| `cert` | N/A | Client certificate |
| `certFile` | N/A | Client certificate file path |
| `key` | N/A | Client key |
| `keyFile` | N/A | Client key file path |
| `handshakeFirst` | false | Connects to the server directly as TLS rather than upgrade the connection. Note that the server must be configured accordingly. |
In some Node and Deno clients, having the option set to an empty option,
requires the client have a secured connection.
### Jitter
The settings `reconnectTimeWait`, `reconnectJitter`, `reconnectJitterTLS`,
`reconnectDelayHandler` are all related. They control how long before the NATS
client attempts to reconnect to a server it has previously connected.
The intention of the settings is to spread out the number of clients attempting
to reconnect to a server over a period of time, and thus preventing a
["Thundering Herd"](https://docs.nats.io/developing-with-nats/reconnect/random).
The relationship between these is:
- If `reconnectDelayHandler` is specified, the client will wait the value
returned by this function. No other value will be taken into account.
- If the client specified TLS options, the client will generate a number between
0 and `reconnectJitterTLS` and add it to `reconnectTimeWait`.
- If the client didn't specify TLS options, the client will generate a number
between 0 and `reconnectJitter` and add it to `reconnectTimeWait`.
--- jetstream/examples/01_consumers.js ---
/*
* Copyright 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 } from "@nats-io/transport-deno";
import { jetstream } from "@nats-io/jetstream";
import { setupStreamAndConsumer } from "./util.js";
// create a connection
const nc = await connect();
const { stream, consumer } = await setupStreamAndConsumer(nc);
// retrieve an existing consumer
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
console.log(await c.info(true));
// getting an ordered consumer requires no name
// as the library will create it
const oc = await js.consumers.get(stream);
console.log(await oc.info(true));
await nc.close();
--- jetstream/examples/02_next.js ---
/*
* Copyright 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 } from "@nats-io/transport-deno";
import { jetstream } from "@nats-io/jetstream";
import { setupStreamAndConsumer } from "./util.js";
// create a connection
const nc = await connect();
// create a stream with a random name with some messages and a consumer
const { stream, consumer } = await setupStreamAndConsumer(nc);
// retrieve an existing consumer
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
// the consumer is simply asked for one message by default
// this will resolve in 30s or null is returned
const m = await c.next();
if (m) {
console.log(m.subject);
m.ack();
} else {
console.log(`didn't get a message`);
}
await nc.close();
--- jetstream/examples/03_batch.js ---
/*
* Copyright 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 } from "@nats-io/transport-deno";
import { jetstream } from "@nats-io/jetstream";
import { setupStreamAndConsumer } from "./util.js";
// create a connection
const nc = await connect();
// create a stream with a random name with some messages and a consumer
const { stream, consumer } = await setupStreamAndConsumer(nc, 10);
// retrieve an existing consumer
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
// with fetch the consumer can manually control the buffering
for (let i = 0; i < 3; i++) {
const messages = await c.fetch({ max_messages: 4, expires: 2000 });
// console.log(messages);
for await (const m of messages) {
m.ack();
}
console.log(`batch completed: ${messages.getProcessed()} msgs processed`);
}
await nc.close();
--- jetstream/examples/04_consume.js ---
/*
* Copyright 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 } from "@nats-io/transport-deno";
import { jetstream } from "@nats-io/jetstream";
import { setupStreamAndConsumer } from "./util.js";
// create a connection
const nc = await connect();
// create a stream with a random name with some messages and a consumer
const { stream, consumer } = await setupStreamAndConsumer(nc);
// retrieve an existing consumer
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
// the consumer is wrapped in loop because this way, if there's some failure
// it will re-setup consume, and carry on.
// this is the basic pattern for processing messages forever
while (true) {
console.log("waiting for messages");
const messages = await c.consume();
try {
for await (const m of messages) {
console.log(m.seq);
m.ack();
}
} catch (err) {
console.log(`consume failed: ${err.message}`);
}
}
--- jetstream/examples/05_consume.js ---
/*
* Copyright 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 } from "@nats-io/transport-deno";
import { jetstream } from "@nats-io/jetstream";
import { setupStreamAndConsumer } from "./util.js";
// create a connection
const nc = await connect();
// create a stream with a random name with some messages and a consumer
const { stream, consumer } = await setupStreamAndConsumer(nc);
// retrieve an existing consumer
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
// we can consume using callbacks too
console.log("waiting for messages");
await c.consume({
callback: (m) => {
console.log(m.seq);
m.ack();
},
});
--- jetstream/examples/06_heartbeats.js ---
/*
* Copyright 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 } from "@nats-io/transport-deno";
import { jetstream } from "@nats-io/jetstream";
import { setupStreamAndConsumer } from "./util.js";
// create a connection
const nc = await connect({ debug: true });
// create a stream with a random name with some messages and a consumer
const { stream, consumer } = await setupStreamAndConsumer(nc);
// retrieve an existing consumer
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
while (true) {
const messages = await c.consume({
max_messages: 1,
});
// watch the to see if the consume operation misses heartbeats
(async () => {
for await (const s of messages.status()) {
switch (s.type) {
case "heartbeats_missed":
console.log(`${s.count} heartbeats missed`);
if (s.count === 2) {
messages.stop();
}
}
}
})();
for await (const m of messages) {
console.log(`${m.seq} ${m?.subject}`);
m.ack();
}
}
--- jetstream/examples/07_consume_jobs.js ---
/*
* Copyright 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, delay } from "@nats-io/transport-deno";
import { SimpleMutex } from "@nats-io/nats-core/internal";
import { jetstream } from "@nats-io/jetstream";
import { setupStreamAndConsumer } from "./util.js";
// create a connection
const nc = await connect();
// make a stream and fill with messages, and create a consumer
// create a stream with a random name with some messages and a consumer
const { stream, consumer } = await setupStreamAndConsumer(nc, 100);
// retrieve an existing consumer
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
const messages = await c.consume({ max_messages: 10 });
// this example controls parallel processing of the messages
// by only allowing 5 concurrent messages to be processed
// and then only allowing additional processing as others complete
const rl = new SimpleMutex(5);
async function schedule(m) {
// pretend to do work
await delay(1000);
m.ack();
console.log(`${m.seq}`);
}
for await (const m of messages) {
await rl.lock();
schedule(m)
.catch((err) => {
console.log(`failed processing: ${err.message}`);
m.nak();
})
.finally(() => {
rl.unlock();
});
}
--- jetstream/examples/08_consume_process.js ---
/*
* Copyright 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 } from "@nats-io/transport-deno";
import { setupStreamAndConsumer } from "./util.js";
import { jetstream } from "@nats-io/jetstream";
// create a connection
const nc = await connect();
// create a stream with a random name with some messages and a consumer
const { stream, consumer } = await setupStreamAndConsumer(nc, 10000);
// retrieve an existing consumer
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
// this example uses a consume that processes the stream
// creating a frequency table based on the subjects found
const messages = await c.consume();
const data = new Map();
for await (const m of messages) {
const chunks = m.subject.split(".");
const v = data.get(chunks[1]) || 0;
data.set(chunks[1], v + 1);
m.ack();
// if no pending, then we have processed the stream
// and we can break
if (m.info.pending === 0) {
break;
}
}
// we can safely delete the consumer
await c.delete();
const keys = [];
for (const k of data.keys()) {
keys.push(k);
}
keys.sort();
keys.forEach((k) => {
console.log(`${k}: ${data.get(k)}`);
});
await nc.close();
--- jetstream/examples/09_replay.js ---
/*
* Copyright 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.
*/
// This example demonstrates replaying JetStream messages.
//
// To run this example:
// 1. Start a NATS server with JetStream enabled: `nats-server -js`
// 2. Run with: `deno run --allow-net examples/09_replay.js`
//
// The example will:
// 1. Create a stream and publish 5 messages
// 2. Replay all messages 5 times, waiting 3 seconds between each replay
import { connect } from "@nats-io/transport-deno";
import { jetstream, jetstreamManager, DeliverPolicy } from "@nats-io/jetstream";
import { delay } from "@nats-io/nats-core";
import { setupStreamAndConsumer } from "./util.js";
// create a connection
const nc = await connect();
// create a stream with 5 messages
const { stream } = await setupStreamAndConsumer(nc, 5);
const jsm = await jetstreamManager(nc);
const js = jetstream(nc);
console.log("Stream created with 5 messages");
// replay the messages 5 times
for (let replay = 1; replay <= 5; replay++) {
console.log(`\n=== REPLAY ${replay} ===`);
// create a consumer that starts from the beginning (replay all)
const consumerName = `replay_consumer_${replay}`;
await jsm.consumers.add(stream, {
name: consumerName,
ack_policy: "explicit",
deliver_policy: DeliverPolicy.All, // replay all messages
});
// get the consumer and consume all messages
const consumer = await js.consumers.get(stream, consumerName);
const messages = await consumer.consume();
console.log("Replaying messages:");
for await (const m of messages) {
console.log(` [${m.seq}] ${m.subject}: ${m.string()}`);
m.ack();
// break when we've processed all messages
if (m.info.pending === 0) {
break;
}
}
// delete the consumer
await consumer.delete();
console.log(`Replay ${replay} completed`);
// wait 3 seconds before next replay
if (replay < 5) {
console.log("Waiting 3 seconds...");
await delay(3000);
}
}
console.log("\nAll replays completed!");
await nc.close();
--- jetstream/examples/js_readme_publish_examples.js ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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, Empty } from "@nats-io/transport-deno";
import { jetstream, jetstreamManager } from "@nats-io/jetstream";
const nc = await connect();
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "a", subjects: ["a.*"] });
// create a jetstream client:
const js = jetstream(nc);
// publish a message received by a stream
let pa = await js.publish("a.b");
// the jetstream returns an acknowledgement with the
// stream that captured the message, it's assigned sequence
// and whether the message is a duplicate.
console.log(
`stored in ${pa.stream} with sequence ${pa.seq} and is a duplicate? ${pa.duplicate}`,
);
// More interesting is the ability to prevent duplicates
// on messages that are stored in the server. If
// you assign a message ID, the server will keep looking
// for the same ID for a configured amount of time (within a
// configurable time window), and reject messages that
// have the same ID:
await js.publish("a.b", Empty, { msgID: "a" });
// you can also specify constraints that should be satisfied.
// For example, you can request the message to have as its
// last sequence before accepting the new message:
await js.publish("a.b", Empty, { expect: { lastMsgID: "a" } });
await js.publish("a.b", Empty, { expect: { lastSequence: 3 } });
// save the last sequence for this publish
pa = await js.publish("a.b", Empty, { expect: { streamName: "a" } });
// you can also mix the above combinations
// now if you have a stream with different subjects, you can also
// assert that the last recorded sequence on subject on the stream matches
const buf = [];
for (let i = 0; i < 100; i++) {
buf.push(js.publish("a.a", Empty));
}
await Promise.all(buf);
// if additional "a.b" has been recorded, this will fail
await js.publish("a.b", Empty, { expect: { lastSubjectSequence: pa.seq } });
await nc.drain();
--- jetstream/examples/jsm_readme_jsm_example.js ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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, Empty } from "@nats-io/transport-deno";
import { AckPolicy, jetstreamManager } from "@nats-io/jetstream";
const nc = await connect();
const jsm = await jetstreamManager(nc);
// list all the streams, the `next()` function
// retrieves a paged result.
const streams = await jsm.streams.list().next();
streams.forEach((si) => {
console.log(si);
});
// add a stream - jetstream can capture nats core messages
const subj = `mystream.*`;
await jsm.streams.add({ name: "mystream", subjects: [subj] });
// publish a reg nats message directly to the stream
for (let i = 0; i < 10; i++) {
nc.publish(`mystream.a`, Empty);
}
// find a stream that stores a specific subject:
const name = await jsm.streams.find("mystream.A");
// retrieve info about the stream by its name
const si = await jsm.streams.info(name);
// update a stream configuration
si.config.subjects?.push("a.b");
await jsm.streams.update(name, si.config);
// get a particular stored message in the stream by sequence
// this is not associated with a consumer
const sm = await jsm.streams.getMessage("mystream", { seq: 1 });
console.log(sm?.seq);
// delete the 5th message in the stream, securely erasing it
await jsm.streams.deleteMessage("mystream", 5);
// purge all messages in the stream, the stream itself
// remains.
await jsm.streams.purge("mystream");
// purge all messages with a specific subject (filter can be a wildcard)
await jsm.streams.purge("mystream", { filter: "a.b" });
// purge messages with a specific subject keeping some messages
await jsm.streams.purge("mystream", { filter: "a.c", keep: 5 });
// purge all messages with upto (not including seq)
await jsm.streams.purge("mystream", { seq: 100 });
// purge all messages with upto sequence that have a matching subject
await jsm.streams.purge("mystream", { filter: "a.d", seq: 100 });
// list all consumers for a stream:
const consumers = await jsm.consumers.list("mystream").next();
consumers.forEach((ci) => {
console.log(ci);
});
// add a new durable pull consumer
await jsm.consumers.add("mystream", {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
});
// retrieve a consumer's configuration
const ci = await jsm.consumers.info("mystream", "me");
console.log(ci);
// delete a particular consumer
await jsm.consumers.delete("mystream", "me");
// delete the stream
await jsm.streams.delete("mystream");
await nc.close();
--- jetstream/examples/util.js ---
/*
* Copyright 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 { nuid } from "@nats-io/nats-core";
import { AckPolicy, jetstreamManager } from "@nats-io/jetstream";
export async function setupStreamAndConsumer(
nc,
messages = 100,
) {
const stream = nuid.next();
const jsm = await jetstreamManager(nc);
const js = jsm.jetstream();
await jsm.streams.add({ name: stream, subjects: [`${stream}.*`] });
const buf = [];
for (let i = 0; i < messages; i++) {
buf.push(js.publish(`${stream}.${i}`, `${i}`));
if (buf.length % 500 === 0) {
await Promise.all(buf);
buf.length = 0;
}
}
if (buf.length > 0) {
await Promise.all(buf);
buf.length = 0;
}
const consumer = await jsm.consumers.add(stream, {
name: stream,
ack_policy: AckPolicy.Explicit,
});
return { stream, consumer };
}
--- jetstream/tests/batch_publisher_test.ts ---
/*
* Copyright 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 {
cleanup,
jetstreamServerConf,
notCompatible,
setup,
} from "test_helpers";
import { type BatchPublisherImpl, jetstreamManager } from "../src/jsclient.ts";
import { assertEquals, assertRejects } from "@std/assert";
Deno.test("batch publisher - basics", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "batch",
allow_atomic: true,
subjects: ["q"],
});
const js = jsm.jetstream();
const bp = await js.startBatch("q", "a");
for (let i = 0; i < 98; i++) {
bp.add("q", i.toString(), { ack: i % 20 === 0 });
}
const ack = await bp.commit("q", "lastone");
assertEquals(ack.seq, 100);
assertEquals(ack.stream, "batch");
assertEquals(ack.count, 100);
assertEquals(ack.batch, bp.id);
await cleanup(ns, nc);
});
Deno.test("batch publisher - out of sequence", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "batch",
allow_atomic: true,
subjects: ["q"],
});
const js = jsm.jetstream();
const bp = await js.startBatch("q", "a");
const bpi = bp as BatchPublisherImpl;
bpi.count++;
await assertRejects(
() => {
return bp.commit("q", "c");
},
Error,
"batch didn't contain number of published messages",
);
await cleanup(ns, nc);
});
Deno.test("batch publisher - two streams", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "a",
allow_atomic: true,
subjects: ["a"],
});
await jsm.streams.add({
name: "b",
allow_atomic: true,
subjects: ["b"],
});
const js = jsm.jetstream();
const bp = await js.startBatch("a", "a");
await assertRejects(
() => {
return bp.add("b", "b", { ack: true });
},
Error,
"atomic publish batch is incomplete",
);
await assertRejects(
() => {
return bp.commit("a", "a");
},
Error,
"batch publisher is done",
);
const si = await jsm.streams.info("b");
assertEquals(si.state.messages, 0);
await cleanup(ns, nc);
});
Deno.test("batch publisher - expect last seq", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "a",
allow_atomic: true,
subjects: ["a"],
});
const js = jsm.jetstream();
// this should have failed...
const b = await js.startBatch("a", "a", {
expect: {
lastSequence: 5,
},
});
// this fails but the message is doggy
await assertRejects(
() => {
return b.commit("a", "");
},
Error,
"batch didn't contain number of published messages",
);
await cleanup(ns, nc);
});
Deno.test("batch publisher - no atomics", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "a",
subjects: ["a"],
});
const js = jsm.jetstream();
await assertRejects(
() => {
return js.startBatch("a", "a");
},
Error,
"atomic publish is disabled",
);
await cleanup(ns, nc);
});
--- jetstream/tests/consume_test.ts ---
/*
* Copyright 2022-2024 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 { cleanup, jetstreamServerConf, setup } from "test_helpers";
import {
assert,
assertEquals,
assertExists,
assertFalse,
assertRejects,
} from "@std/assert";
import { initStream, setupStreamAndConsumer } from "./jstest_util.ts";
import {
deadline,
deferred,
delay,
errors,
nanos,
type NatsConnectionImpl,
syncIterator,
} from "@nats-io/nats-core/internal";
import type { PullConsumerMessagesImpl } from "../src/consumer.ts";
import {
AckPolicy,
DeliverPolicy,
isPullConsumer,
isPushConsumer,
jetstream,
jetstreamManager,
} from "../src/mod.ts";
import type { PushConsumerMessagesImpl } from "../src/pushconsumer.ts";
import type { ConsumerNotification, HeartbeatsMissed } from "../src/types.ts";
Deno.test("consumers - consume", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const count = 1000;
const { stream, consumer } = await setupStreamAndConsumer(nc, count);
const js = jetstream(nc, { timeout: 30_000 });
const c = await js.consumers.get(stream, consumer);
assert(isPullConsumer(c));
assertFalse(isPushConsumer(c));
const ci = await c.info();
assertEquals(ci.num_pending, count);
const start = Date.now();
const iter = await c.consume({ expires: 2_000, max_messages: 10 });
for await (const m of iter) {
m.ack();
if (m.info.pending === 0) {
const millis = Date.now() - start;
console.log(
`consumer: ${millis}ms - ${count / (millis / 1000)} msgs/sec`,
);
break;
}
}
assertEquals(iter.getReceived(), count);
assertEquals(iter.getProcessed(), count);
assertEquals((await c.info()).num_pending, 0);
await cleanup(ns, nc);
});
Deno.test("consumers - consume callback rejects iter", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream, consumer } = await setupStreamAndConsumer(nc, 0);
const js = jetstream(nc);
const c = await js.consumers.get(stream, consumer);
const iter = await c.consume({
expires: 5_000,
max_messages: 10_000,
callback: (m) => {
m.ack();
},
});
await assertRejects(
async () => {
for await (const _o of iter) {
// should fail
}
},
errors.InvalidOperationError,
"iterator cannot be used when a callback is registered",
);
iter.stop();
await cleanup(ns, nc);
});
Deno.test("consume - 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.consume({
max_messages: 100,
idle_heartbeat: 1000,
expires: 30000,
}) as PushConsumerMessagesImpl;
// make heartbeats trigger
(nc as NatsConnectionImpl)._resub(iter.sub, "foo");
const d = deferred<ConsumerNotification>();
await (async () => {
const status = iter.status();
for await (const s of status) {
d.resolve(s);
iter.stop();
break;
}
})();
await (async () => {
for await (const _r of iter) {
// nothing
}
})();
const cs = await d;
assertEquals(cs.type, "heartbeats_missed");
assertEquals((cs as HeartbeatsMissed).count, 2);
await cleanup(ns, nc);
});
Deno.test("consume - deleted consumer", 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.consume({
expires: 3000,
});
const deleted = deferred();
let notFound = 0;
const done = deferred<number>();
(async () => {
const status = iter.status();
for await (const s of status) {
if (s.type === "consumer_deleted") {
deleted.resolve();
}
if (s.type === "consumer_not_found") {
notFound++;
if (notFound > 1) {
done.resolve();
}
}
}
})().then();
(async () => {
for await (const _m of iter) {
// nothing
}
})().then();
setTimeout(() => {
jsm.consumers.delete(stream, "a");
}, 1000);
await deleted;
await done;
await iter.close();
await cleanup(ns, nc);
});
Deno.test("consume - sub leaks", 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.consume({ expires: 30000 });
const done = (async () => {
for await (const _m of iter) {
// nothing
}
})().then();
setTimeout(() => {
iter.close();
}, 1000);
await done;
//@ts-ignore: test
assertEquals(nc.protocol.subscriptions.size(), 1);
await cleanup(ns, nc);
});
Deno.test("consume - drain", 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
const js = jetstream(nc);
const c = await js.consumers.get(stream, stream);
const iter = await c.consume({ expires: 30000 });
setTimeout(() => {
nc.drain();
}, 100);
const done = (async () => {
for await (const _m of iter) {
// nothing
}
})().then();
await deadline(done, 1000);
await cleanup(ns, nc);
});
Deno.test("consume - sync", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
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() as PullConsumerMessagesImpl;
const sync = syncIterator(iter);
assertExists(await sync.next());
assertExists(await sync.next());
iter.stop();
assertEquals(await sync.next(), null);
await cleanup(ns, nc);
});
Deno.test("consume - stream not found request abort", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
const c = await js.consumers.get("A", "a");
const iter = await c.consume({
expires: 3000,
abort_on_missing_resource: true,
});
await jsm.streams.delete("A");
await assertRejects(
async () => {
for await (const _ of iter) {
// nothing
}
},
Error,
"stream not found",
);
await cleanup(ns, nc);
});
Deno.test("consume - consumer deleted request abort", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
const c = await js.consumers.get("A", "a");
const iter = await c.consume({
expires: 3000,
abort_on_missing_resource: true,
});
const done = assertRejects(
async () => {
for await (const _ of iter) {
// nothing
}
},
Error,
"consumer deleted",
);
await delay(1000);
await c.delete();
await done;
await cleanup(ns, nc);
});
Deno.test("consume - consumer not found request abort", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
const c = await js.consumers.get("A", "a");
await c.delete();
const iter = await c.consume({
expires: 3000,
abort_on_missing_resource: true,
});
await assertRejects(
async () => {
for await (const _ of iter) {
// nothing
}
},
Error,
"consumer not found",
);
await cleanup(ns, nc);
});
Deno.test("consume - consumer bind", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
const c = await js.consumers.get("A", "a");
await c.delete();
const cisub = nc.subscribe("$JS.API.CONSUMER.INFO.A.a", {
callback: () => {},
});
const iter = await c.consume({
expires: 1000,
bind: true,
});
let hbm = 0;
let cnf = 0;
(async () => {
for await (const s of iter.status()) {
switch (s.type) {
case "heartbeats_missed":
hbm++;
if (hbm > 5) {
iter.stop();
}
break;
case "consumer_not_found":
cnf++;
break;
}
}
})().then();
const done = (async () => {
for await (const _ of iter) {
// nothing
}
})();
await done;
assert(hbm > 1);
assertEquals(cnf, 0);
assertEquals(cisub.getProcessed(), 0);
await cleanup(ns, nc);
});
Deno.test("consume - connection close exits", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
await js.publish("a");
const c = await js.consumers.get("A", "a");
const iter = await c.consume({
expires: 2000,
max_messages: 10,
callback: (m) => {
m.ack();
},
}) as PullConsumerMessagesImpl;
await nc.close();
await deadline(iter.closed(), 1000);
await cleanup(ns, nc);
});
Deno.test("consume - one pending is none", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jsm.jetstream();
const buf = [];
for (let i = 0; i < 100; i++) {
buf.push(js.publish("a", `${i}`));
}
await Promise.all(buf);
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const c = await js.consumers.get("A", "a");
const iter = await c.consume({ bind: true, max_messages: 1 });
for await (const m of iter) {
assertEquals(iter.getPending(), 0);
assertEquals(iter.getReceived(), m.seq);
m.ack();
if (m.info.pending === 0) {
break;
}
}
await cleanup(ns, nc);
});
Deno.test("consume - stream not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
const c = await jsm.jetstream().consumers.get("messages", "c");
await jsm.streams.delete("messages");
const iter = await c.consume({ expires: 5_000 });
const hbmP = deferred();
const buf: ConsumerNotification[] = [];
(async () => {
for await (const s of await iter.status()) {
console.log(s);
buf.push(s);
if (s.type === "heartbeats_missed" && s.count === 3) {
hbmP.resolve();
}
}
})().then();
const nextP = deferred();
(async () => {
for await (const _ of iter) {
nextP.resolve();
}
})().then();
await hbmP;
const snfs = buf.filter((s) => s.type === "stream_not_found").length;
const nrs = buf.filter((s) => s.type === "no_responders").length;
assert(snfs > 0);
assert(nrs > 0);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.jetstream().publish("m");
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
await nextP;
await cleanup(ns, nc);
});
Deno.test("consume - consumer not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
const c = await jsm.jetstream().consumers.get("messages", "c");
await jsm.consumers.delete("messages", "c");
const iter = await c.consume({ expires: 5_000 });
const hbmP = deferred();
const buf: ConsumerNotification[] = [];
(async () => {
for await (const s of await iter.status()) {
console.log(s);
buf.push(s);
if (s.type === "heartbeats_missed" && s.count === 3) {
hbmP.resolve();
}
}
})().then();
const nextP = deferred();
(async () => {
for await (const _ of iter) {
nextP.resolve();
}
})().then();
await hbmP;
const snfs = buf.filter((s) => s.type === "consumer_not_found").length;
const nrs = buf.filter((s) => s.type === "no_responders").length;
assert(snfs > 0);
assert(nrs > 0);
await jsm.jetstream().publish("m");
const ci = await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
console.log(ci);
await nextP;
await cleanup(ns, nc);
});
Deno.test("consumer - internal close listener", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const nci = nc as NatsConnectionImpl;
//@ts-ignore: internal
assertEquals(nci.closeListeners, undefined);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "leak", subjects: ["leak.*"] });
const ci = await jsm.consumers.add("leak", {});
const js = jsm.jetstream();
await js.publish("leak.a");
await js.publish("leak.a");
await js.publish("leak.a");
// next
const c = await js.consumers.get("leak", ci.name);
await c.next();
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 0);
// fetch
let iter = await c.fetch({ max_messages: 1 });
await (async () => {
for await (const _ of iter) {
// nothing
}
})();
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 0);
iter = await c.consume({ max_messages: 1 });
await (async () => {
for await (const _ of iter) {
break;
}
})();
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 0);
// long
iter = await c.consume({ max_messages: 1 });
let done = (async () => {
for await (const _ of iter) {
// nothing
}
})();
await nc.flush();
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 1);
iter.stop();
await done;
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 0);
iter = await c.consume({ max_messages: 1 });
done = (async () => {
for await (const _ of iter) {
// nothing
}
})();
// @ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 1);
await nc.close();
await done;
// @ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 0);
await cleanup(ns, nc);
});
Deno.test("consume - max_waiting throttles", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jsm.jetstream();
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
max_waiting: 1,
});
const c = await js.consumers.get("A", "a");
// @ts-ignore: test
// nc.options.debug = true;
const p = c.next({ expires: 10_000 });
await delay(500);
let hb = 0;
let nexts = 0;
const cm = await c.consume({ expires: 5_000 });
const done = (async () => {
for await (const s of cm.status()) {
if (
s.type === "heartbeats_missed"
) {
hb++;
}
if (s.type === "next") {
nexts++;
if (hb > 0) {
break;
}
}
}
})().then();
(async () => {
for await (const _ of cm) {
// do nothing
}
})().then();
await Promise.all([p, done]);
await cleanup(ns, nc);
});
Deno.test("consume - max_bytes throttles", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jsm.jetstream();
await js.publish("a", new Uint8Array(32));
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const c = await js.consumers.get("A", "a");
await delay(500);
let next = 0;
let hbs = 0;
const cm = await c.consume({ expires: 5_000, max_bytes: 8 });
const done = (async () => {
for await (const s of cm.status()) {
switch (s.type) {
case "next":
next++;
// if we got heartbeats, we emitted next to recover
if (hbs > 0) {
return;
}
break;
case "heartbeats_missed":
hbs++;
break;
}
}
})().then();
(async () => {
for await (const _ of cm) {
// do nothing
}
})().then();
await done;
await cleanup(ns, nc);
});
--- jetstream/tests/consumers_ordered_test.ts ---
/*
* Copyright 2023-2024 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 {
assert,
assertEquals,
assertExists,
assertRejects,
assertStringIncludes,
} from "@std/assert";
import {
type ConsumerNotification,
DeliverPolicy,
jetstream,
jetstreamManager,
} from "../src/mod.ts";
import type { ConsumerMessages, JsMsg } from "../src/mod.ts";
import {
cleanup,
jetstreamServerConf,
notCompatible,
setup,
} from "test_helpers";
import { deadline, deferred, delay } from "@nats-io/nats-core";
import type {
PullConsumerImpl,
PullConsumerMessagesImpl,
} from "../src/consumer.ts";
import { delayUntilAssetNotFound } from "./util.ts";
import { flakyTest } from "../../test_helpers/mod.ts";
Deno.test("ordered consumers - get", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
await assertRejects(
async () => {
await js.consumers.get("a");
},
Error,
"stream not found",
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test"] });
await js.publish("test");
const oc = await js.consumers.get("test") as PullConsumerImpl;
assertExists(oc);
const ci = await oc.info();
assertEquals(ci.name, `${oc.opts.name_prefix}_${oc.serial}`);
assertEquals(ci.num_pending, 1);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - fetch", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
await js.publish("test.a");
await js.publish("test.b");
await js.publish("test.c");
const oc = await js.consumers.get("test") as PullConsumerImpl;
assertExists(oc);
let iter = await oc.fetch({ max_messages: 1 });
for await (const m of iter) {
assertEquals(m.subject, "test.a");
assertEquals(m.seq, 1);
}
iter = await oc.fetch({ max_messages: 1 });
for await (const m of iter) {
assertEquals(m.subject, "test.b");
assertEquals(m.seq, 2);
}
await cleanup(ns, nc);
});
Deno.test("ordered consumers - consume reset", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
await js.publish("test.a");
await js.publish("test.b");
await js.publish("test.c");
const oc = await js.consumers.get("test") as PullConsumerImpl;
assertExists(oc);
const seen: number[] = new Array(3).fill(0);
const done = deferred();
const callback = (r: JsMsg) => {
const idx = r.seq - 1;
seen[idx]++;
// mess with the internals so we see these again
if (seen[idx] === 1) {
const state = oc.orderedConsumerState!;
const cursor = state.cursor;
cursor.deliver_seq--;
cursor.stream_seq--;
}
if (r.info.pending === 0) {
iter.stop();
done.resolve();
}
};
const iter = await oc.consume({
max_messages: 1,
callback,
});
await done;
assertEquals(seen, [2, 2, 1]);
assertEquals(oc.serial, 3);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - consume", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
await js.publish("test.a");
await js.publish("test.b");
await js.publish("test.c");
const oc = await js.consumers.get("test") as PullConsumerImpl;
assertExists(oc);
const iter = await oc.consume({ max_messages: 1 });
for await (const m of iter) {
if (m.info.pending === 0) {
break;
}
}
await cleanup(ns, nc);
});
Deno.test("ordered consumers - filters consume", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await js.publish("test.a");
await js.publish("test.b");
await js.publish("test.c");
const oc = await js.consumers.get("test", { filter_subjects: ["test.b"] });
assertExists(oc);
const iter = await oc.consume();
for await (const m of iter) {
assertEquals("test.b", m.subject);
if (m.info.pending === 0) {
break;
}
}
assertEquals(iter.getProcessed(), 1);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - filters fetch", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await js.publish("test.a");
await js.publish("test.b");
await js.publish("test.c");
const oc = await js.consumers.get("test", { filter_subjects: ["test.b"] });
assertExists(oc);
const iter = await oc.fetch({ expires: 1000 });
for await (const m of iter) {
assertEquals("test.b", m.subject);
}
assertEquals(iter.getProcessed(), 1);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - fetch reject consumer type change or concurrency", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
const oc = await js.consumers.get("test");
const iter = await oc.fetch();
(async () => {
for await (const _r of iter) {
// nothing
}
})().then();
await assertRejects(
() => {
return oc.fetch();
},
Error,
"ordered consumer doesn't support concurrent fetch",
);
await assertRejects(
() => {
return oc.consume();
},
Error,
"ordered consumer initialized as fetch",
);
await iter.stop();
await cleanup(ns, nc);
});
Deno.test("ordered consumers - consume reject consumer type change or concurrency", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
const oc = await js.consumers.get("test");
const iter = await oc.consume({ expires: 3000 });
(async () => {
for await (const _r of iter) {
// nothing
}
})().then();
await assertRejects(
async () => {
await oc.consume();
},
Error,
"ordered consumer doesn't support concurrent consume",
);
await assertRejects(
async () => {
await oc.fetch();
},
Error,
"ordered consumer already initialized as consume",
);
await iter.stop();
await cleanup(ns, nc);
});
Deno.test("ordered consumers - last per subject", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.a"),
]);
let oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.LastPerSubject,
});
let iter = await oc.fetch({ max_messages: 1 });
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 2);
}
})();
oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.LastPerSubject,
});
iter = await oc.consume({ max_messages: 1 });
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 2);
break;
}
})();
await cleanup(ns, nc);
});
Deno.test("ordered consumers - start sequence", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.b"),
]);
let oc = await js.consumers.get("test", {
opt_start_seq: 2,
});
let iter = await oc.fetch({ max_messages: 1 });
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 2);
}
})();
oc = await js.consumers.get("test", {
opt_start_seq: 2,
});
iter = await oc.consume({ max_messages: 1 });
await (async () => {
for await (const r of iter) {
assertEquals(r.info.streamSequence, 2);
assertEquals(r.subject, "test.b");
break;
}
})();
await cleanup(ns, nc);
});
Deno.test("ordered consumers - last", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.b"),
]);
let oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.Last,
});
let iter = await oc.fetch({ max_messages: 1 });
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 2);
assertEquals(m.subject, "test.b");
}
})();
oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.Last,
});
iter = await oc.consume({ max_messages: 1 });
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 2);
assertEquals(m.subject, "test.b");
break;
}
})();
await cleanup(ns, nc);
});
Deno.test("ordered consumers - new", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.b"),
]);
let oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.New,
});
let iter = await oc.fetch({ max_messages: 1 });
await (async () => {
await js.publish("test.c");
for await (const m of iter) {
assertEquals(m.info.streamSequence, 3);
assertEquals(m.subject, "test.c");
}
})();
oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.New,
});
iter = await oc.consume({ max_messages: 1 });
await (async () => {
await js.publish("test.d");
for await (const m of iter) {
assertEquals(m.info.streamSequence, 4);
assertEquals(m.subject, "test.d");
break;
}
})();
await cleanup(ns, nc);
});
Deno.test("ordered consumers - start time", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.b"),
]);
await delay(500);
const date = new Date().toISOString();
let oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.StartTime,
opt_start_time: date,
});
await js.publish("test.c");
let iter = await oc.fetch({ max_messages: 1 });
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 3);
assertEquals(m.subject, "test.c");
}
})();
oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.StartTime,
opt_start_time: date,
});
iter = await oc.consume({ max_messages: 1 });
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 3);
assertEquals(m.subject, "test.c");
break;
}
})();
await cleanup(ns, nc);
});
Deno.test("ordered consumers - start time reset", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.b"),
]);
await delay(500);
const date = new Date().toISOString();
const oc = await js.consumers.get("test", {
deliver_policy: DeliverPolicy.StartTime,
opt_start_time: date,
}) as PullConsumerImpl;
await js.publish("test.c");
const iter = await oc.fetch({ max_messages: 1 }) as PullConsumerMessagesImpl;
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 3);
assertEquals(m.subject, "test.c");
// now that we are here
const opts = oc.getConsumerOpts();
assertEquals(opts.opt_start_seq, 4);
assertEquals(opts.deliver_policy, DeliverPolicy.StartSequence);
}
})();
await cleanup(ns, nc);
});
Deno.test("ordered consumers - next", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test"] });
const js = jetstream(nc);
const c = await js.consumers.get("test");
let m = await c.next({ expires: 1000 });
assertEquals(m, null);
await Promise.all([
js.publish("test", "hello"),
js.publish("test", "goodbye"),
]);
await nc.flush();
m = await c.next({ expires: 1000 });
assertEquals(m?.seq, 1);
m = await c.next({ expires: 1000 });
assertEquals(m?.seq, 2);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - sub leaks next()", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream } = await initStream(nc);
//@ts-ignore: test
assertEquals(nc.protocol.subscriptions.size(), 1);
const js = jetstream(nc);
const c = await js.consumers.get(stream);
await c.next({ expires: 1000 });
//@ts-ignore: test
assertEquals(nc.protocol.subscriptions.size(), 1);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - sub leaks fetch()", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream } = await initStream(nc);
//@ts-ignore: test
assertEquals(nc.protocol.subscriptions.size(), 1);
const js = jetstream(nc);
const c = await js.consumers.get(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("ordered consumers - sub leaks consume()", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream } = await initStream(nc);
//@ts-ignore: test
assertEquals(nc.protocol.subscriptions.size(), 1);
const js = jetstream(nc);
const c = await js.consumers.get(stream);
const iter = await c.consume({ expires: 30000 });
const done = (async () => {
for await (const _m of iter) {
// nothing
}
})().then();
setTimeout(() => {
iter.close();
}, 1000);
await done;
//@ts-ignore: test
assertEquals(nc.protocol.subscriptions.size(), 1);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - consume drain", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream } = await initStream(nc);
const js = jetstream(nc);
const c = await js.consumers.get(stream);
const iter = await c.consume({ expires: 30000 });
setTimeout(() => {
nc.drain();
}, 100);
const done = (async () => {
for await (const _m of iter) {
// nothing
}
})().then();
await deadline(done, 1000);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - headers only", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const oc = await js.consumers.get("test", { headers_only: true });
const ci = await oc.info();
assertExists(ci);
assertEquals(ci.config.headers_only, true);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - max deliver", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const oc = await js.consumers.get("test");
const ci = await oc.info();
assertExists(ci);
assertEquals(ci.config.max_deliver, 1);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - mem", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const oc = await js.consumers.get("test");
const ci = await oc.info();
assertExists(ci);
assertEquals(ci.config.mem_storage, true);
await cleanup(ns, nc);
});
Deno.test("ordered 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);
const consumer = await js.consumers.get("messages");
const iter = await consumer.consume() as PullConsumerMessagesImpl;
const done = (async () => {
for await (const _m of iter) {
// nothing
}
})().catch();
assertStringIncludes(iter.sub.getSubject(), "x.");
iter.stop();
await done;
await cleanup(ns, nc);
});
Deno.test("ordered consumers - fetch deleted consumer", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
const c = await js.consumers.get("A");
const iter = await c.fetch({
expires: 3000,
});
const exited = assertRejects(
async () => {
for await (const _ of iter) {
// nothing
}
},
Error,
"consumer deleted",
);
await delay(1000);
await c.delete();
await exited;
await cleanup(ns, nc);
});
Deno.test("ordered consumers - next deleted consumer", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["hello"] });
const js = jetstream(nc);
const c = await js.consumers.get("A");
const exited = assertRejects(
() => {
return c.next({ expires: 4000 });
},
Error,
"consumer deleted",
);
await delay(1000);
await c.delete();
await exited;
await cleanup(ns, nc);
});
Deno.test("ordered consumers - consume stream not found request abort", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
const c = await js.consumers.get("A");
await jsm.streams.delete("A");
const iter = await c.consume({
expires: 3000,
abort_on_missing_resource: true,
});
await assertRejects(
async () => {
for await (const _ of iter) {
// ignore
}
},
Error,
"stream not found",
);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - consume consumer deleted request abort", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
const c = await js.consumers.get("A");
const iter = await c.consume({
expires: 3000,
abort_on_missing_resource: true,
});
const done = assertRejects(
async () => {
for await (const _ of iter) {
// nothing
}
},
Error,
"consumer deleted",
);
await delay(1000);
await c.delete();
await done;
await cleanup(ns, nc);
});
Deno.test("ordered consumers - bind is rejected", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
const c = await js.consumers.get("A");
await assertRejects(
() => {
return c.next({ bind: true });
},
Error,
"'bind' is not supported",
);
await assertRejects(
() => {
return c.fetch({ bind: true });
},
Error,
"'bind' is not supported",
);
await assertRejects(
() => {
return c.consume({ bind: true });
},
Error,
"'bind' is not supported",
);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - name prefix", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
const c = await js.consumers.get("A", { name_prefix: "hello" });
const ci = await c.info(true);
assert(ci.name.startsWith("hello"));
await assertRejects(
() => {
return js.consumers.get("A", { name_prefix: "" });
},
Error,
"name_prefix name required",
);
await assertRejects(
() => {
return js.consumers.get("A", { name_prefix: "one.two" });
},
Error,
"name_prefix name ('one.two') cannot contain '.'",
);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - fetch reset", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
await js.publish("a", JSON.stringify(1));
const c = await js.consumers.get("A") as PullConsumerImpl;
let recreates = 0;
function countResets(iter: ConsumerMessages): Promise<void> {
return (async () => {
for await (const s of iter.status()) {
if (s.type === "ordered_consumer_recreated") {
recreates++;
}
}
})();
}
// after the first message others will get published
let iter = await c.fetch({ max_messages: 10, expires: 3_000 });
const first = countResets(iter);
const sequences = [];
for await (const m of iter) {
sequences.push(m.json());
}
const buf = [];
for (let i = 2; i < 12; i++) {
buf.push(js.publish("a", JSON.stringify(i)));
}
await Promise.all(buf);
iter = await c.fetch({ max_messages: 10, expires: 2_000 });
const second = countResets(iter);
const done = (async () => {
for await (const m of iter) {
sequences.push(m.json());
}
})().catch();
await Promise.all([first, second, done]);
assertEquals(c.serial, 1);
assertEquals(recreates, 0);
assertEquals(sequences, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - consume reset", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
await js.publish("a", JSON.stringify(1));
let recreates = 0;
function countRecreates(iter: ConsumerMessages): Promise<void> {
return (async () => {
for await (const s of iter.status()) {
if (s.type === "ordered_consumer_recreated") {
recreates++;
}
}
})();
}
const c = await js.consumers.get("A") as PullConsumerImpl;
// after the first message others will get published
const iter = await c.consume({ max_messages: 11, idle_heartbeat: 1_000 });
countRecreates(iter).catch();
const sequences = [];
for await (const m of iter) {
sequences.push(m.json());
// mess with the internal state to cause a reset
if (m.seq === 1) {
c.orderedConsumerState!.cursor.deliver_seq = 3;
const buf = [];
for (let i = 2; i < 20; i++) {
buf.push(js.publish("a", JSON.stringify(i)));
}
await Promise.all(buf);
}
if (m.seq === 11) {
break;
}
}
assertEquals(c.serial, 2);
assertEquals(recreates, 1);
assertEquals(sequences, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - next reset", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
await js.publish("a", JSON.stringify(1));
await js.publish("a", JSON.stringify(2));
const c = await js.consumers.get("A") as PullConsumerImpl;
// get the first
let m = await c.next({ expires: 1000 });
assertExists(m);
assertEquals(m.json(), 1);
// force a reset
c.orderedConsumerState!.cursor.deliver_seq = 100;
m = await c.next({ expires: 1000 });
assertExists(m);
assertEquals(m.json(), 2);
assertEquals(c.serial, 2);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - no reset on next", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
await js.publish("a", JSON.stringify(1));
await js.publish("a", JSON.stringify(2));
await nc.flush();
const c = await js.consumers.get("A") as PullConsumerImpl;
let m = await c.next();
assertExists(m);
m = await c.next();
assertExists(m);
assertEquals(c.serial, 1);
await cleanup(ns, nc);
});
Deno.test("ordered consumers - initial creation fails, consumer fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jsm.jetstream();
const c = await js.consumers.get("A");
await jsm.streams.delete("A");
const iter = await c.consume({
abort_on_missing_resource: true,
idle_heartbeat: 1000,
});
await assertRejects(
async () => {
for await (const _ of iter) {
// nothing
}
},
Error,
"stream not found",
);
await cleanup(ns, nc);
});
Deno.test(
"ordered consumers - stale reference recovers",
async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jsm.jetstream();
await Promise.all([
js.publish("a", JSON.stringify(1)),
js.publish("a", JSON.stringify(2)),
]);
const c = await js.consumers.get("A") as PullConsumerImpl;
let m = await c.next({ expires: 1000 });
assertExists(m);
assertEquals(m.json<number>(), 1);
assert(await c.delete());
// continue until the server says the consumer doesn't exist
await delayUntilAssetNotFound(c);
// so should get CnF once
await assertRejects(
() => {
return c.next({ expires: 1000 });
},
Error,
"no responders",
);
// but now it will be created in line
m = await c.next({ expires: 1000 });
assertExists(m);
assertEquals(m.json<number>(), 2);
await cleanup(ns, nc);
},
);
Deno.test(
"ordered consumers - consume stale reference recovers",
flakyTest(async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
await js.publish("a", JSON.stringify(1));
const c = await js.consumers.get("A") as PullConsumerImpl;
assert(await c.delete());
// continue until the server says the consumer doesn't exist
await delayUntilAssetNotFound(c);
const iter = await c.consume({ idle_heartbeat: 1_000, expires: 30_000 });
let recreates = 0;
function countRecreates(iter: ConsumerMessages): Promise<void> {
return (async () => {
for await (const s of iter.status()) {
if (s.type === "ordered_consumer_recreated") {
recreates++;
}
}
})();
}
const done = countRecreates(iter);
await (async () => {
for await (const m of iter) {
assertEquals(m.json<number>(), 1);
break;
}
})();
await done;
assertEquals(c.serial, 2);
await cleanup(ns, nc);
}),
);
Deno.test("ordered consumer - deliver all policy", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jetstream(nc);
await js.consumers.get("A", { deliver_policy: DeliverPolicy.All });
await cleanup(ns, nc);
});
Deno.test("ordered - consume stream not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
const c = await jsm.jetstream().consumers.get("messages");
await jsm.streams.delete("messages");
const iter = await c.consume({ expires: 5_000 });
const hbmP = deferred();
const buf: ConsumerNotification[] = [];
(async () => {
for await (const s of await iter.status()) {
console.log(s);
buf.push(s);
if (s.type === "heartbeats_missed" && s.count === 3) {
hbmP.resolve();
}
}
})().then();
const nextP = deferred();
(async () => {
for await (const _ of iter) {
nextP.resolve();
}
})().then();
await hbmP;
const snfs = buf.filter((s) => s.type === "stream_not_found").length;
const nrs = buf.filter((s) => s.type === "no_responders").length;
assert(snfs > 0);
assert(nrs > 0);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.jetstream().publish("m");
await nextP;
await cleanup(ns, nc);
});
Deno.test("ordered - fetch stream not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
const c = await jsm.jetstream().consumers.get("messages");
await jsm.streams.delete("messages");
await assertRejects(
async () => {
const iter = await c.fetch({ expires: 5_000 });
for await (const _ of iter) {
// will fail
}
},
Error,
"no responders",
);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.jetstream().publish("m");
const mP = deferred();
(async () => {
const iter = await c.fetch({ expires: 5_000 });
for await (const _ of iter) {
mP.resolve();
break;
}
})().then();
await mP;
await cleanup(ns, nc);
});
Deno.test("ordered - next stream not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
const c = await jsm.jetstream().consumers.get("messages");
await jsm.streams.delete("messages");
await assertRejects(
() => {
return c.next();
},
Error,
"no responders",
);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.jetstream().publish("m");
const m = await c.next();
assertExists(m);
await cleanup(ns, nc);
});
--- jetstream/tests/consumers_push_test.ts ---
import {
AckPolicy,
DeliverPolicy,
isPullConsumer,
isPushConsumer,
jetstream,
jetstreamManager,
} from "../src/mod.ts";
import { isBoundPushConsumerOptions } from "../src/types.ts";
import { cleanup, jetstreamServerConf, Lock, setup } from "test_helpers";
import { nanos } from "@nats-io/nats-core";
import { assert, assertEquals, assertExists, assertFalse } from "@std/assert";
import type { PushConsumerMessagesImpl } from "../src/pushconsumer.ts";
import { deadline, delay } from "@nats-io/nats-core/internal";
Deno.test("push consumers - basics", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["A.*"] });
await Promise.all([
js.publish("A.a"),
js.publish("A.b"),
js.publish("A.c"),
]);
const opts = {
durable_name: "B",
deliver_subject: "hello",
deliver_policy: DeliverPolicy.All,
idle_heartbeat: nanos(5000),
flow_control: true,
ack_policy: AckPolicy.Explicit,
};
assert(isBoundPushConsumerOptions(opts));
await jsm.consumers.add("A", opts);
const c = await js.consumers.getPushConsumer("A", "B");
assert(isPushConsumer(c));
assertFalse(isPullConsumer(c));
let info = await c.info(true);
assertEquals(info.config.deliver_group, undefined);
const iter = await c.consume();
await (async () => {
for await (const m of iter) {
m.ackAck().then(() => {
if (m.info.streamSequence === 3) {
iter.stop();
}
});
}
})();
await iter.closed();
info = await c.info();
assertEquals(info.num_pending, 0);
assertEquals(info.delivered.stream_seq, 3);
assertEquals(info.ack_floor.stream_seq, 3);
await cleanup(ns, nc);
});
Deno.test("push consumers - basics cb", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["A.*"] });
await Promise.all([
js.publish("A.a"),
js.publish("A.b"),
js.publish("A.c"),
]);
await jsm.consumers.add("A", {
durable_name: "B",
deliver_subject: "hello",
deliver_policy: DeliverPolicy.All,
idle_heartbeat: nanos(5000),
flow_control: true,
ack_policy: AckPolicy.Explicit,
});
const c = await js.consumers.getPushConsumer("A", "B");
let info = await c.info(true);
assertEquals(info.config.deliver_group, undefined);
const iter = await c.consume({
callback: (m) => {
m.ackAck()
.then(() => {
if (m.info.pending === 0) {
iter.stop();
}
});
},
});
await iter.closed();
assertEquals(iter.getProcessed(), 3);
info = await c.info();
assertEquals(info.num_pending, 0);
assertEquals(info.delivered.stream_seq, 3);
assertEquals(info.ack_floor.stream_seq, 3);
await cleanup(ns, nc);
});
Deno.test("push consumers - queue", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
jetstream: {
max_file_store: 1024 * 1024 * 1024,
},
}),
);
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["A.*"] });
let info = await jsm.consumers.add("A", {
durable_name: "B",
deliver_subject: "here",
deliver_group: "q",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
assertEquals(info.config.deliver_subject, "here");
assertEquals(info.config.deliver_group, "q");
const lock = Lock(1000, 0);
const c = await js.consumers.getPushConsumer("A", "B");
info = await c.info(true);
assertEquals(info.config.deliver_group, "q");
const c2 = await js.consumers.getPushConsumer("A", "B");
info = await c2.info(true);
assertEquals(info.config.deliver_group, "q");
const iter = await c.consume();
(async () => {
for await (const m of iter) {
m.ack();
lock.unlock();
}
})().then();
const iter2 = await c2.consume();
(async () => {
for await (const m of iter2) {
m.ack();
lock.unlock();
}
})().then();
const buf = [];
for (let i = 0; i < 1000; i++) {
buf.push(js.publish(`A.${i}`));
if (buf.length % 500 === 0) {
await Promise.all(buf);
buf.length = 0;
}
}
await lock;
assert(iter.getProcessed() > 0);
assert(iter2.getProcessed() > 0);
info = await c.info(false);
assertEquals(info.delivered.consumer_seq, 1000);
assertEquals(info.num_pending, 0);
assertEquals(info.push_bound, true);
await cleanup(ns, nc);
});
Deno.test("push consumers - connection status iterator closes", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["A.*"] });
await jsm.consumers.add("A", {
durable_name: "B",
deliver_subject: "here",
idle_heartbeat: nanos(1000),
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const c = await js.consumers.getPushConsumer("A", "B");
const msgs = await c.consume({
callback: (m) => {
m.ack();
},
}) as PushConsumerMessagesImpl;
await delay(1000);
assertExists(msgs.statusIterator);
await msgs.close();
assert(msgs.statusIterator.done);
await cleanup(ns, nc);
});
Deno.test("push consumers - connection close closes", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["A.*"] });
await jsm.consumers.add("A", {
durable_name: "B",
deliver_subject: "here",
idle_heartbeat: nanos(1000),
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const c = await js.consumers.getPushConsumer("A", "B");
const iter = await c.consume({
callback: (m) => {
m.ack();
},
}) as PushConsumerMessagesImpl;
await nc.close();
await deadline(iter.closed(), 1000);
await cleanup(ns, nc);
});
--- jetstream/tests/consumers_test.ts ---
/*
* 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);
});
--- jetstream/tests/direct_consumer_test.ts ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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 } from "@std/assert";
import { jetstreamManager, type StoredMsg } from "../src/mod.ts";
import {
cleanup,
jetstreamServerConf,
notCompatible,
setup,
} from "test_helpers";
import {
DirectConsumer,
type DirectStartOptions,
DirectStreamAPIImpl,
} from "../src/jsm_direct.ts";
Deno.test("direct consumer - next", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"], allow_direct: true });
const js = jsm.jetstream();
await Promise.all([
js.publish("a"),
js.publish("a"),
js.publish("a"),
]);
const dc = new DirectConsumer(
"A",
new DirectStreamAPIImpl(nc),
{ seq: 0 } as DirectStartOptions,
);
const m = await dc.next();
assertEquals(m?.seq, 1);
assertEquals((await dc.next())?.seq, 2);
assertEquals((await dc.next())?.seq, 3);
assertEquals(await dc.next(), null);
await cleanup(ns, nc);
});
Deno.test("direct consumer - batch", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"], allow_direct: true });
const js = jsm.jetstream();
const buf = [];
for (let i = 0; i < 100; i++) {
buf.push(js.publish("a", `${i}`));
}
await Promise.all(buf);
const dc = new DirectConsumer(
"A",
new DirectStreamAPIImpl(nc),
{ seq: 0 } as DirectStartOptions,
);
let iter = await dc.fetch({ batch: 5 });
let s = 0;
let last: StoredMsg | undefined;
for await (const sm of iter) {
assertEquals(sm.seq, ++s);
last = sm;
}
assertEquals(s, 5);
assertEquals(last?.pending, 95);
const n = await dc.next();
if (n) {
last = n;
s = 6;
}
assertEquals(last?.seq, 6);
iter = await dc.fetch();
for await (const sm of iter) {
last = sm;
assertEquals(sm.seq, ++s);
}
assertEquals(s, 100);
assertEquals(last?.pending, 0);
await cleanup(ns, nc);
});
Deno.test("direct consumer - consume", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "A",
subjects: ["a", "b"],
allow_direct: true,
});
const js = jsm.jetstream();
const buf = [];
for (let i = 0; i < 100; i++) {
const subj = Math.random() >= .5 ? "b" : "a";
buf.push(js.publish(subj, `${i}`));
}
await Promise.all(buf);
const dc = new DirectConsumer(
"A",
new DirectStreamAPIImpl(nc),
{ seq: 0 } as DirectStartOptions,
);
dc.debug();
let nexts = 0;
(async () => {
for await (const s of dc.status()) {
switch (s.type) {
case "next":
nexts += s.options.batch;
break;
default:
// nothing
}
}
})().catch();
const iter = await dc.consume({ batch: 7 });
for await (const m of iter) {
if (m.pending === 0) {
break;
}
}
assertEquals(iter.getProcessed(), 100);
assert(nexts > 100);
await cleanup(ns, nc);
});
--- jetstream/tests/fetch_test.ts ---
/*
* Copyright 2022-2024 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 { cleanup, jetstreamServerConf, setup } from "test_helpers";
import { initStream } from "./jstest_util.ts";
import { assertEquals, assertExists, assertRejects } from "@std/assert";
import {
deadline,
deferred,
delay,
Empty,
nanos,
syncIterator,
} from "@nats-io/nats-core";
import type { NatsConnectionImpl } from "@nats-io/nats-core/internal";
import {
AckPolicy,
DeliverPolicy,
jetstream,
jetstreamManager,
} from "../src/mod.ts";
import type { PullConsumerMessagesImpl } from "../src/consumer.ts";
Deno.test("fetch - no messages", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(stream, {
durable_name: "b",
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
const consumer = await js.consumers.get(stream, "b");
const iter = await consumer.fetch({
max_messages: 100,
expires: 1000,
});
for await (const m of iter) {
m.ack();
}
assertEquals(iter.getReceived(), 0);
assertEquals(iter.getProcessed(), 0);
await cleanup(ns, nc);
});
Deno.test("fetch - less messages", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream, subj } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj, Empty);
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(stream, {
durable_name: "b",
ack_policy: AckPolicy.Explicit,
});
const consumer = await js.consumers.get(stream, "b");
assertEquals((await consumer.info(true)).num_pending, 1);
const iter = await consumer.fetch({ expires: 1000, max_messages: 10 });
for await (const m of iter) {
m.ack();
}
assertEquals(iter.getReceived(), 1);
assertEquals(iter.getProcessed(), 1);
await cleanup(ns, nc);
});
Deno.test("fetch - exactly messages", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream, subj } = await initStream(nc);
const js = jetstream(nc);
await Promise.all(
new Array(200).fill("a").map((_, idx) => {
return js.publish(subj, `${idx}`);
}),
);
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(stream, {
durable_name: "b",
ack_policy: AckPolicy.Explicit,
});
const consumer = await js.consumers.get(stream, "b");
assertEquals((await consumer.info(true)).num_pending, 200);
const iter = await consumer.fetch({ expires: 5000, max_messages: 100 });
for await (const m of iter) {
m.ack();
}
assertEquals(iter.getReceived(), 100);
assertEquals(iter.getProcessed(), 100);
await cleanup(ns, nc);
});
Deno.test("fetch - deleted consumer", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
const c = await js.consumers.get("A", "a");
const iter = await c.fetch({
expires: 3000,
});
const exited = assertRejects(
async () => {
for await (const _ of iter) {
// nothing
}
},
Error,
"consumer deleted",
);
await delay(1000);
await c.delete();
await exited;
await cleanup(ns, nc);
});
Deno.test("fetch - listener leaks", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["hello"] });
const js = jetstream(nc);
await jsm.consumers.add("messages", {
durable_name: "myconsumer",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const nci = nc as NatsConnectionImpl;
const base = nci.protocol.listeners.length;
const consumer = await js.consumers.get("messages", "myconsumer");
const iter = await consumer.fetch({ max_messages: 1, expires: 2000 });
for await (const _ of iter) {
// nothing
}
assertEquals(nci.protocol.listeners.length, base);
await cleanup(ns, nc);
});
Deno.test("fetch - sync", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
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.fetch({ max_messages: 2 });
const sync = syncIterator(iter);
assertExists(await sync.next());
assertExists(await sync.next());
assertEquals(await sync.next(), null);
await cleanup(ns, nc);
});
Deno.test("fetch - consumer bind", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
await js.publish("a");
const c = await js.consumers.get("A", "a");
const cisub = nc.subscribe("$JS.API.CONSUMER.INFO.A.a", {
callback: () => {},
});
let iter = await c.fetch({
expires: 1000,
bind: true,
});
for await (const _ of iter) {
// nothing
}
iter = await c.fetch({
expires: 1000,
bind: true,
});
for await (const _ of iter) {
// nothing
}
assertEquals(cisub.getProcessed(), 0);
await cleanup(ns, nc);
});
Deno.test("fetch - exceeding max_messages will stop", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
max_batch: 100,
});
const js = jetstream(nc);
const c = await js.consumers.get("A", "a");
const iter = await c.fetch({ max_messages: 1000 });
await assertRejects(
async () => {
for await (const _ of iter) {
// ignore
}
},
Error,
"exceeded maxrequestbatch of 100",
);
await cleanup(ns, nc);
});
Deno.test("fetch - timer is based on idle_hb", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
await js.publish("a");
const c = await js.consumers.get("A", "a");
const iter = await c.fetch({
expires: 2000,
max_messages: 10,
}) as PullConsumerMessagesImpl;
let hbm = false;
(async () => {
for await (const s of iter.status()) {
console.log(s);
if (s.type === "heartbeats_missed") {
hbm = true;
}
}
})().then();
const buf = [];
await assertRejects(
async () => {
for await (const m of iter) {
buf.push(m);
m.ack();
// make the subscription now fail
const nci = nc as NatsConnectionImpl;
nci._resub(iter.sub, "foo");
}
},
Error,
"heartbeats missed",
);
assertEquals(buf.length, 1);
assertEquals(hbm, true);
await cleanup(ns, nc);
});
Deno.test("fetch - connection close exits", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
await js.publish("a");
const c = await js.consumers.get("A", "a");
const iter = await c.fetch({
expires: 30_000,
max_messages: 10,
}) as PullConsumerMessagesImpl;
const done = (async () => {
for await (const _ of iter) {
// nothing
}
})();
await nc.close();
await deadline(done, 1000);
await cleanup(ns, nc);
});
Deno.test("fetch - stream not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
const c = await jsm.jetstream().consumers.get("messages", "c");
await jsm.streams.delete("messages");
await assertRejects(
async () => {
const iter = await c.fetch({ expires: 5_000 });
for await (const _ of iter) {
// ignored
}
},
Error,
"no responders",
);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
await jsm.jetstream().publish("m");
const mP = deferred();
const iter = await c.fetch({ expires: 5_000 });
for await (const _ of iter) {
mP.resolve();
break;
}
await mP;
await cleanup(ns, nc);
});
Deno.test("fetch - consumer not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
const c = await jsm.jetstream().consumers.get("messages", "c");
await jsm.consumers.delete("messages", "c");
await assertRejects(
async () => {
const iter = await c.fetch({ expires: 5_000 });
for await (const _ of iter) {
// ignored
}
},
Error,
"no responders",
);
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
await jsm.jetstream().publish("m");
const mP = deferred();
const iter = await c.fetch({ expires: 5_000 });
for await (const _ of iter) {
mP.resolve();
break;
}
await mP;
await cleanup(ns, nc);
});
--- jetstream/tests/jetstream_pullconsumer_test.ts ---
/*
* 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 {
cleanup,
connect,
jetstreamExportServerConf,
jetstreamServerConf,
notCompatible,
setup,
} from "test_helpers";
import { initStream } from "./jstest_util.ts";
import {
AckPolicy,
type ConsumerConfig,
DeliverPolicy,
type OverflowMinPendingAndMinAck,
type PrioritizedOptions,
PriorityPolicy,
} from "../src/jsapi_types.ts";
import { assertEquals, assertExists } from "@std/assert";
import {
deferred,
delay,
Empty,
type Msg,
nanos,
nuid,
} from "@nats-io/nats-core";
import {
type ConsumeOptions,
type ConsumerMessages,
jetstream,
jetstreamManager,
} from "../src/mod.ts";
Deno.test("jetstream - pull consumer 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, {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
max_batch: 10,
max_expires: nanos(20000),
});
assertEquals(v.config.max_batch, 10);
assertEquals(v.config.max_expires, nanos(20000));
await cleanup(ns, nc);
});
Deno.test("jetstream - cross account pull", async () => {
const { ns, nc: admin } = await setup(jetstreamExportServerConf(), {
user: "js",
pass: "js",
});
// add a stream
const { stream, subj } = await initStream(admin);
const admjs = jetstream(admin);
await admjs.publish(subj);
await admjs.publish(subj);
const admjsm = await jetstreamManager(admin);
// create a durable config
await admjsm.consumers.add(stream, {
ack_policy: AckPolicy.None,
durable_name: "me",
});
const nc = await connect({
port: ns.port,
user: "a",
pass: "s3cret",
inboxPrefix: "A",
});
// the api prefix is not used for pull/fetch()
const js = jetstream(nc, { apiPrefix: "IPA" });
const c = await js.consumers.get(stream, "me");
let msg = await c.next();
assertExists(msg);
assertEquals(msg.seq, 1);
msg = await c.next();
assertExists(msg);
assertEquals(msg.seq, 2);
msg = await c.next({ expires: 5000 });
assertEquals(msg, null);
await cleanup(ns, admin, nc);
});
Deno.test("jetstream - last of", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const n = nuid.next();
await jsm.streams.add({
name: n,
subjects: [`${n}.>`],
});
const subja = `${n}.A`;
const subjb = `${n}.B`;
const js = jetstream(nc);
await js.publish(subja, Empty);
await js.publish(subjb, Empty);
await js.publish(subjb, Empty);
await js.publish(subja, Empty);
const opts = {
durable_name: "B",
filter_subject: subjb,
deliver_policy: DeliverPolicy.Last,
ack_policy: AckPolicy.Explicit,
} as Partial<ConsumerConfig>;
await jsm.consumers.add(n, opts);
const c = await js.consumers.get(n, "B");
const m = await c.next();
assertExists(m);
assertEquals(m.seq, 3);
await cleanup(ns, nc);
});
Deno.test("jetstream - priority group overflow", async (t) => {
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: "A",
subjects: [`a`],
});
const js = jetstream(nc);
const buf = [];
for (let i = 0; i < 100; i++) {
buf.push(js.publish("a", Empty));
}
await Promise.all(buf);
const opts = {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
priority_groups: ["overflow"],
priority_policy: PriorityPolicy.Overflow,
};
await jsm.consumers.add("A", opts);
function spyPull(): Promise<Msg> {
const d = deferred<Msg>();
nc.subscribe(`$JS.API.CONSUMER.MSG.NEXT.A.a`, {
callback: (err, msg) => {
if (err) {
d.reject(err);
}
d.resolve(msg);
},
});
return d;
}
await t.step("consume", async () => {
async function check(opts: ConsumeOptions): Promise<void> {
const c = await js.consumers.get("A", "a");
const d = spyPull();
const c1 = await c.consume(opts);
const done = (async () => {
for await (const m of c1) {
m.ack();
}
})();
const m = await d;
c1.stop();
await done;
const po = m.json<OverflowMinPendingAndMinAck>();
const oopts = opts as OverflowMinPendingAndMinAck;
assertEquals(po.group, opts.group);
assertEquals(po.min_ack_pending, oopts.min_ack_pending);
assertEquals(po.min_pending, oopts.min_pending);
}
await check({
max_messages: 2,
group: "overflow",
min_ack_pending: 2,
});
await check({
max_messages: 2,
group: "overflow",
min_pending: 10,
});
await check({
max_messages: 2,
group: "overflow",
min_pending: 10,
min_ack_pending: 100,
});
});
await t.step("fetch", async () => {
async function check(opts: ConsumeOptions): Promise<void> {
const c = await js.consumers.get("A", "a");
const d = spyPull();
const iter = await c.fetch(opts);
for await (const m of iter) {
m.ack();
}
const m = await d;
const po = m.json<OverflowMinPendingAndMinAck>();
const oopts = opts as OverflowMinPendingAndMinAck;
assertEquals(po.group, opts.group);
assertEquals(po.min_ack_pending, oopts.min_ack_pending);
assertEquals(po.min_pending, oopts.min_pending);
}
await check({
max_messages: 2,
group: "overflow",
min_ack_pending: 2,
expires: 1000,
});
await check({
max_messages: 2,
group: "overflow",
min_pending: 10,
expires: 1000,
});
await check({
max_messages: 2,
group: "overflow",
min_pending: 10,
min_ack_pending: 100,
expires: 1000,
});
});
await t.step("next", async () => {
async function check(opts: ConsumeOptions): Promise<void> {
const c = await js.consumers.get("A", "a");
const d = spyPull();
await c.next(opts);
const m = await d;
const po = m.json<OverflowMinPendingAndMinAck>();
const oopts = opts as OverflowMinPendingAndMinAck;
assertEquals(po.group, opts.group);
assertEquals(po.min_ack_pending, oopts.min_ack_pending);
assertEquals(po.min_pending, oopts.min_pending);
}
await check({
max_messages: 2,
group: "overflow",
min_ack_pending: 2,
expires: 1000,
});
await check({
max_messages: 2,
group: "overflow",
min_pending: 10,
expires: 1000,
});
await check({
max_messages: 2,
group: "overflow",
min_pending: 10,
min_ack_pending: 100,
expires: 1000,
});
});
await cleanup(ns, nc);
});
Deno.test("jetstream - priority group prioritized", async (t) => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "A",
subjects: [`a`],
allow_atomic: true,
});
const js = jetstream(nc);
const b = await js.startBatch("a", Empty);
for (let i = 0; i < 98; i++) {
await b.add("a", Empty, { ack: i % 20 === 0 });
}
const done = await b.commit("a", Empty);
assertEquals(done.count, 100);
const opts = {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
priority_groups: ["prioritized"],
priority_policy: PriorityPolicy.Prioritized,
};
await jsm.consumers.add("A", opts);
function spyPull(): Promise<Msg> {
const d = deferred<Msg>();
nc.subscribe(`$JS.API.CONSUMER.MSG.NEXT.A.a`, {
callback: (err, msg) => {
if (err) {
d.reject(err);
}
d.resolve(msg);
},
});
return d;
}
await t.step("consume", async () => {
async function check(opts: ConsumeOptions): Promise<void> {
const c = await js.consumers.get("A", "a");
const d = spyPull();
const c1 = await c.consume(opts);
const done = (async () => {
for await (const m of c1) {
m.ack();
}
})();
const m = await d;
c1.stop();
await done;
// check the pull options have the priority options
const po = m.json<PrioritizedOptions>();
assertEquals(po.group, opts.group);
assertEquals(po.priority, opts.priority);
}
await check({
max_messages: 2,
group: "prioritized",
priority: 1,
});
await check({
max_messages: 2,
group: "prioritized",
priority: 8,
});
await check({
max_messages: 2,
group: "prioritized",
priority: 5,
});
});
await t.step("fetch", async () => {
async function check(opts: ConsumeOptions): Promise<void> {
const c = await js.consumers.get("A", "a");
const d = spyPull();
const iter = await c.fetch(opts);
for await (const m of iter) {
m.ack();
}
const m = await d;
// check the pull options have the priority options
const po = m.json<PrioritizedOptions>();
assertEquals(po.group, opts.group);
assertEquals(po.priority, opts.priority);
}
await check({
max_messages: 2,
group: "prioritized",
priority: 1,
expires: 1000,
});
await check({
max_messages: 2,
group: "prioritized",
priority: 8,
expires: 1000,
});
await check({
max_messages: 2,
group: "prioritized",
priority: 5,
expires: 1000,
});
});
await t.step("next", async () => {
async function check(opts: ConsumeOptions): Promise<void> {
const c = await js.consumers.get("A", "a");
const d = spyPull();
await c.next(opts);
const m = await d;
const po = m.json<PrioritizedOptions>();
assertEquals(po.group, opts.group);
assertEquals(po.priority, opts.priority);
}
await check({
group: "prioritized",
priority: 2,
expires: 1000,
});
await check({
group: "prioritized",
priority: 8,
expires: 1000,
});
await check({
group: "prioritized",
priority: 5,
expires: 1000,
});
});
await cleanup(ns, nc);
});
Deno.test("jetstream - pinned client", 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: "A",
subjects: [`a`],
});
const js = jetstream(nc);
const buf = [];
for (let i = 0; i < 100; i++) {
buf.push(js.publish("a", Empty));
}
await Promise.all(buf);
const opts = {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
priority_groups: ["pinned"],
priority_timeout: nanos(5_000),
priority_policy: PriorityPolicy.PinnedClient,
};
await jsm.consumers.add("A", opts);
async function processStatus(
msgs: ConsumerMessages,
bailOn = "",
): Promise<string[]> {
const buf = [];
for await (const s of msgs.status()) {
//@ts-ignore: test
buf.push(s.type);
if (bailOn === s.type) {
msgs.stop();
}
}
return buf;
}
async function process(msgs: ConsumerMessages, wait = 0) {
for await (const m of msgs) {
if (wait > 0) {
await delay(wait);
}
m.ack();
if (m.info.pending === 0) {
break;
}
}
}
const a = await js.consumers.get("A", "a");
const iter = await a.consume({ group: "pinned", max_messages: 1 });
const eventsA = processStatus(iter, "consumer_unpinned").catch();
const done = process(iter, 10_000);
const b = await js.consumers.get("A", "a");
const iter2 = await b.consume({ group: "pinned", max_messages: 1 });
const eventsB = processStatus(iter2).catch();
await process(iter2);
await done;
assertEquals(iter.getReceived(), 1);
assertEquals(iter2.getReceived(), 99);
// check that A was unpinned
assertEquals(
(await eventsA).filter((e) => {
return e === "consumer_unpinned";
}).length,
1,
);
// and B was pinned
assertEquals(
(await eventsB).filter((e) => {
return e === "consumer_pinned";
}).length,
1,
);
await cleanup(ns, nc);
});
Deno.test("jetstream - unpin client", 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: "A",
subjects: [`a`],
});
const js = jetstream(nc);
for (let i = 0; i < 100; i++) {
await js.publish("a", Empty);
}
const opts = {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
priority_groups: ["pinned"],
priority_timeout: nanos(10_000),
priority_policy: PriorityPolicy.PinnedClient,
};
await jsm.consumers.add("A", opts);
async function bailOn(
n: string,
msgs: ConsumerMessages,
bailOn = "",
): Promise<void> {
for await (const s of msgs.status()) {
// @ts-ignore: teest
console.log(n, s.type === "next" ? s.type + "=" + s.options?.id : s.type);
if (bailOn === s.type) {
console.log(n, "bailing");
msgs.stop();
}
}
}
async function process(n: string, msgs: ConsumerMessages) {
for await (const m of msgs) {
console.log(n, m.seq);
m.ack();
await delay(3_000);
}
}
const a = await js.consumers.get("A", "a");
const iter = await a.consume({
group: "pinned",
max_messages: 1,
expires: 5000,
});
bailOn("a", iter, "consumer_unpinned").catch();
const done = process("a", iter);
const b = await js.consumers.get("A", "a");
const iter2 = await b.consume({
group: "pinned",
max_messages: 1,
expires: 5000,
});
bailOn("b", iter2, "consumer_pinned").catch();
process("b", iter2).then();
(async () => {
for await (const a of jsm.advisories()) {
console.log("advisory:", a.kind);
}
})().then();
await jsm.consumers.unpin("A", "a", "pinned");
await done;
await js.publish("a", Empty);
await iter.closed();
assertEquals(iter.getReceived(), 1);
await iter2.closed();
assertEquals(iter2.getReceived(), 1);
await cleanup(ns, nc);
});
--- jetstream/tests/jetstream_pushconsumer_test.ts ---
/*
* 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 {
assertBetween,
cleanup,
connect,
jetstreamExportServerConf,
jetstreamServerConf,
notCompatible,
setup,
} from "test_helpers";
import { initStream } from "./jstest_util.ts";
import {
createInbox,
deferred,
delay,
Empty,
InvalidArgumentError,
nanos,
nuid,
syncIterator,
} from "@nats-io/nats-core";
import type { BoundPushConsumerOptions, PubAck } from "../src/types.ts";
import { assert, assertEquals, assertExists, assertRejects } from "@std/assert";
import {
AckPolicy,
DeliverPolicy,
PriorityPolicy,
StorageType,
} from "../src/jsapi_types.ts";
import type { JsMsg } from "../src/jsmsg.ts";
import { jetstream, jetstreamManager } from "../src/mod.ts";
import type {
PushConsumerImpl,
PushConsumerMessagesImpl,
} from "../src/pushconsumer.ts";
import type { NatsConnectionImpl } from "@nats-io/nats-core/internal";
Deno.test("jetstream - durable", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj);
const jsm = await js.jetstreamManager();
await jsm.consumers.add(stream, {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
deliver_subject: createInbox(),
});
const c = await js.consumers.getPushConsumer(stream, "me");
const iter = await c.consume();
for await (const m of iter) {
m.ack();
break;
}
// consumer should exist
const ci = await c.info();
assertEquals(ci.name, "me");
// delete the consumer
await c.delete();
await assertRejects(
async () => {
await c.info();
},
Error,
"consumer not found",
undefined,
);
await cleanup(ns, nc);
});
Deno.test("jetstream - queue error checks", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.3.5")) {
return;
}
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.consumers.add(stream, {
durable_name: "me",
deliver_subject: "x",
deliver_group: "x",
idle_heartbeat: nanos(1000),
});
},
Error,
"'idle_heartbeat','deliver_group' are mutually exclusive",
undefined,
);
await assertRejects(
async () => {
await jsm.consumers.add(stream, {
durable_name: "me",
deliver_subject: "x",
deliver_group: "x",
flow_control: true,
});
},
InvalidArgumentError,
"'flow_control','deliver_group' are mutually exclusive",
);
await cleanup(ns, nc);
});
Deno.test("jetstream - max ack pending", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const jsm = await jetstreamManager(nc);
const d = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
const buf: Promise<PubAck>[] = [];
const js = jetstream(nc);
d.forEach((v) => {
buf.push(js.publish(subj, v, { msgID: v }));
});
await Promise.all(buf);
const consumers = await jsm.consumers.list(stream).next();
assert(consumers.length === 0);
const ci = await jsm.consumers.add(
stream,
{
max_ack_pending: 2,
ack_policy: AckPolicy.Explicit,
deliver_subject: createInbox(),
},
);
const c = await js.consumers.getPushConsumer(stream, ci.name);
const iter = await c.consume();
await (async () => {
for await (const m of iter) {
assert(
iter.getPending() < 3,
`didn't expect pending messages greater than 2`,
);
m.ack();
if (m.info.pending === 0) {
break;
}
}
})();
await cleanup(ns, nc);
});
Deno.test("jetstream - deliver new", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { subj, stream } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj, Empty, { expect: { lastSequence: 0 } });
await js.publish(subj, Empty, { expect: { lastSequence: 1 } });
await js.publish(subj, Empty, { expect: { lastSequence: 2 } });
await js.publish(subj, Empty, { expect: { lastSequence: 3 } });
await js.publish(subj, Empty, { expect: { lastSequence: 4 } });
const jsm = await js.jetstreamManager();
const ci = await jsm.consumers.add(stream, {
ack_policy: AckPolicy.Explicit,
deliver_policy: DeliverPolicy.New,
deliver_subject: createInbox(),
});
const c = await js.consumers.getPushConsumer(stream, ci.name);
const iter = await c.consume();
const done = (async () => {
for await (const m of iter) {
assertEquals(m.seq, 6);
break;
}
})();
await js.publish(subj, Empty, { expect: { lastSequence: 5 } });
await done;
await cleanup(ns, nc);
});
Deno.test("jetstream - deliver last", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { subj, stream } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj, Empty, { expect: { lastSequence: 0 } });
await js.publish(subj, Empty, { expect: { lastSequence: 1 } });
await js.publish(subj, Empty, { expect: { lastSequence: 2 } });
await js.publish(subj, Empty, { expect: { lastSequence: 3 } });
await js.publish(subj, Empty, { expect: { lastSequence: 4 } });
const jsm = await js.jetstreamManager();
const ci = await jsm.consumers.add(stream, {
ack_policy: AckPolicy.Explicit,
deliver_policy: DeliverPolicy.Last,
deliver_subject: createInbox(),
});
const c = await js.consumers.getPushConsumer(stream, ci.name);
const iter = await c.consume();
const done = (async () => {
for await (const m of iter) {
assertEquals(m.seq, 5);
break;
}
})();
await done;
await cleanup(ns, nc);
});
Deno.test("jetstream - deliver seq", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { subj, stream } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj, Empty, { expect: { lastSequence: 0 } });
await js.publish(subj, Empty, { expect: { lastSequence: 1 } });
await js.publish(subj, Empty, { expect: { lastSequence: 2 } });
await js.publish(subj, Empty, { expect: { lastSequence: 3 } });
await js.publish(subj, Empty, { expect: { lastSequence: 4 } });
const jsm = await js.jetstreamManager();
const ci = await jsm.consumers.add(stream, {
ack_policy: AckPolicy.Explicit,
deliver_policy: DeliverPolicy.StartSequence,
opt_start_seq: 2,
deliver_subject: createInbox(),
});
const c = await js.consumers.getPushConsumer(stream, ci.name);
const iter = await c.consume();
const done = (async () => {
for await (const m of iter) {
assertEquals(m.seq, 2);
break;
}
})();
await done;
await cleanup(ns, nc);
});
Deno.test("jetstream - deliver start time", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { subj, stream } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj, Empty, { expect: { lastSequence: 0 } });
await js.publish(subj, Empty, { expect: { lastSequence: 1 } });
await delay(1000);
const now = new Date();
await js.publish(subj, Empty, { expect: { lastSequence: 2 } });
const jsm = await js.jetstreamManager();
const ci = await jsm.consumers.add(stream, {
ack_policy: AckPolicy.Explicit,
deliver_policy: DeliverPolicy.StartTime,
opt_start_time: now.toISOString(),
deliver_subject: createInbox(),
});
const sub = await js.consumers.getPushConsumer(stream, ci.name);
const iter = await sub.consume();
const done = (async () => {
for await (const m of iter) {
assertEquals(m.seq, 3);
break;
}
})();
await done;
await cleanup(ns, nc);
});
Deno.test("jetstream - deliver last per subject", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc)) {
return;
}
const jsm = await jetstreamManager(nc);
const stream = nuid.next();
const subj = `${stream}.*`;
await jsm.streams.add(
{ name: stream, subjects: [subj] },
);
const js = jetstream(nc);
await js.publish(`${stream}.A`, Empty, { expect: { lastSequence: 0 } });
await js.publish(`${stream}.B`, Empty, { expect: { lastSequence: 1 } });
await js.publish(`${stream}.A`, Empty, { expect: { lastSequence: 2 } });
await js.publish(`${stream}.B`, Empty, { expect: { lastSequence: 3 } });
await js.publish(`${stream}.A`, Empty, { expect: { lastSequence: 4 } });
await js.publish(`${stream}.B`, Empty, { expect: { lastSequence: 5 } });
let ci = await jsm.consumers.add(stream, {
ack_policy: AckPolicy.Explicit,
deliver_policy: DeliverPolicy.LastPerSubject,
filter_subject: ">",
deliver_subject: createInbox(),
});
const sub = await js.consumers.getPushConsumer(stream, ci.name);
const iter = await sub.consume();
const buf: JsMsg[] = [];
const done = (async () => {
for await (const m of iter) {
buf.push(m);
if (buf.length === 2) {
break;
}
}
})();
await done;
assertEquals(buf[0].info.streamSequence, 5);
assertEquals(buf[1].info.streamSequence, 6);
ci = await sub.info();
assertEquals(ci.num_ack_pending, 2);
await cleanup(ns, nc);
});
Deno.test("jetstream - cross account subscribe", async () => {
const { ns, nc: admin } = await setup(jetstreamExportServerConf(), {
user: "js",
pass: "js",
});
// add a stream
const { subj, stream } = await initStream(admin);
const adminjs = jetstream(admin);
await adminjs.publish(subj);
await adminjs.publish(subj);
const nc = await connect({
port: ns.port,
user: "a",
pass: "s3cret",
inboxPrefix: "A",
});
const js = jetstream(nc, { apiPrefix: "IPA" });
const jsm = await js.jetstreamManager();
let ci = await jsm.consumers.add(stream, {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
deliver_subject: createInbox("A"),
});
const acks: Promise<boolean>[] = [];
const sub = await js.consumers.getPushConsumer(stream, ci.name);
const messages = await sub.consume();
await (async () => {
for await (const m of messages) {
acks.push(m.ackAck());
if (m.seq === 2) {
break;
}
}
})();
await Promise.all(acks);
ci = await sub.info();
assertEquals(ci.num_pending, 0);
assertEquals(ci.delivered.stream_seq, 2);
assertEquals(ci.ack_floor.stream_seq, 2);
await sub.delete();
await assertRejects(
async () => {
await sub.info();
},
Error,
"consumer not found",
undefined,
);
await cleanup(ns, admin, nc);
});
Deno.test("jetstream - ack lease extends with working", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const sn = nuid.next();
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: sn, subjects: [`${sn}.>`] });
const js = jetstream(nc);
await js.publish(`${sn}.A`, Empty, { msgID: "1" });
const cc = {
"ack_wait": nanos(2000),
"deliver_subject": createInbox(),
"ack_policy": AckPolicy.Explicit,
"durable_name": "me",
};
const ci = await jsm.consumers.add(sn, cc);
const c = await js.consumers.getPushConsumer(sn, ci.name);
const messages = await c.consume();
const done = (async () => {
for await (const m of messages) {
const timer = setInterval(() => {
m.working();
}, 750);
// we got a message now we are going to delay for 31 sec
await delay(15);
const ci = await jsm.consumers.info(sn, "me");
assertEquals(ci.num_ack_pending, 1);
m.ack();
clearInterval(timer);
break;
}
})();
await done;
// make sure the message went out
await nc.flush();
const ci2 = await c.info();
assertEquals(ci2.delivered.stream_seq, 1);
assertEquals(ci2.num_redelivered, 0);
assertEquals(ci2.num_ack_pending, 0);
await cleanup(ns, nc);
});
Deno.test("jetstream - idle heartbeats", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj);
const jsm = await jetstreamManager(nc);
const inbox = createInbox();
await jsm.consumers.add(stream, {
durable_name: "me",
deliver_subject: inbox,
idle_heartbeat: nanos(2000),
});
const c = await js.consumers.getPushConsumer(stream, "me");
const messages = await c.consume({
callback: (_) => {
},
});
const status = messages.status();
for await (const s of status) {
console.log(s);
if (s.type === "heartbeat") {
assertEquals(s.lastConsumerSequence, 1);
assertEquals(s.lastStreamSequence, 1);
messages.stop();
}
}
await messages.closed();
await cleanup(ns, nc);
});
Deno.test("jetstream - flow control", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
jetstream: {
max_file_store: -1,
},
}),
);
const { stream, subj } = await initStream(nc);
const data = new Uint8Array(1024 * 100);
const js = jetstream(nc);
const proms = [];
for (let i = 0; i < 2000; i++) {
proms.push(js.publish(subj, data));
nc.publish(subj, data);
if (proms.length % 100 === 0) {
await Promise.all(proms);
proms.length = 0;
}
}
if (proms.length) {
await Promise.all(proms);
}
await nc.flush();
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(stream, {
durable_name: "me",
deliver_subject: createInbox(),
flow_control: true,
idle_heartbeat: nanos(5000),
});
const c = await js.consumers.getPushConsumer(stream, "me");
const iter = await c.consume({ callback: () => {} });
const status = iter.status();
for await (const s of status) {
if (s.type === "flow_control") {
iter.stop();
}
}
await cleanup(ns, nc);
});
Deno.test("jetstream - durable resumes", async () => {
let { ns, nc } = await setup(jetstreamServerConf({}), {
maxReconnectAttempts: -1,
reconnectTimeWait: 100,
});
const { stream, subj } = await initStream(nc);
const jsm = await jetstreamManager(nc);
const js = jetstream(nc);
let values = ["a", "b", "c"];
for (const v of values) {
await js.publish(subj, v);
}
await jsm.consumers.add(stream, {
ack_policy: AckPolicy.Explicit,
deliver_policy: DeliverPolicy.All,
deliver_subject: createInbox(),
durable_name: "me",
});
const sub = await js.consumers.getPushConsumer(stream, "me");
const iter = await sub.consume({
callback: (m) => {
m.ack();
if (m.seq === 6) {
iter.close();
}
},
});
await nc.flush();
await ns.stop();
ns = await ns.restart();
await delay(300);
values = ["d", "e", "f"];
for (const v of values) {
await js.publish(subj, v);
}
await nc.flush();
await iter.closed();
const si = await jsm.streams.info(stream);
assertEquals(si.state.messages, 6);
const ci = await sub.info();
assertEquals(ci.delivered.stream_seq, 6);
assertEquals(ci.num_pending, 0);
await cleanup(ns, nc);
});
Deno.test("jetstream - nak delay", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.7.1")) {
return;
}
const { subj, stream } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj);
let start = 0;
const jsm = await js.jetstreamManager();
const ci = await jsm.consumers.add(stream, {
ack_policy: AckPolicy.Explicit,
deliver_subject: createInbox(),
});
const c = await js.consumers.getPushConsumer(stream, ci.name);
const iter = await c.consume({
callback: (m) => {
if (m.redelivered) {
m.ack();
iter.close();
} else {
start = Date.now();
m.nak(2000);
}
},
});
await iter.closed();
const delay = Date.now() - start;
assertBetween(delay, 1800, 2200);
await cleanup(ns, nc);
});
Deno.test("jetstream - bind", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
jetstream: {
max_file_store: -1,
},
}),
);
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
const stream = nuid.next();
const subj = `${stream}.*`;
await jsm.streams.add({
name: stream,
subjects: [subj],
});
const buf = [];
const data = new Uint8Array(1024 * 100);
for (let i = 0; i < 100; i++) {
buf.push(js.publish(`${stream}.${i}`, data));
}
await Promise.all(buf);
const ci = await jsm.consumers.add(stream, {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
deliver_subject: createInbox(),
idle_heartbeat: nanos(1000),
flow_control: true,
});
jsm.consumers.info = () => {
return Promise.reject("called info");
};
console.log(ci.config);
const c = await js.consumers.getBoundPushConsumer(
ci.config as BoundPushConsumerOptions,
);
await assertRejects(
() => {
return c.info();
},
Error,
"bound consumers cannot info",
);
await assertRejects(
() => {
return c.delete();
},
Error,
"bound consumers cannot delete",
);
const messages = await c.consume();
(async () => {
for await (const m of messages) {
m.ack();
if (m.info.pending === 0) {
await delay(3000);
messages.stop();
}
}
})().then();
let fc = 0;
let hb = 0;
for await (const s of messages.status()) {
if (s.type === "flow_control") {
fc++;
} else if (s.type === "heartbeat") {
hb++;
}
}
await messages.closed();
assert(fc > 0);
assert(hb > 0);
assertEquals(messages.getProcessed(), 100);
await cleanup(ns, nc);
});
Deno.test("jetstream - bind example", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
const subj = `A.*`;
await jsm.streams.add({
name: "A",
subjects: [subj],
});
const deliver_subject = createInbox();
await jsm.consumers.add("A", {
durable_name: "me",
deliver_subject,
ack_policy: AckPolicy.Explicit,
idle_heartbeat: nanos(1000),
flow_control: true,
});
const c = await js.consumers.getBoundPushConsumer({
deliver_subject,
idle_heartbeat: nanos(1000),
});
const messages = await c.consume();
(async () => {
for await (const m of messages) {
m.ack();
if (m.info.streamSequence === 100) {
break;
}
}
})().then();
const status = messages.status();
(async () => {
for await (const s of status) {
console.log(s);
}
})().then();
const buf = [];
for (let i = 0; i < 100; i++) {
buf.push(js.publish(`A.${i}`, `${i}`));
if (buf.length % 10) {
await Promise.all(buf);
buf.length = 0;
}
}
await messages.closed();
await cleanup(ns, nc);
});
Deno.test("jetstream - push consumer is bound", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(stream, {
durable_name: "me",
deliver_subject: "here",
});
const js = jsm.jetstream();
const c = await js.consumers.getPushConsumer(stream, "me");
let msgs = await c.consume({
callback: (m) => {
m.ack();
},
});
await assertRejects(
() => {
return c.consume();
},
Error,
"consumer already started",
);
// close and restart should be ok
await msgs.close();
msgs = await c.consume({
callback: (m) => {
m.ack();
},
});
const c2 = await js.consumers.getPushConsumer(stream, "me");
await assertRejects(
() => {
return c2.consume({
callback: (m) => {
m.ack();
},
});
},
Error,
"consumer is already bound",
);
msgs.stop();
await cleanup(ns, nc);
});
Deno.test("jetstream - idleheartbeats notifications don't cancel", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const c = await js.consumers.getBoundPushConsumer({
deliver_subject: "foo",
idle_heartbeat: nanos(1000),
});
const msgs = await c.consume({ callback: () => {} });
let missed = 0;
for await (const s of msgs.status()) {
if (s.type === "heartbeats_missed") {
missed++;
if (missed > 3) {
await msgs.close();
}
}
}
await msgs.closed();
await cleanup(ns, nc);
});
Deno.test("jetstream - push on stopped server doesn't close client", async () => {
let { ns, nc } = await setup(jetstreamServerConf(), {
maxReconnectAttempts: -1,
});
const reconnected = deferred<void>();
(async () => {
let reconnects = 0;
for await (const s of nc.status()) {
switch (s.type) {
case "reconnecting":
reconnects++;
if (reconnects === 2) {
ns.restart().then((s) => {
ns = s;
});
}
break;
case "reconnect":
setTimeout(() => {
reconnected.resolve();
}, 1000);
break;
default:
// nothing
}
}
})().then();
const jsm = await jetstreamManager(nc);
const si = await jsm.streams.add({ name: nuid.next(), subjects: ["test"] });
const { name: stream } = si.config;
const js = jetstream(nc);
await jsm.consumers.add(stream, {
durable_name: "dur",
ack_policy: AckPolicy.Explicit,
deliver_subject: "bar",
});
const c = await js.consumers.getPushConsumer(stream, "dur");
const msgs = await c.consume({
callback: (m) => {
m.ack();
},
});
setTimeout(() => {
ns.stop();
}, 2000);
await reconnected;
assertEquals(nc.isClosed(), false);
assertEquals((msgs as PushConsumerMessagesImpl).sub.isClosed(), false);
await cleanup(ns, nc);
});
Deno.test("jetstream - push sync", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const name = nuid.next();
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name,
subjects: [name],
storage: StorageType.Memory,
});
await jsm.consumers.add(name, {
durable_name: name,
ack_policy: AckPolicy.Explicit,
deliver_subject: "here",
});
const js = jetstream(nc);
await js.publish(name);
await js.publish(name);
const c = await js.consumers.getPushConsumer(name, name);
const sync = syncIterator(await c.consume());
assertExists(await sync.next());
assertExists(await sync.next());
await cleanup(ns, nc);
});
Deno.test("jetstream - ordered push consumer honors inbox prefix", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(
{
authorization: {
users: [{
user: "a",
password: "a",
permission: {
subscribe: ["my_inbox_prefix.>", "another.>"],
publish: ">",
},
}],
},
},
),
{
user: "a",
pass: "a",
inboxPrefix: "my_inbox_prefix",
},
);
const nci = nc as NatsConnectionImpl;
assertEquals(nci.options.inboxPrefix, "my_inbox_prefix");
const name = nuid.next();
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name,
subjects: [name],
storage: StorageType.Memory,
});
const js = jsm.jetstream();
await js.publish(name, "hello");
let c = await js.consumers.getPushConsumer(name);
let cc = c as PushConsumerImpl;
// here the create inbox added another token
assert(cc.opts.deliver_prefix?.startsWith("my_inbox_prefix."));
let iter = await c.consume();
for await (const m of iter) {
m.ack();
iter.stop();
}
// now check that if they gives a deliver prefix we use that
// over the inbox
c = await js.consumers.getPushConsumer(name, { deliver_prefix: "another" });
cc = c as PushConsumerImpl;
// here we are just the template
assert(cc.opts.deliver_prefix?.startsWith("another"));
iter = await c.consume();
for await (const m of iter) {
m.ack();
iter.stop();
}
await cleanup(ns, nc);
});
Deno.test("jetstream - push consumer doesn't support priority groups", 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: "A", subjects: ["test"] });
await assertRejects(
() => {
return jsm.consumers.add("A", {
ack_policy: AckPolicy.None,
deliver_subject: "foo",
priority_groups: ["hello"],
priority_policy: PriorityPolicy.Overflow,
});
},
Error,
"'deliver_subject' cannot be set when using priority groups",
);
await cleanup(ns, nc);
});
--- jetstream/tests/jetstream_test.ts ---
/*
* 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);
});
--- jetstream/tests/jetstream409_test.ts ---
/*
* Copyright 2022-2024 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 { nanos } from "@nats-io/nats-core";
import { AckPolicy, jetstream, jetstreamManager } from "../src/mod.ts";
import { assert, assertEquals, assertRejects, fail } from "@std/assert";
import { initStream } from "./jstest_util.ts";
import { cleanup, jetstreamServerConf, setup } from "test_helpers";
Deno.test("409 - max expires", 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,
max_expires: nanos(1_000),
});
const js = jetstream(nc);
const c = await js.consumers.get(stream, "a");
await assertRejects(
() => {
return c.next({ expires: 30_000 });
},
Error,
"exceeded maxrequestexpires",
);
await assertRejects(
async () => {
const iter = await c.fetch({ expires: 30_000 });
for await (const _ of iter) {
fail("shouldn't have gotten a message");
}
},
Error,
"exceeded maxrequestexpires",
);
const iter = await c.consume({
expires: 2_000,
callback: () => {
fail("shouldn't have gotten a message");
},
});
let count = 0;
(async () => {
for await (const s of iter.status()) {
if (s.type === "exceeded_limits") {
if (s.description.includes("exceeded maxrequestexpires")) {
count++;
if (count === 2) {
iter.close();
}
}
}
}
})().then();
await iter.closed();
assert(count >= 2);
await cleanup(ns, nc);
});
Deno.test("409 - max message size", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj, new Uint8Array(1024));
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(stream, {
durable_name: "a",
ack_policy: AckPolicy.Explicit,
});
const c = await js.consumers.get(stream, "a");
const msgs = await c.fetch({ max_bytes: 10 });
(async () => {
for await (const s of msgs.status()) {
if (s.type === "discard") {
if (s.bytesLeft === 10) {
msgs.stop();
}
}
}
})().then();
const err = await assertRejects(async () => {
for await (const _ of msgs) {
fail("shoudn't have gotten any messages");
}
});
assertEquals((err as Error).message, "message size exceeds maxbytes");
const iter = await c.consume({
expires: 5_000,
max_bytes: 10,
callback: () => {
fail("this shouldn't have been called");
},
});
for await (const s of iter.status()) {
if (s.type === "heartbeats_missed") {
// the 409 kicked in, and the client backed off
iter.close().catch();
}
}
await iter.closed();
await cleanup(ns, nc);
});
Deno.test("409 - max batch", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
// only one pull request
await jsm.consumers.add(stream, {
durable_name: "a",
ack_policy: AckPolicy.Explicit,
max_batch: 10,
});
const js = jetstream(nc);
const c = await js.consumers.get(stream, "a");
await assertRejects(
async () => {
const msgs = await c.fetch({ max_messages: 100, expires: 1_000 });
for await (const _ of msgs) {
// nothing
}
},
Error,
"exceeded maxrequestbatch",
);
let count = 0;
const iter = await c.consume({
max_messages: 100,
expires: 2_000,
callback: () => {},
});
for await (const s of iter.status()) {
if (s.type === "exceeded_limits") {
if (s.description.includes("exceeded maxrequestbatch")) {
count++;
if (count >= 2) {
iter.stop();
}
}
}
}
await iter.closed();
assert(count >= 2);
await cleanup(ns, nc);
});
// 2.11.6 changed behaviour - so requrests with expires will not receive `exceeded maxwaiting`
//
// Deno.test("409 - max waiting", async () => {
// const { ns, nc } = await setup(jetstreamServerConf({}));
// const { stream } = await initStream(nc);
//
// const jsm = await jetstreamManager(nc);
//
// // only one pull request
// await jsm.consumers.add(stream, {
// durable_name: "a",
// ack_policy: AckPolicy.Explicit,
// max_waiting: 1,
// });
//
// const js = jetstream(nc);
// const c = await js.consumers.get(stream, "a");
//
// // consume with an open pull
// const blocking = await c.consume({ callback: () => {} });
//
// await assertRejects(
// () => {
// return c.next({ expires: 1_000 });
// },
// Error,
// "exceeded maxwaiting",
// );
//
// await assertRejects(
// async () => {
// const msgs = await c.fetch({ expires: 1_000 });
// for await (const _ of msgs) {
// // nothing
// }
// },
// Error,
// "exceeded maxwaiting",
// );
//
// let count = 0;
//
// const iter = await c.consume({ expires: 1_000, callback: () => {} });
// for await (const s of iter.status()) {
// if (s.type === "exceeded_limits") {
// if (s.description.includes("exceeded maxwaiting")) {
// count++;
// if (count >= 2) {
// iter.stop();
// }
// }
// }
// }
// await iter.closed();
// assert(count >= 2);
//
// // stop the consumer blocking
// blocking.stop();
// await blocking.closed();
// await cleanup(ns, nc);
// });
--- jetstream/tests/jscluster_test.ts ---
import { jetstream, jetstreamManager } from "../src/jsclient.ts";
import {
cleanup,
connect,
flakyTest,
jetstreamServerConf,
NatsServer,
notCompatible,
} from "test_helpers";
import {
DiscardPolicy,
RetentionPolicy,
StorageType,
} from "../src/jsapi_types.ts";
import type { StreamConfig, StreamUpdateConfig } from "../src/jsapi_types.ts";
import { nanos } from "@nats-io/nats-core/internal";
import {
assertArrayIncludes,
assertEquals,
assertExists,
assertRejects,
fail,
} from "@std/assert";
Deno.test("jetstream - mirror alternates", async () => {
const servers = await NatsServer.jetstreamCluster(3);
const nc = await connect({ port: servers[0].port });
if (await notCompatible(servers[0], nc, "2.8.2")) {
await NatsServer.stopAll(servers, true);
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "src", subjects: ["A", "B"] });
const nc1 = await connect({ port: servers[1].port });
const jsm1 = await jetstreamManager(nc1);
await jsm1.streams.add({
name: "mirror",
mirror: {
name: "src",
},
});
const n = await jsm1.streams.find("A");
const si = await jsm1.streams.info(n);
assertEquals(si.alternates?.length, 2);
await nc.close();
await nc1.close();
await NatsServer.stopAll(servers, true);
});
Deno.test("jsm - stream update properties", async () => {
const ns = await NatsServer.start(jetstreamServerConf({}));
const nc = await connect({ port: ns.port });
const jsm = await jetstreamManager(nc, { timeout: 3000 });
await jsm.streams.add({
name: "a",
storage: StorageType.File,
subjects: ["x"],
});
const name = "n";
await jsm.streams.add({
name,
storage: StorageType.File,
subjects: ["subj"],
duplicate_window: nanos(30 * 1000),
});
async function updateOption(
opt: Partial<StreamUpdateConfig | StreamConfig>,
shouldFail = false,
): Promise<void> {
try {
const si = await jsm.streams.update(name, opt);
for (const v of Object.keys(opt)) {
const sc = si.config;
//@ts-ignore: test
assertEquals(sc[v], opt[v]);
}
if (shouldFail) {
fail("expected to fail with update: " + JSON.stringify(opt));
}
} catch (err) {
if (!shouldFail) {
fail((err as Error).message);
}
}
}
await updateOption({ name: "nn" }, true);
await updateOption({ retention: RetentionPolicy.Interest }, true);
await updateOption({ storage: StorageType.Memory }, true);
await updateOption({ max_consumers: 5 }, true);
await updateOption({ subjects: ["subj", "a"] });
await updateOption({ description: "xx" });
await updateOption({ max_msgs_per_subject: 5 });
await updateOption({ max_msgs: 100 });
await updateOption({ max_age: nanos(45 * 1000) });
await updateOption({ max_bytes: 10240 });
await updateOption({ max_msg_size: 10240 });
await updateOption({ discard: DiscardPolicy.New });
await updateOption({ no_ack: true });
await updateOption({ duplicate_window: nanos(15 * 1000) });
await updateOption({ allow_rollup_hdrs: true });
await updateOption({ allow_rollup_hdrs: false });
// stream update allows the replica count to be bumped
// this is possibly by design?
await updateOption({ num_replicas: 3 });
await updateOption({ num_replicas: 1 });
await updateOption({ deny_delete: true });
await updateOption({ deny_purge: true });
await updateOption({ sources: [{ name: "a" }] });
await updateOption({ sealed: true });
await updateOption({ sealed: false }, true);
await jsm.streams.add({ name: "m", mirror: { name: "a" } });
await updateOption({ mirror: { name: "nn" } }, true);
await cleanup(ns, nc);
});
Deno.test(
"streams - mirrors",
flakyTest(async () => {
const cluster = await NatsServer.jetstreamCluster(3);
const nc = await connect({ port: cluster[0].port });
const jsm = await jetstreamManager(nc);
// create a stream in a different server in the cluster
await jsm.streams.add({
name: "src",
subjects: ["src.*"],
placement: {
cluster: cluster[1].config.cluster.name,
tags: cluster[1].config.server_tags,
},
});
// create a mirror in the server we connected
await jsm.streams.add({
name: "mirror",
placement: {
cluster: cluster[2].config.cluster.name,
tags: cluster[2].config.server_tags,
},
mirror: {
name: "src",
},
});
const js = jetstream(nc);
const s = await js.streams.get("src");
assertExists(s);
assertEquals(s.name, "src");
// Wait for mirror to be registered and appear in alternates
let alternates: Awaited<ReturnType<typeof s.alternates>> = [];
for (let i = 0; i < 10; i++) {
alternates = await s.alternates();
if (alternates.length === 2) break;
await new Promise((resolve) => setTimeout(resolve, 100));
}
assertEquals(2, alternates.length);
assertArrayIncludes(alternates.map((a) => a.name), ["src", "mirror"]);
await assertRejects(
async () => {
await js.streams.get("another");
},
Error,
"stream not found",
);
const s2 = await s.best();
const selected = (await s.info(true)).alternates?.[0]?.name ?? "";
assertEquals(s2.name, selected);
await nc.close();
await NatsServer.stopAll(cluster, true);
}),
);
--- jetstream/tests/jsm_direct_test.ts ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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 {
assertArrayIncludes,
assertEquals,
assertExists,
assertIsError,
assertRejects,
fail,
} from "@std/assert";
import { deferred, delay } from "@nats-io/nats-core";
import {
jetstream,
JetStreamError,
jetstreamManager,
StorageType,
type StoredMsg,
} from "../src/mod.ts";
import {
cleanup,
jetstreamServerConf,
notCompatible,
notSupported,
setup,
} from "test_helpers";
import { JetStreamStatusError } from "../src/jserrors.ts";
import type { JetStreamManagerImpl } from "../src/jsclient.ts";
import type { DirectBatchOptions, DirectLastFor } from "../src/jsapi_types.ts";
import type {
NatsConnectionImpl,
QueuedIteratorImpl,
} from "@nats-io/nats-core/internal";
Deno.test("direct - version checks", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
assertExists(nc.info);
nc.info.version = "2.0.0";
(nc as NatsConnectionImpl).features.update("2.0.0");
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await assertRejects(
() => {
return jsm.direct.getMessage("A", { start_time: new Date() });
},
Error,
"start_time direct option require server 2.11.0",
);
await assertRejects(
() => {
return jsm.direct.getBatch(
"A",
{ seq: 1, batch: 100 },
);
},
Error,
"batch direct require server 2.11.0",
);
await assertRejects(
() => {
return jsm.direct.getBatch(
"A",
{ seq: 1, batch: 100 },
);
},
Error,
"batch direct require server 2.11.0",
);
await cleanup(ns, nc);
});
Deno.test("direct - decoder", async (t) => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a", "b"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
await js.publish("a", "hello world");
await js.publish("b", JSON.stringify({ hello: "world" }));
await t.step("string", async () => {
const m = await jsm.direct.getMessage("A", { seq: 1 });
assertExists(m);
assertEquals(m.string(), "hello world");
});
await t.step("json", async () => {
const m = await jsm.direct.getMessage("A", { seq: 2 });
assertExists(m);
assertEquals(m.json(), { hello: "world" });
});
await cleanup(ns, nc);
});
Deno.test("direct - get", async (t) => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>", "b.>", "z.a"],
storage: StorageType.File,
allow_direct: true,
});
const js = jetstream(nc);
await Promise.all([
js.publish(`a.1`, "<payload>"),
js.publish(`b.1`, "<payload>"),
]);
await delay(1000);
await Promise.all([
js.publish(`z.a`, new Uint8Array(15)),
js.publish(`a.2`, "<payload>"),
js.publish(`b.2`, "<payload>"),
js.publish(`z.a`, new Uint8Array(15)),
js.publish(`a.3`, "<payload>"),
js.publish(`b.3`, "<payload>"),
js.publish(`z.a`, new Uint8Array(15)),
js.publish(`a.4`, "<payload>"),
js.publish(`b.4`, "<payload>"),
js.publish(`z.a`, new Uint8Array(15)),
js.publish(`a.5`, "<payload>"),
js.publish(`b.5`, "<payload>"),
js.publish(`z.a`, new Uint8Array(15)),
js.publish(`a.6`, "<payload>"),
js.publish(`b.6`, "<payload>"),
js.publish(`z.a`, new Uint8Array(15)),
js.publish(`a.7`, "<payload>"),
js.publish(`b.7`, "<payload>"),
js.publish(`z.a`, new Uint8Array(15)),
js.publish(`a.8`, "<payload>"),
js.publish(`b.8`, "<payload>"),
js.publish(`z.a`, new Uint8Array(15)),
]);
// const c = await js.consumers.get("A");
// const iter = await c.fetch({ max_messages: 24 });
// for await (const m of iter) {
// console.log(m.seq, m.subject);
// }
await assertRejects(
() => {
return jsm.direct.getMessage("A", { seq: 0 });
},
JetStreamError,
"empty request",
);
await t.step("seq", async () => {
const m = await jsm.direct.getMessage("A", { seq: 1 });
assertExists(m);
assertEquals(m.seq, 1);
assertEquals(m.subject, "a.1");
});
await t.step("first with subject", async () => {
const m = await jsm.direct.getMessage("A", { next_by_subj: "z.a" });
assertExists(m);
assertEquals(m.seq, 3);
});
await t.step("next with subject from sequence", async () => {
const m = await jsm.direct.getMessage("A", { seq: 4, next_by_subj: "z.a" });
assertExists(m);
assertEquals(m.seq, 6);
});
await t.step("start_time", async () => {
if (await notSupported(ns, "2.11.0")) {
return Promise.resolve();
}
const start_time = (await jsm.direct.getMessage("A", { seq: 3 }))?.time;
assertExists(start_time);
const m = await jsm.direct.getMessage("A", { start_time });
assertExists(m);
assertEquals(m?.seq, 3);
assertEquals(m?.subject, "z.a");
});
await t.step("last_by_subject", async () => {
const m = await jsm.direct.getMessage("A", { last_by_subj: "z.a" });
assertExists(m);
assertEquals(m.seq, 24);
assertEquals(m.subject, "z.a");
});
await cleanup(ns, nc);
});
Deno.test("direct - callback", async (t) => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const nci = nc as NatsConnectionImpl;
const jsm = await jetstreamManager(nci) as JetStreamManagerImpl;
await t.step("no stream", async () => {
const d = deferred();
const iter = await jsm.direct.getBatch("hello", {
seq: 1,
callback: (done, _) => {
assertExists(done);
assertIsError(done.err, Error, "no responders");
d.resolve();
},
}) as QueuedIteratorImpl<StoredMsg>;
const err = await iter.iterClosed;
assertIsError(err, Error, "no responders");
});
await t.step("message not found", async () => {
await jsm.streams.add({
name: "empty",
subjects: ["empty"],
storage: StorageType.Memory,
allow_direct: true,
});
const iter = await jsm.direct.getBatch("empty", {
//@ts-ignore: test
seq: 1,
callback: (done, _) => {
assertExists(done);
assertIsError(done.err, JetStreamStatusError, "message not found");
},
}) as QueuedIteratorImpl<StoredMsg>;
const err = await iter.iterClosed;
assertIsError(err, JetStreamStatusError, "message not found");
});
await t.step("6 messages", async () => {
await jsm.streams.add({
name: "A",
subjects: ["a.*"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
await Promise.all([
js.publish("a.a"),
js.publish("a.b"),
js.publish("a.c"),
]);
const buf: StoredMsg[] = [];
const iter = await jsm.direct.getBatch("A", {
batch: 10,
seq: 1,
callback: (done, sm) => {
if (done) {
if (done.err) {
console.log(done.err);
fail(done.err.message);
}
return;
}
buf.push(sm);
},
}) as QueuedIteratorImpl<StoredMsg>;
const err = await iter.iterClosed;
assertEquals(err, undefined);
const subj = buf.map((m) => m.subject);
assertEquals(subj.length, 3);
assertEquals(subj[0], "a.a");
assertEquals(subj[1], "a.b");
assertEquals(subj[2], "a.c");
});
await cleanup(ns, nc);
});
Deno.test("direct - batch", async (t) => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const nci = nc as NatsConnectionImpl;
const jsm = await jetstreamManager(nci) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
const d = deferred();
let i = 0;
const timer = setInterval(async () => {
i++;
await js.publish(`a.a`, new Uint8Array(i));
if (i === 8) {
clearInterval(timer);
d.resolve();
}
}, 250);
await d;
type tt = {
opts: DirectBatchOptions;
expect: number[];
};
async function assertBatch(tc: tt, debug = false): Promise<void> {
if (debug) {
nci.options.debug = true;
}
const iter = await jsm.direct.getBatch("A", tc.opts);
const buf: number[] = [];
for await (const m of iter) {
buf.push(m.seq);
}
if (debug) {
nci.options.debug = false;
}
assertArrayIncludes(buf, tc.expect);
assertEquals(buf.length, tc.expect.length);
}
async function getDateFor(seq: number): Promise<Date> {
const m = await jsm.direct.getMessage("A", { seq: seq });
assertExists(m);
assertEquals(m.seq, seq);
return m.time;
}
await t.step("fails without any option in addition to batch", async () => {
await assertRejects(
() => {
return assertBatch({
opts: {
batch: 3,
} as DirectBatchOptions,
expect: [],
});
},
JetStreamError,
"empty request",
);
});
await t.step("start sequence", () => {
return assertBatch({
//@ts-ignore: test
opts: {
batch: 3,
seq: 3,
},
expect: [3, 4, 5],
});
});
await t.step("start sequence are mutually exclusive start_time", async () => {
const start_time = await getDateFor(3);
await assertRejects(
() => {
return assertBatch({
//@ts-ignore: test
opts: {
seq: 100,
start_time,
},
expect: [3, 4, 5],
});
},
JetStreamError,
"bad request",
);
});
await t.step("start_time", async () => {
const start_time = await getDateFor(3);
await assertBatch({
//@ts-ignore: test
opts: {
start_time,
batch: 10,
},
expect: [3, 4, 5, 6, 7, 8],
});
});
await t.step("max_bytes", async () => {
await assertBatch({
//@ts-ignore: test
opts: {
seq: 1,
max_bytes: 4,
},
expect: [1],
});
});
await cleanup(ns, nc);
});
Deno.test("direct - last message for", async (t) => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const nci = nc as NatsConnectionImpl;
const jsm = await jetstreamManager(nci) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a", "b", "z"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
await Promise.all([
js.publish("a", "1"),
js.publish("a", "2"),
js.publish("a", "last a"),
js.publish("b", "1"),
]);
await delay(100);
await Promise.all([
js.publish("b", "last b"),
js.publish("z", "last z"),
]);
type tt = {
opts: DirectLastFor;
expect: number[];
};
async function assertBatch(tc: tt, debug = false): Promise<void> {
if (debug) {
nci.options.debug = true;
}
const iter = await jsm.direct.getLastMessagesFor("A", tc.opts);
const buf: number[] = [];
for await (const m of iter) {
buf.push(m.seq);
}
if (debug) {
nci.options.debug = false;
}
assertArrayIncludes(buf, tc.expect);
assertEquals(buf.length, tc.expect.length);
}
async function getDateFor(seq: number): Promise<Date> {
const m = await jsm.direct.getMessage("A", { seq: seq });
assertExists(m);
assertEquals(m.seq, seq);
return m.time;
}
await t.step("not matched filter", async () => {
await assertBatch({ opts: { multi_last: ["c"] }, expect: [] });
});
await t.step("single filter", async () => {
await assertBatch({ opts: { multi_last: ["a"] }, expect: [3] });
});
await t.step("multiple filter", async () => {
await assertBatch({
opts: { multi_last: ["a", "b", "z"] },
expect: [3, 5, 6],
});
});
await t.step("up_to_time", async () => {
const up_to_time = await getDateFor(5);
await assertBatch(
{ opts: { up_to_time, multi_last: ["a", "b", "z"] }, expect: [3, 4] },
);
});
await t.step("up_to_seq", async () => {
await assertBatch(
{ opts: { up_to_seq: 4, multi_last: ["a", "b", "z"] }, expect: [3, 4] },
);
});
await cleanup(ns, nc);
});
Deno.test("direct - batch next_by_subj", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const nci = nc as NatsConnectionImpl;
const jsm = await jetstreamManager(nci) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a", "b"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jsm.jetstream();
const buf = [];
for (let i = 0; i < 50; i++) {
buf.push(js.publish(`a`), buf.push(js.publish(`b`)));
}
await Promise.all(buf);
const msgs = [];
let iter = await jsm.direct.getBatch("A", {
seq: 0,
batch: 100,
next_by_subj: "a",
});
for await (const m of iter) {
msgs.push(m.subject);
}
assertEquals(msgs.length, 50);
for (let i = 0; i < 50; i++) {
assertEquals(msgs[i], "a");
}
msgs.length = 0;
iter = await jsm.direct.getBatch("A", {
seq: 50,
batch: 100,
next_by_subj: "b",
});
for await (const m of iter) {
msgs.push(m.subject);
}
assertEquals(msgs.length, 26);
for (let i = 0; i < 26; i++) {
assertEquals(msgs[i], "b");
}
await cleanup(ns, nc);
});
Deno.test("direct - batch no messages", async () => {
const { ns, nc } = await setup(jetstreamServerConf(), { debug: true });
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const nci = nc as NatsConnectionImpl;
const jsm = await jetstreamManager(nci) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a", "b", "z"],
storage: StorageType.Memory,
allow_direct: true,
});
const iter = await jsm.direct.getLastMessagesFor("A", {
multi_last: ["a", "b", "z"],
batch: 100,
});
for await (const m of iter) {
console.log(m.seq);
}
assertEquals(iter.getProcessed(), 0);
await cleanup(ns, nc);
});
Deno.test("direct - get no messages", async () => {
const { ns, nc } = await setup(jetstreamServerConf(), { debug: true });
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const nci = nc as NatsConnectionImpl;
const jsm = await jetstreamManager(nci) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a", "b", "z"],
storage: StorageType.Memory,
allow_direct: true,
});
const m = await jsm.direct.getMessage("A", { last_by_subj: "a" });
assertEquals(m, null);
await cleanup(ns, nc);
});
--- jetstream/tests/jsm_test.ts ---
/*
* Copyright 2021-2024 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,
assertArrayIncludes,
assertEquals,
assertExists,
assertRejects,
assertThrows,
fail,
} from "@std/assert";
import type { NatsConnectionImpl } from "@nats-io/nats-core/internal";
import { Feature } from "@nats-io/nats-core/internal";
import type { NatsConnection } from "@nats-io/nats-core";
import {
deferred,
delay,
Empty,
errors,
headers,
InvalidArgumentError,
jwtAuthenticator,
nanos,
nkeys,
nuid,
} from "@nats-io/nats-core";
import type {
ConsumerConfig,
ConsumerInfo,
Lister,
PubAck,
StreamConfig,
StreamInfo,
StreamSource,
} from "../src/mod.ts";
import {
AckPolicy,
AdvisoryKind,
DiscardPolicy,
jetstream,
jetstreamManager,
StorageType,
} from "../src/mod.ts";
import { initStream } from "./jstest_util.ts";
import {
cleanup,
connect,
flakyTest,
jetstreamExportServerConf,
jetstreamServerConf,
NatsServer,
notCompatible,
setup,
} from "test_helpers";
import { validateName } from "../src/jsutil.ts";
import { encodeAccount, encodeOperator, encodeUser } from "@nats-io/jwt";
import { convertStreamSourceDomain } from "../src/jsmstream_api.ts";
import type { ConsumerAPIImpl } from "../src/jsmconsumer_api.ts";
import {
ConsumerApiAction,
PriorityPolicy,
StoreCompression,
} from "../src/jsapi_types.ts";
import type { JetStreamManagerImpl } from "../src/jsclient.ts";
import { stripNatsMetadata } from "./util.ts";
import { jserrors } from "../src/jserrors.ts";
import type { WithRequired } from "../../core/src/util.ts";
import { assertBetween } from "../../test_helpers/mod.ts";
const StreamNameRequired = "stream name required";
const ConsumerNameRequired = "durable name required";
Deno.test("jsm - jetstream not enabled", async () => {
// start a regular server - no js conf
const { ns, nc } = await setup();
await assertRejects(
() => {
return jetstreamManager(nc);
},
jserrors.JetStreamNotEnabled,
);
await cleanup(ns, nc);
});
Deno.test("jsm - account info", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const ai = await jsm.getAccountInfo();
assert(ai.limits.max_memory === -1 || ai.limits.max_memory > 0);
await cleanup(ns, nc);
});
Deno.test("jsm - account not enabled", async () => {
const conf = {
"no_auth_user": "b",
accounts: {
A: {
jetstream: "enabled",
users: [{ user: "a", password: "a" }],
},
B: {
users: [{ user: "b" }],
},
},
};
const { ns, nc } = await setup(jetstreamServerConf(conf));
await assertRejects(
() => {
return jetstreamManager(nc);
},
jserrors.JetStreamNotEnabled,
);
const a = await connect(
{ port: ns.port, user: "a", pass: "a" },
);
await jetstreamManager(a);
await a.close();
await cleanup(ns, nc);
});
Deno.test("jsm - empty stream config fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.add({} as StreamConfig);
},
Error,
StreamNameRequired,
undefined,
);
await cleanup(ns, nc);
});
Deno.test("jsm - empty stream config update fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const name = nuid.next();
let ci = await jsm.streams.add({ name: name, subjects: [`${name}.>`] });
assertEquals(ci!.config!.subjects!.length, 1);
await assertRejects(
async () => {
await jsm.streams.update("", {} as StreamConfig);
},
Error,
StreamNameRequired,
undefined,
);
ci!.config!.subjects!.push("foo");
ci = await jsm.streams.update(name, ci.config);
assertEquals(ci!.config!.subjects!.length, 2);
await cleanup(ns, nc);
});
Deno.test("jsm - update stream name is internally added", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const name = nuid.next();
const ci = await jsm.streams.add({
name: name,
subjects: [`${name}.>`],
});
assertEquals(ci!.config!.subjects!.length, 1);
const si = await jsm.streams.update(name, { subjects: [`${name}.>`, "foo"] });
assertEquals(si!.config!.subjects!.length, 2);
await cleanup(ns, nc);
});
Deno.test("jsm - delete empty stream name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.delete("");
},
Error,
StreamNameRequired,
undefined,
);
await cleanup(ns, nc);
});
Deno.test("jsm - info empty stream name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.info("");
},
Error,
StreamNameRequired,
undefined,
);
await cleanup(ns, nc);
});
Deno.test("jsm - info msg not found stream name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await assertRejects(
async () => {
await jsm.streams.info(name);
},
jserrors.StreamNotFoundError,
);
await cleanup(ns, nc);
});
Deno.test("jsm - delete msg empty stream name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.deleteMessage("", 1);
},
Error,
StreamNameRequired,
undefined,
);
await cleanup(ns, nc);
});
Deno.test("jsm - delete msg not found stream name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await assertRejects(
async () => {
await jsm.streams.deleteMessage(name, 1);
},
jserrors.StreamNotFoundError,
);
await cleanup(ns, nc);
});
Deno.test("jsm - no stream lister is empty", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const streams = await jsm.streams.list().next();
assertEquals(streams.length, 0);
await cleanup(ns, nc);
});
Deno.test("jsm - stream names is empty", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const names = await jsm.streams.names().next();
assertEquals(names.length, 0);
await cleanup(ns, nc);
});
Deno.test("jsm - lister after empty, empty", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const lister = jsm.streams.list();
let streams = await lister.next();
assertEquals(streams.length, 0);
streams = await lister.next();
assertEquals(streams.length, 0);
await cleanup(ns, nc);
});
Deno.test("jsm - add stream", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const name = nuid.next();
let si = await jsm.streams.add({ name });
assertEquals(si.config.name, name);
const fn = (i: StreamInfo): boolean => {
stripNatsMetadata(si.config.metadata);
stripNatsMetadata(i.config.metadata);
assertEquals(i.config, si.config);
assertEquals(i.state, si.state);
assertEquals(i.created, si.created);
return true;
};
fn(await jsm.streams.info(name));
let lister = await jsm.streams.list().next();
fn(lister[0]);
// add some data
await jetstream(nc).publish(name, Empty);
si = await jsm.streams.info(name);
lister = await jsm.streams.list().next();
fn(lister[0]);
await cleanup(ns, nc);
});
Deno.test("jsm - purge not found stream name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await assertRejects(
async () => {
await jsm.streams.purge(name);
},
jserrors.StreamNotFoundError,
);
await cleanup(ns, nc);
});
Deno.test("jsm - purge empty stream name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.purge("");
},
Error,
StreamNameRequired,
undefined,
);
await cleanup(ns, nc);
});
Deno.test("jsm - stream purge", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await jetstream(nc).publish(subj, Empty);
let si = await jsm.streams.info(stream);
assertEquals(si.state.messages, 1);
await jsm.streams.purge(stream);
si = await jsm.streams.info(stream);
assertEquals(si.state.messages, 0);
await cleanup(ns, nc);
});
Deno.test("jsm - purge by sequence", 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 Promise.all([
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
]);
const pi = await jsm.streams.purge(stream, { seq: 4 });
assertEquals(pi.purged, 3);
const si = await jsm.streams.info(stream);
assertEquals(si.state.first_seq, 4);
await cleanup(ns, nc);
});
Deno.test("jsm - purge by filtered sequence", 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 Promise.all([
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
]);
const pi = await jsm.streams.purge(stream, { seq: 4, filter: `${stream}.b` });
assertEquals(pi.purged, 1);
const si = await jsm.streams.info(stream);
assertEquals(si.state.first_seq, 1);
assertEquals(si.state.messages, 8);
await cleanup(ns, nc);
});
Deno.test("jsm - purge 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 Promise.all([
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
]);
const pi = await jsm.streams.purge(stream, { filter: `${stream}.b` });
assertEquals(pi.purged, 3);
const si = await jsm.streams.info(stream);
assertEquals(si.state.first_seq, 1);
assertEquals(si.state.messages, 6);
await cleanup(ns, nc);
});
Deno.test("jsm - purge 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 Promise.all([
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
]);
const pi = await jsm.streams.purge(stream, { filter: `${stream}.b` });
assertEquals(pi.purged, 3);
const si = await jsm.streams.info(stream);
assertEquals(si.state.first_seq, 1);
assertEquals(si.state.messages, 6);
await cleanup(ns, nc);
});
Deno.test("jsm - purge keep", 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 Promise.all([
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
]);
const pi = await jsm.streams.purge(stream, { keep: 1 });
assertEquals(pi.purged, 8);
const si = await jsm.streams.info(stream);
assertEquals(si.state.first_seq, 9);
assertEquals(si.state.messages, 1);
await cleanup(ns, nc);
});
Deno.test("jsm - purge filtered keep", 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 Promise.all([
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
js.publish(`${stream}.a`),
js.publish(`${stream}.b`),
js.publish(`${stream}.c`),
]);
let pi = await jsm.streams.purge(stream, { keep: 1, filter: `${stream}.a` });
assertEquals(pi.purged, 2);
pi = await jsm.streams.purge(stream, { keep: 1, filter: `${stream}.b` });
assertEquals(pi.purged, 2);
pi = await jsm.streams.purge(stream, { keep: 1, filter: `${stream}.c` });
assertEquals(pi.purged, 2);
const si = await jsm.streams.info(stream);
assertEquals(si.state.first_seq, 7);
assertEquals(si.state.messages, 3);
await cleanup(ns, nc);
});
Deno.test("jsm - purge seq and keep fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
() => {
return jsm.streams.purge("a", { keep: 10, seq: 5 });
},
Error,
"'keep','seq' are mutually exclusive",
);
await cleanup(ns, nc);
});
Deno.test("jsm - stream delete", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await jetstream(nc).publish(subj, Empty);
await jsm.streams.delete(stream);
await assertRejects(
async () => {
await jsm.streams.info(stream);
},
jserrors.StreamNotFoundError,
);
await cleanup(ns, nc);
});
Deno.test("jsm - stream delete message", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await jetstream(nc).publish(subj, Empty);
let si = await jsm.streams.info(stream);
assertEquals(si.state.messages, 1);
assertEquals(si.state.first_seq, 1);
assertEquals(si.state.last_seq, 1);
assert(await jsm.streams.deleteMessage(stream, 1));
si = await jsm.streams.info(stream);
assertEquals(si.state.messages, 0);
assertEquals(si.state.first_seq, 2);
assertEquals(si.state.last_seq, 1);
await cleanup(ns, nc);
});
Deno.test("jsm - stream delete info", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const { stream, subj } = await initStream(nc);
const jsm = await jetstreamManager(nc);
const js = jetstream(nc);
await js.publish(subj);
await js.publish(subj);
await js.publish(subj);
await jsm.streams.deleteMessage(stream, 2);
const si = await jsm.streams.info(stream, { deleted_details: true });
assertEquals(si.state.num_deleted, 1);
assertEquals(si.state.deleted, [2]);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer info on empty stream name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.consumers.info("", "");
},
Error,
StreamNameRequired,
undefined,
);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer info on empty consumer name fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.consumers.info("foo", "");
},
Error,
ConsumerNameRequired,
undefined,
);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer info on not found stream fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.consumers.info("foo", "dur");
},
jserrors.StreamNotFoundError,
);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer info on not found consumer", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.consumers.info(stream, "dur");
},
jserrors.ConsumerNotFoundError,
);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer info", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(
stream,
{ durable_name: "dur", ack_policy: AckPolicy.Explicit },
);
const ci = await jsm.consumers.info(stream, "dur");
assertEquals(ci.name, "dur");
assertEquals(ci.config.durable_name, "dur");
assertEquals(ci.config.ack_policy, "explicit");
await cleanup(ns, nc);
});
Deno.test("jsm - no consumer lister with empty stream fails", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
assertThrows(
() => {
jsm.consumers.list("");
},
Error,
StreamNameRequired,
);
await cleanup(ns, nc);
});
Deno.test("jsm - no consumer lister with no consumers empty", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
const consumers = await jsm.consumers.list(stream).next();
assertEquals(consumers.length, 0);
await cleanup(ns, nc);
});
Deno.test("jsm - lister", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(
stream,
{ durable_name: "dur", ack_policy: AckPolicy.Explicit },
);
let consumers = await jsm.consumers.list(stream).next();
assertEquals(consumers.length, 1);
assertEquals(consumers[0].config.durable_name, "dur");
await jsm.consumers.delete(stream, "dur");
consumers = await jsm.consumers.list(stream).next();
assertEquals(consumers.length, 0);
await cleanup(ns, nc);
});
Deno.test("jsm - update stream", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
let si = await jsm.streams.info(stream);
assertEquals(si.config!.subjects!.length, 1);
si.config!.subjects!.push("foo");
si = await jsm.streams.update(stream, si.config);
assertEquals(si.config!.subjects!.length, 2);
await cleanup(ns, nc);
});
Deno.test("jsm - get message", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const h = headers();
h.set("xxx", "a");
const js = jetstream(nc);
await js.publish(subj, JSON.stringify(1), { headers: h });
await js.publish(subj, JSON.stringify(2));
const jsm = await jetstreamManager(nc);
let sm = await jsm.streams.getMessage(stream, { seq: 1 });
assertExists(sm);
assertEquals(sm.subject, subj);
assertEquals(sm.seq, 1);
assertEquals(sm.json<number>(), 1);
sm = await jsm.streams.getMessage(stream, { seq: 2 });
assertExists(sm);
assertEquals(sm.subject, subj);
assertEquals(sm.seq, 2);
assertEquals(sm.json<number>(), 2);
assertEquals(await jsm.streams.getMessage(stream, { seq: 3 }), null);
await cleanup(ns, nc);
});
Deno.test("jsm - get message (not found)", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.streams.getMessage("A", { last_by_subj: "a" });
await cleanup(ns, nc);
});
Deno.test("jsm - get message payload", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const { stream, subj } = await initStream(nc);
const js = jetstream(nc);
await js.publish(subj, Empty, { msgID: "empty" });
await js.publish(subj, "", { msgID: "empty2" });
const jsm = await jetstreamManager(nc);
let sm = await jsm.streams.getMessage(stream, { seq: 1 });
assertExists(sm);
assertEquals(sm.subject, subj);
assertEquals(sm.seq, 1);
assertEquals(sm.data, Empty);
sm = await jsm.streams.getMessage(stream, { seq: 2 });
assertExists(sm);
assertEquals(sm.subject, subj);
assertEquals(sm.seq, 2);
assertEquals(sm.data, Empty);
assertEquals(sm.string(), "");
await cleanup(ns, nc);
});
Deno.test("jsm - advisories", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const iter = jsm.advisories();
const streamAction = deferred();
(async () => {
for await (const a of iter) {
if (a.kind === AdvisoryKind.StreamAction) {
streamAction.resolve();
}
}
})().then();
await initStream(nc);
await streamAction;
await cleanup(ns, nc);
});
Deno.test("jsm - validate name", () => {
type t = [string, boolean];
const tests: t[] = [
["", false],
[".", false],
["*", false],
[">", false],
["hello.", false],
["hello.*", false],
["hello.>", false],
["one.two", false],
["one*two", false],
["one>two", false],
["stream", true],
];
tests.forEach((v, idx) => {
try {
validateName(`${idx}`, v[0]);
if (!v[1]) {
fail(`${v[0]} should have been rejected`);
}
} catch (_err) {
if (v[1]) {
fail(`${v[0]} should have been valid`);
}
}
});
});
Deno.test("jsm - minValidation", () => {
type t = [string, boolean];
const tests: t[] = [
["", false],
[".", false],
["*", false],
[">", false],
["hello.", false],
["hello\r", false],
["hello\n", false],
["hello\t", false],
["hello ", false],
["hello.*", false],
["hello.>", false],
["one.two", false],
["one*two", false],
["one>two", false],
["stream", true],
];
tests.forEach((v, idx) => {
try {
validateName(`${idx}`, v[0]);
if (!v[1]) {
fail(`${v[0]} should have been rejected`);
}
} catch (_err) {
if (v[1]) {
fail(`${v[0]} should have been valid`);
}
}
});
});
Deno.test("jsm - cross account streams", async () => {
const { ns, nc } = await setup(jetstreamExportServerConf(), {
user: "a",
pass: "s3cret",
});
const sawIPA = deferred();
nc.subscribe("IPA.>", {
callback: () => {
sawIPA.resolve();
},
max: 1,
});
const jsm = await jetstreamManager(nc, { apiPrefix: "IPA" });
await sawIPA;
// no streams
let streams = await jsm.streams.list().next();
assertEquals(streams.length, 0);
// add a stream
const stream = nuid.next();
const subj = `${stream}.A`;
await jsm.streams.add({ name: stream, subjects: [subj] });
// list the stream
streams = await jsm.streams.list().next();
assertEquals(streams.length, 1);
// cannot publish to the stream from the client account
// publish from the js account
const admin = await connect({ port: ns.port, user: "js", pass: "js" });
admin.publish(subj);
admin.publish(subj);
await admin.flush();
// info
let si = await jsm.streams.info(stream);
assertEquals(si.state.messages, 2);
// get message
const sm = await jsm.streams.getMessage(stream, { seq: 1 });
assertEquals(sm?.seq, 1);
// delete message
let ok = await jsm.streams.deleteMessage(stream, 1);
assertEquals(ok, true);
si = await jsm.streams.info(stream);
assertEquals(si.state.messages, 1);
// purge
const pr = await jsm.streams.purge(stream);
assertEquals(pr.success, true);
assertEquals(pr.purged, 1);
si = await jsm.streams.info(stream);
assertEquals(si.state.messages, 0);
// update
const config = streams[0].config as StreamConfig;
config.subjects!.push(`${stream}.B`);
si = await jsm.streams.update(config.name, config);
assertEquals(si.config.subjects!.length, 2);
// find
const sn = await jsm.streams.find(`${stream}.B`);
assertEquals(sn, stream);
// delete
ok = await jsm.streams.delete(stream);
assertEquals(ok, true);
streams = await jsm.streams.list().next();
assertEquals(streams.length, 0);
await cleanup(ns, nc, admin);
});
Deno.test(
"jsm - cross account consumers",
flakyTest(async () => {
const { ns, nc } = await setup(jetstreamExportServerConf(), {
user: "a",
pass: "s3cret",
});
const sawIPA = deferred();
nc.subscribe("IPA.>", {
callback: () => {
sawIPA.resolve();
},
max: 1,
});
const jsm = await jetstreamManager(nc, { apiPrefix: "IPA" });
await sawIPA;
// add a stream
const stream = nuid.next();
const subj = `${stream}.A`;
await jsm.streams.add({ name: stream, subjects: [subj] });
let consumers = await jsm.consumers.list(stream).next();
assertEquals(consumers.length, 0);
await jsm.consumers.add(stream, {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
});
// cannot publish to the stream from the client account
// publish from the js account
const admin = await connect({ port: ns.port, user: "js", pass: "js" });
admin.publish(subj);
admin.publish(subj);
await admin.flush();
consumers = await jsm.consumers.list(stream).next();
assertEquals(consumers.length, 1);
assertEquals(consumers[0].name, "me");
assertEquals(consumers[0].config.durable_name, "me");
assertEquals(consumers[0].num_pending, 2);
const ci = await jsm.consumers.info(stream, "me");
assertEquals(ci.name, "me");
assertEquals(ci.config.durable_name, "me");
assertEquals(ci.num_pending, 2);
const ok = await jsm.consumers.delete(stream, "me");
assertEquals(ok, true);
await assertRejects(
async () => {
await jsm.consumers.info(stream, "me");
},
Error,
"consumer not found",
undefined,
);
await cleanup(ns, nc, admin);
}),
);
Deno.test("jsm - jetstream error info", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await assertRejects(
() => {
return jsm.streams.add(
{
name: "a",
num_replicas: 3,
subjects: ["a.>"],
},
);
},
jserrors.JetStreamApiError,
"replicas > 1 not supported in non-clustered mode",
);
await cleanup(ns, nc);
});
Deno.test("jsm - update consumer", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.4")) {
return;
}
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(stream, {
durable_name: "dur",
ack_policy: AckPolicy.Explicit,
ack_wait: nanos(2000),
max_ack_pending: 500,
headers_only: false,
max_deliver: 100,
});
// update is simply syntatic sugar for add providing a type to
// help the IDE show editable properties - server will still
// reject options it doesn't deem editable
const ci = await jsm.consumers.update(stream, "dur", {
ack_wait: nanos(3000),
max_ack_pending: 5,
headers_only: true,
max_deliver: 2,
});
assertEquals(ci.config.ack_wait, nanos(3000));
assertEquals(ci.config.max_ack_pending, 5);
assertEquals(ci.config.headers_only, true);
assertEquals(ci.config.max_deliver, 2);
await cleanup(ns, nc);
});
Deno.test("jsm - stream info subjects", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.7.2")) {
return;
}
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await jsm.streams.add({ name, subjects: [`${name}.>`] });
const js = jetstream(nc);
await js.publish(`${name}.a`);
await js.publish(`${name}.a.b`);
await js.publish(`${name}.a.b.c`);
let si = await jsm.streams.info(name, { subjects_filter: `>` });
assertEquals(si.state.num_subjects, 3);
assert(si.state.subjects);
assertEquals(Object.keys(si.state.subjects).length, 3);
assertEquals(si.state.subjects[`${name}.a`], 1);
assertEquals(si.state.subjects[`${name}.a.b`], 1);
assertEquals(si.state.subjects[`${name}.a.b.c`], 1);
si = await jsm.streams.info(name, { subjects_filter: `${name}.a.>` });
assertEquals(si.state.num_subjects, 3);
assert(si.state.subjects);
assertEquals(Object.keys(si.state.subjects).length, 2);
assertEquals(si.state.subjects[`${name}.a.b`], 1);
assertEquals(si.state.subjects[`${name}.a.b.c`], 1);
si = await jsm.streams.info(name);
assertEquals(si.state.subjects, undefined);
await cleanup(ns, nc);
});
Deno.test("jsm - account limits", async () => {
const O = nkeys.createOperator();
const SYS = nkeys.createAccount();
const A = nkeys.createAccount();
const resolver: Record<string, string> = {};
resolver[A.getPublicKey()] = await encodeAccount("A", A, {
limits: {
conn: -1,
subs: -1,
tiered_limits: {
R1: {
disk_storage: 1024 * 1024,
consumer: -1,
streams: -1,
},
},
},
}, { signer: O });
resolver[SYS.getPublicKey()] = await encodeAccount("SYS", SYS, {
limits: {
conn: -1,
subs: -1,
},
}, { signer: O });
const conf = {
operator: await encodeOperator("O", O, {
system_account: SYS.getPublicKey(),
}),
resolver: "MEMORY",
"resolver_preload": resolver,
};
const ns = await NatsServer.start(jetstreamServerConf(conf));
const U = nkeys.createUser();
const ujwt = await encodeUser("U", U, A, { bearer_token: true });
const nc = await connect({
port: ns.port,
maxReconnectAttempts: -1,
authenticator: jwtAuthenticator(ujwt),
});
const jsm = await jetstreamManager(nc);
const ai = await jsm.getAccountInfo();
assertEquals(ai.tiers?.R1?.limits.max_storage, 1024 * 1024);
assertEquals(ai.tiers?.R1?.limits.max_consumers, -1);
assertEquals(ai.tiers?.R1?.limits.max_streams, -1);
assertEquals(ai.tiers?.R1?.limits.max_ack_pending, -1);
await cleanup(ns, nc);
});
Deno.test("jsm - stream update preserves other value", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "a",
storage: StorageType.File,
discard: DiscardPolicy.New,
subjects: ["x"],
});
const si = await jsm.streams.update("a", { subjects: ["x", "y"] });
assertEquals(si.config.discard, DiscardPolicy.New);
assertEquals(si.config.subjects, ["x", "y"]);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer name", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "A",
subjects: ["foo", "bar"],
});
async function addC(
config: Partial<ConsumerConfig>,
expect: string,
): Promise<ConsumerInfo> {
const d = deferred();
nc.subscribe("$JS.API.CONSUMER.>", {
callback: (err, msg) => {
if (err) {
d.reject(err);
}
if (msg.subject === expect) {
d.resolve();
}
},
timeout: 1000,
});
let ci: ConsumerInfo;
try {
ci = await jsm.consumers.add("A", config);
} catch (err) {
d.resolve();
return Promise.reject(err);
}
await d;
return Promise.resolve(ci!);
}
// an ephemeral with a name
let ci = await addC({
ack_policy: AckPolicy.Explicit,
inactive_threshold: nanos(1000),
name: "a",
}, "$JS.API.CONSUMER.CREATE.A.a");
assertExists(ci.config.inactive_threshold);
ci = await addC({
ack_policy: AckPolicy.Explicit,
durable_name: "b",
}, "$JS.API.CONSUMER.CREATE.A.b");
assertEquals(ci.config.inactive_threshold, undefined);
// a ephemeral with a filter
ci = await addC({
ack_policy: AckPolicy.Explicit,
name: "c",
filter_subject: "bar",
}, "$JS.API.CONSUMER.CREATE.A.c.bar");
assertExists(ci.config.inactive_threshold);
// a deprecated ephemeral
ci = await addC({
ack_policy: AckPolicy.Explicit,
filter_subject: "bar",
}, "$JS.API.CONSUMER.CREATE.A");
assertExists(ci.name);
assertEquals(ci.config.durable_name, undefined);
assertExists(ci.config.inactive_threshold);
await cleanup(ns, nc);
});
async function testConsumerNameAPI(nc: NatsConnection) {
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "A",
subjects: ["foo", "bar"],
});
async function addC(
config: Partial<ConsumerConfig>,
expect: string,
): Promise<ConsumerInfo> {
const d = deferred();
nc.subscribe("$JS.API.CONSUMER.>", {
callback: (err, msg) => {
if (err) {
d.reject(err);
}
if (msg.subject === expect) {
d.resolve();
}
},
timeout: 1000,
});
let ci: ConsumerInfo;
try {
ci = await jsm.consumers.add("A", config);
} catch (err) {
d.resolve();
return Promise.reject(err);
}
await d;
return Promise.resolve(ci!);
}
// a named ephemeral
await assertRejects(
async () => {
await addC({
ack_policy: AckPolicy.Explicit,
inactive_threshold: nanos(1000),
name: "a",
}, "$JS.API.CONSUMER.CREATE.A");
},
errors.InvalidArgumentError,
"'name' requires server",
);
const ci = await addC({
ack_policy: AckPolicy.Explicit,
inactive_threshold: nanos(1000),
}, "$JS.API.CONSUMER.CREATE.A");
assertExists(ci.config.inactive_threshold);
assertExists(ci.name);
await addC({
ack_policy: AckPolicy.Explicit,
durable_name: "b",
}, "$JS.API.CONSUMER.DURABLE.CREATE.A.b");
}
Deno.test("jsm - consumer name apis are not used on old servers", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.7.0")) {
return;
}
// change the version of the server to force legacy apis
const nci = nc as NatsConnectionImpl;
nci.features.update("2.7.0");
await testConsumerNameAPI(nc);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer name apis are not used when disabled", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const nci = nc as NatsConnectionImpl;
nci.features.disable(Feature.JS_NEW_CONSUMER_CREATE_API);
await testConsumerNameAPI(nc);
await cleanup(ns, nc);
});
Deno.test("jsm - mirror_direct options", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const jsm = await jetstreamManager(nc);
let si = await jsm.streams.add({
name: "A",
allow_direct: true,
subjects: ["a.*"],
max_msgs_per_subject: 1,
});
assertEquals(si.config.allow_direct, true);
si = await jsm.streams.add({
name: "B",
allow_direct: true,
mirror_direct: true,
mirror: {
name: "A",
},
max_msgs_per_subject: 1,
});
assertEquals(si.config.allow_direct, true);
assertEquals(si.config.mirror_direct, true);
await cleanup(ns, nc);
});
Deno.test("jsm - consumers with name and durable_name", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const { stream } = await initStream(nc);
// should be ok
await jsm.consumers.add(stream, {
name: "x",
durable_name: "x",
ack_policy: AckPolicy.None,
});
// should fail from the server
await assertRejects(
async () => {
await jsm.consumers.add(stream, {
name: "y",
durable_name: "z",
ack_policy: AckPolicy.None,
});
},
Error,
"consumer name in subject does not match durable name in request",
);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer name is validated", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const { stream } = await initStream(nc);
const jsm = await jetstreamManager(nc);
function test(
n: string,
conf: Partial<ConsumerConfig> = {},
): Promise<unknown> {
const opts = Object.assign({ name: n, ack_policy: AckPolicy.None }, conf);
return jsm.consumers.add(stream, opts);
}
await assertRejects(
async () => {
await test("hello.world");
},
Error,
"consumer 'name' cannot contain '.'",
);
await assertRejects(
async () => {
await test("hello>world");
},
Error,
"consumer 'name' cannot contain '>'",
);
await assertRejects(
async () => {
await test("one*two");
},
Error,
"consumer 'name' cannot contain '*'",
);
await assertRejects(
async () => {
await test(".");
},
Error,
"consumer 'name' cannot contain '.'",
);
await cleanup(ns, nc);
});
Deno.test("jsm - discard_new_per_subject option", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.2")) {
return;
}
const jsm = await jetstreamManager(nc);
// discard policy new is required
await assertRejects(
async () => {
await jsm.streams.add({
name: "A",
discard_new_per_subject: true,
subjects: ["A.>"],
max_msgs_per_subject: 1,
});
},
Error,
"discard new per subject requires discard new policy to be set",
);
const si = await jsm.streams.add({
name: "A",
discard: DiscardPolicy.New,
discard_new_per_subject: true,
subjects: ["A.>"],
max_msgs_per_subject: 1,
});
assertEquals(si.config.discard_new_per_subject, true);
const js = jetstream(nc);
await js.publish("A.b", Empty);
await assertRejects(
() => {
return js.publish("A.b", Empty);
},
Error,
"maximum messages per subject exceeded",
);
await cleanup(ns, nc);
});
Deno.test("jsm - paginated subjects", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
jetstream: {
max_memory_store: -1,
},
}),
);
const jsm = await jetstreamManager(nc);
const js = jetstream(nc);
await initStream(nc, "a", {
subjects: ["a.*"],
storage: StorageType.Memory,
});
const proms: Promise<PubAck>[] = [];
for (let i = 1; i <= 100_001; i++) {
proms.push(js.publish(`a.${i}`));
if (proms.length === 1000) {
await Promise.all(proms);
proms.length = 0;
}
}
if (proms.length) {
await Promise.all(proms);
}
const si = await jsm.streams.info("a", {
subjects_filter: ">",
});
const names = Object.getOwnPropertyNames(si.state.subjects);
assertEquals(names.length, 100_001);
await cleanup(ns, nc);
});
Deno.test("jsm - paged stream list", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
jetstream: {
max_memory_store: -1,
},
}),
);
const jsm = await jetstreamManager(nc);
for (let i = 0; i < 257; i++) {
await jsm.streams.add({
name: `${i}`,
subjects: [`${i}`],
storage: StorageType.Memory,
});
}
const lister = jsm.streams.list();
const streams: StreamInfo[] = [];
for await (const si of lister) {
streams.push(si);
}
assertEquals(streams.length, 257);
streams.sort((a, b) => {
const na = parseInt(a.config.name);
const nb = parseInt(b.config.name);
return na - nb;
});
for (let i = 0; i < streams.length; i++) {
assertEquals(streams[i].config.name, `${i}`);
}
await cleanup(ns, nc);
});
Deno.test("jsm - paged consumer infos", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
jetstream: {
max_memory_store: -1,
},
}),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "A",
subjects: ["a"],
storage: StorageType.Memory,
});
for (let i = 0; i < 257; i++) {
await jsm.consumers.add("A", {
durable_name: `${i}`,
ack_policy: AckPolicy.None,
});
}
const lister = jsm.consumers.list("A");
const consumers: ConsumerInfo[] = [];
for await (const si of lister) {
consumers.push(si);
}
assertEquals(consumers.length, 257);
consumers.sort((a, b) => {
const na = parseInt(a.name);
const nb = parseInt(b.name);
return na - nb;
});
for (let i = 0; i < consumers.length; i++) {
assertEquals(consumers[i].name, `${i}`);
}
await cleanup(ns, nc);
});
Deno.test("jsm - list filter", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
jetstream: {
max_memory_store: -1,
},
}),
);
const spec: WithRequired<Partial<StreamConfig>, "name">[] = [
{ name: "s1", subjects: ["foo"] },
{ name: "s2", subjects: ["bar"] },
{ name: "s3", subjects: ["foo.*", "bar.*"] },
{ name: "s4", subjects: ["foo-1.A"] },
{ name: "s5", subjects: ["foo.A.bar.B"] },
{ name: "s6", subjects: ["foo.C.bar.D.E"] },
];
const jsm = await jetstreamManager(nc);
for (let i = 0; i < spec.length; i++) {
const s = spec[i];
s.storage = StorageType.Memory;
await jsm.streams.add(s);
}
const tests: { filter: string; expected: string[] }[] = [
{ filter: "foo", expected: ["s1"] },
{ filter: "bar", expected: ["s2"] },
{ filter: "*", expected: ["s1", "s2"] },
{ filter: ">", expected: ["s1", "s2", "s3", "s4", "s5", "s6"] },
{ filter: "*.A", expected: ["s3", "s4"] },
];
for (let i = 0; i < tests.length; i++) {
const lister = jsm.streams.list(tests[i].filter);
const streams = await lister.next();
const names = streams.map((si) => {
return si.config.name;
});
names.sort();
assertEquals(names, tests[i].expected);
}
await cleanup(ns, nc);
});
Deno.test("jsm - stream names list filtering subject", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
const spec = [
{ name: "s1", subjects: ["foo"] },
{ name: "s2", subjects: ["bar"] },
{ name: "s3", subjects: ["foo.*", "bar.*"] },
{ name: "s4", subjects: ["foo-1.A"] },
{ name: "s5", subjects: ["foo.A.bar.B"] },
{ name: "s6", subjects: ["foo.C.bar.D.E"] },
];
const buf: Promise<StreamInfo>[] = [];
spec.forEach(({ name, subjects }) => {
buf.push(jsm.streams.add({ name, subjects }));
});
await Promise.all(buf);
async function collect<T>(iter: Lister<T>): Promise<T[]> {
const names: T[] = [];
for await (const n of iter) {
names.push(n);
}
return names;
}
const tests = [
{ filter: "foo", expected: ["s1"] },
{ filter: "bar", expected: ["s2"] },
{ filter: "*", expected: ["s1", "s2"] },
{ filter: ">", expected: ["s1", "s2", "s3", "s4", "s5", "s6"] },
{ filter: "", expected: ["s1", "s2", "s3", "s4", "s5", "s6"] },
{ filter: "*.A", expected: ["s3", "s4"] },
];
async function t(filter: string, expected: string[]) {
const lister = await jsm.streams.names(filter);
const names = await collect<string>(lister);
assertArrayIncludes(names, expected);
assertEquals(names.length, expected.length);
}
for (let i = 0; i < tests.length; i++) {
const { filter, expected } = tests[i];
await t(filter, expected);
}
await cleanup(ns, nc);
});
Deno.test("jsm - remap domain", () => {
const sc = {} as Partial<StreamConfig>;
assertEquals(convertStreamSourceDomain(sc.mirror), undefined);
assertEquals(sc.sources?.map(convertStreamSourceDomain), undefined);
sc.mirror = {
name: "a",
domain: "a",
};
assertEquals(convertStreamSourceDomain(sc.mirror), {
name: "a",
external: { api: `$JS.a.API` },
});
const sources = [
{ name: "x", domain: "x" },
{ name: "b", external: { api: `$JS.b.API` } },
] as Partial<StreamSource[]>;
const processed = sources.map(convertStreamSourceDomain);
assertEquals(processed, [
{ name: "x", external: { api: "$JS.x.API" } },
{ name: "b", external: { api: "$JS.b.API" } },
]);
});
Deno.test("jsm - filter_subjects", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await jsm.streams.add({ name, subjects: [`a.>`] });
const ci = await jsm.consumers.add(name, {
durable_name: "dur",
filter_subjects: [
"a.b",
"a.c",
],
ack_policy: AckPolicy.Explicit,
});
assertEquals(ci.config.filter_subject, undefined);
assert(Array.isArray(ci.config.filter_subjects));
assertArrayIncludes(ci.config.filter_subjects, ["a.b", "a.c"]);
await cleanup(ns, nc);
});
Deno.test("jsm - filter_subjects rejects filter_subject", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await jsm.streams.add({ name, subjects: [`a.>`] });
await assertRejects(
async () => {
await jsm.consumers.add(name, {
durable_name: "dur",
filter_subject: "a.a",
filter_subjects: [
"a.b",
"a.c",
],
ack_policy: AckPolicy.Explicit,
});
},
Error,
"consumer cannot have both FilterSubject and FilterSubjects specified",
);
await cleanup(ns, nc);
});
Deno.test("jsm - update filter_subject", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await jsm.streams.add({ name, subjects: [`a.>`] });
let ci = await jsm.consumers.add(name, {
durable_name: "dur",
filter_subject: "a.x",
ack_policy: AckPolicy.Explicit,
});
assertEquals(ci.config.filter_subject, "a.x");
ci = await jsm.consumers.update(name, "dur", {
filter_subject: "a.y",
});
assertEquals(ci.config.filter_subject, "a.y");
await cleanup(ns, nc);
});
Deno.test("jsm - update filter_subjects", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await jsm.streams.add({ name, subjects: [`a.>`] });
let ci = await jsm.consumers.add(name, {
durable_name: "dur",
filter_subjects: ["a.x"],
ack_policy: AckPolicy.Explicit,
});
assertArrayIncludes(ci.config.filter_subjects!, ["a.x"]);
ci = await jsm.consumers.update(name, "dur", {
filter_subjects: ["a.x", "a.y"],
});
assertArrayIncludes(ci.config.filter_subjects!, ["a.x", "a.y"]);
await cleanup(ns, nc);
});
Deno.test("jsm - update from filter_subject to filter_subjects", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const name = nuid.next();
await jsm.streams.add({ name, subjects: [`a.>`] });
let ci = await jsm.consumers.add(name, {
durable_name: "dur",
filter_subject: "a.x",
ack_policy: AckPolicy.Explicit,
});
assertEquals(ci.config.filter_subject, "a.x");
// fail if not removing filter_subject
await assertRejects(
async () => {
await jsm.consumers.update(name, "dur", {
filter_subjects: ["a.x", "a.y"],
});
},
Error,
"consumer cannot have both FilterSubject and FilterSubjects specified",
);
// now switch it
ci = await jsm.consumers.update(name, "dur", {
filter_subject: "",
filter_subjects: ["a.x", "a.y"],
});
assertEquals(ci.config.filter_subject, undefined);
assertArrayIncludes(ci.config.filter_subjects!, ["a.x", "a.y"]);
// fail if not removing filter_subjects
await assertRejects(
async () => {
await jsm.consumers.update(name, "dur", {
filter_subject: "a.x",
});
},
Error,
"consumer cannot have both FilterSubject and FilterSubjects specified",
);
// and from filter_subjects back
ci = await jsm.consumers.update(name, "dur", {
filter_subject: "a.x",
filter_subjects: [],
});
assertEquals(ci.config.filter_subject, "a.x");
assertEquals(ci.config.filter_subjects!, undefined);
await cleanup(ns, nc);
});
Deno.test("jsm - stored msg decode", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const name = nuid.next();
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
const js = jetstream(nc);
await jsm.streams.add({ name, subjects: [`a.>`], allow_direct: false });
await js.publish("a.a", "hello");
await js.publish("a.a", JSON.stringify({ one: "two", a: [1, 2, 3] }));
assertEquals(
(await jsm.streams.getMessage(name, { seq: 1 }))?.string(),
"hello",
);
assertEquals((await jsm.streams.getMessage(name, { seq: 2 }))?.json(), {
one: "two",
a: [1, 2, 3],
});
await cleanup(ns, nc);
});
Deno.test("jsm - stream/consumer metadata", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
async function addStream(name: string, md?: Record<string, string>) {
const si = await jsm.streams.add({
name,
subjects: [name],
metadata: md,
});
if (md) {
stripNatsMetadata(si.config.metadata);
stripNatsMetadata(md);
assertEquals(si.config.metadata, md || {});
}
}
async function updateStream(name: string, md?: Record<string, string>) {
const si = await jsm.streams.update(name, {
metadata: md,
});
stripNatsMetadata(si.config.metadata);
stripNatsMetadata(md);
assertEquals(si.config.metadata, md);
}
async function addConsumer(
stream: string,
name: string,
md?: Record<string, string>,
) {
const ci = await jsm.consumers.add(stream, {
durable_name: name,
metadata: md,
});
stripNatsMetadata(ci.config.metadata);
if (md) {
assertEquals(ci.config.metadata, md);
}
}
async function updateConsumer(
stream: string,
name: string,
md?: Record<string, string>,
) {
const ci = await jsm.consumers.update(stream, name, { metadata: md });
stripNatsMetadata(ci.config.metadata);
stripNatsMetadata(md);
assertEquals(ci.config.metadata, md);
}
// we should be able to add/update metadata
let stream = nuid.next();
let consumer = nuid.next();
await addStream(stream, { hello: "world" });
await updateStream(stream, { one: "two" });
await addConsumer(stream, consumer, { test: "true" });
await updateConsumer(stream, consumer, { foo: "bar" });
// fake a server version change
(nc as NatsConnectionImpl).features.update("2.9.0");
stream = nuid.next();
consumer = nuid.next();
await assertRejects(
async () => {
await addStream(stream, { hello: "world" });
},
Error,
"stream 'metadata' requires server 2.10.0",
);
// add without md
await addStream(stream);
// should fail update w/ metadata
await assertRejects(
async () => {
await updateStream(stream, { hello: "world" });
},
Error,
"stream 'metadata' requires server 2.10.0",
);
// should fail adding consumer with md
await assertRejects(
async () => {
await addConsumer(stream, consumer, { hello: "world" });
},
InvalidArgumentError,
"'metadata' requires server",
);
// add w/o metadata
await addConsumer(stream, consumer);
// should fail to update consumer with md
await assertRejects(
async () => {
await updateConsumer(stream, consumer, { hello: "world" });
},
InvalidArgumentError,
"'metadata' requires server",
);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer api action", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "stream", subjects: ["a"] });
const config = {
ack_policy: AckPolicy.Explicit,
durable_name: "hello",
} as ConsumerConfig;
// testing the app, consumer update does an info
// so testing the underlying API
const api = jsm.consumers as ConsumerAPIImpl;
await assertRejects(
async () => {
await api.addUpdate("stream", config, {
action: ConsumerApiAction.Update,
});
},
Error,
"consumer does not exist",
);
await api.add("stream", config);
// this should fail if options on the consumer changed
config.inactive_threshold = nanos(60 * 1000);
await assertRejects(
async () => {
await api.add("stream", config);
},
Error,
"consumer already exists",
);
await cleanup(ns, nc);
});
Deno.test("jsm - validate stream name in operations", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
const names = ["", ".", "*", ">", "\r", "\n", "\t", " "];
const tests = [
{
name: "add stream",
fn: (name: string) => {
return jsm.streams.add({ name, subjects: ["a"] });
},
},
{
name: "stream info",
fn: (name: string) => {
return jsm.streams.info(name);
},
},
{
name: "stream update",
fn: (name: string) => {
return jsm.streams.update(name, { subjects: ["a", "b"] });
},
},
{
name: "stream purge",
fn: (name: string) => {
return jsm.streams.purge(name);
},
},
{
name: "stream delete",
fn: (name: string) => {
return jsm.streams.delete(name);
},
},
{
name: "getMessage",
fn: (name: string) => {
return jsm.streams.getMessage(name, { seq: 1 });
},
},
{
name: "deleteMessage",
fn: (name: string) => {
return jsm.streams.deleteMessage(name, 1);
},
},
];
for (let j = 0; j < tests.length; j++) {
const test = tests[j];
for (let idx = 0; idx < names.length; idx++) {
let v = names[idx];
try {
await test.fn(v[0]);
if (!v[1]) {
fail(`${test.name} - ${v} should have been rejected`);
}
} catch (err) {
if (v === "\r") v = "\\r";
if (v === "\n") v = "\\n";
if (v === "\t") v = "\\t";
const m = v === ""
? "stream name required"
: `stream name ('${names[idx]}') cannot contain '${v}'`;
assertEquals((err as Error).message, m, `${test.name} - ${m}`);
}
}
}
await cleanup(ns, nc);
});
Deno.test("jsm - validate consumer name", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
const stream = nuid.next();
await jsm.streams.add({ name: stream, subjects: [stream] });
// we don't test for empty, because consumer validation will
// not test durable or name unless set - default is for server
// to set name
const tests = [".", "*", ">", "\r", "\n", "\t", " "];
for (let idx = 0; idx < tests.length; idx++) {
let v = tests[idx];
try {
await jsm.consumers.add(stream, {
durable_name: v,
ack_policy: AckPolicy.None,
});
fail(`${v} should have been rejected`);
} catch (err) {
if (v === "\r") v = "\\r";
if (v === "\n") v = "\\n";
if (v === "\t") v = "\\t";
const m = `durable name ('${tests[idx]}') cannot contain '${v}'`;
assertEquals((err as Error).message, m);
}
}
for (let idx = 0; idx < tests.length; idx++) {
let v = tests[idx];
try {
await jsm.consumers.add(stream, {
name: v,
ack_policy: AckPolicy.None,
});
fail(`${v} should have been rejected`);
} catch (err) {
if (v === "\r") v = "\\r";
if (v === "\n") v = "\\n";
if (v === "\t") v = "\\t";
const m = `consumer 'name' cannot contain '${v}'`;
assertEquals((err as Error).message, m);
}
}
await cleanup(ns, nc);
});
Deno.test("jsm - validate consumer name in operations", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
const names = ["", ".", "*", ">", "\r", "\n", "\t", " "];
const tests = [
{
name: "consumer info",
fn: (name: string) => {
return jsm.consumers.info("foo", name);
},
},
{
name: "consumer delete",
fn: (name: string) => {
return jsm.consumers.delete("foo", name);
},
},
{
name: "consumer update",
fn: (name: string) => {
return jsm.consumers.update("foo", name, { description: "foo" });
},
},
];
for (let j = 0; j < tests.length; j++) {
const test = tests[j];
for (let idx = 0; idx < names.length; idx++) {
let v = names[idx];
try {
await test.fn(v[0]);
if (!v[1]) {
fail(`${test.name} - ${v} should have been rejected`);
}
} catch (err) {
if (v === "\r") v = "\\r";
if (v === "\n") v = "\\n";
if (v === "\t") v = "\\t";
const m = v === ""
? "durable name required"
: `durable name ('${names[idx]}') cannot contain '${v}'`;
assertEquals((err as Error).message, m, `${test.name} - ${m}`);
}
}
}
await cleanup(ns, nc);
});
Deno.test("jsm - source transforms", 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);
await jsm.streams.add({
name,
subjects: ["foo", "bar"],
storage: StorageType.Memory,
});
const js = jetstream(nc);
await Promise.all([
js.publish("foo"),
js.publish("bar"),
]);
const mi = await jsm.streams.add({
name: name + 2,
storage: StorageType.Memory,
mirror: {
name,
subject_transforms: [
{
src: "foo",
dest: "foo-transformed",
},
{
src: "bar",
dest: "bar-transformed",
},
],
},
});
assertEquals(mi.config.mirror?.subject_transforms?.length, 2);
const transforms = mi.config.mirror?.subject_transforms?.map((v) => {
return v.dest;
});
assertExists(transforms);
assertArrayIncludes(transforms, ["foo-transformed", "bar-transformed"]);
const subjects = [];
const c = await js.consumers.get(name + 2);
const iter = await c.fetch({ max_messages: 2 });
for await (const m of iter) {
subjects.push(m.subject);
}
assertArrayIncludes(transforms, ["foo-transformed", "bar-transformed"]);
await cleanup(ns, nc);
});
Deno.test("jsm - source transforms rejected on old servers", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const nci = nc as NatsConnectionImpl;
nci.features.update("2.8.0");
nci.info!.version = "2.8.0";
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.add({
name: "n",
subjects: ["foo"],
storage: StorageType.Memory,
subject_transform: {
src: "foo",
dest: "transformed-foo",
},
});
},
Error,
"stream 'subject_transform' requires server 2.10.0",
);
await jsm.streams.add({
name: "src",
subjects: ["foo", "bar"],
storage: StorageType.Memory,
});
await assertRejects(
async () => {
await jsm.streams.add({
name: "n",
storage: StorageType.Memory,
mirror: {
name: "src",
subject_transforms: [
{
src: "foo",
dest: "foo-transformed",
},
],
},
});
},
Error,
"stream mirror 'subject_transforms' requires server 2.10.0",
);
await assertRejects(
async () => {
await jsm.streams.add(
{
name: "n",
storage: StorageType.Memory,
sources: [{
name: "src",
subject_transforms: [
{
src: "foo",
dest: "foo-transformed",
},
],
}],
},
);
},
Error,
"stream sources 'subject_transforms' requires server 2.10.0",
);
await cleanup(ns, nc);
});
Deno.test("jsm - stream compression not supported", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const nci = nc as NatsConnectionImpl;
nci.features.update("2.9.0");
nci.info!.version = "2.9.0";
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.add({
name: "n",
subjects: ["foo"],
storage: StorageType.File,
compression: StoreCompression.S2,
});
},
Error,
"stream 'compression' requires server 2.10.0",
);
await cleanup(ns, nc);
});
Deno.test("jsm - stream compression", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
let si = await jsm.streams.add({
name: "n",
subjects: ["foo"],
storage: StorageType.File,
compression: StoreCompression.S2,
});
assertEquals(si.config.compression, StoreCompression.S2);
si = await jsm.streams.update("n", { compression: StoreCompression.None });
assertEquals(si.config.compression, StoreCompression.None);
await cleanup(ns, nc);
});
Deno.test("jsm - stream consumer limits", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
let si = await jsm.streams.add({
name: "map",
subjects: ["foo"],
storage: StorageType.Memory,
consumer_limits: {
max_ack_pending: 20,
inactive_threshold: nanos(60_000),
},
});
assertEquals(si.config.consumer_limits?.max_ack_pending, 20);
assertEquals(si.config.consumer_limits?.inactive_threshold, nanos(60_000));
const ci = await jsm.consumers.add("map", { durable_name: "map" });
assertEquals(ci.config.max_ack_pending, 20);
assertEquals(ci.config.inactive_threshold, nanos(60_000));
si = await jsm.streams.update("map", {
consumer_limits: {
max_ack_pending: 200,
inactive_threshold: nanos(120_000),
},
});
assertEquals(si.config.consumer_limits?.max_ack_pending, 200);
assertEquals(si.config.consumer_limits?.inactive_threshold, nanos(120_000));
await cleanup(ns, nc);
});
Deno.test("jsm - stream consumer limits override", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const si = await jsm.streams.add({
name: "map",
subjects: ["foo"],
storage: StorageType.Memory,
consumer_limits: {
max_ack_pending: 20,
inactive_threshold: nanos(60_000),
},
});
assertEquals(si.config.consumer_limits?.max_ack_pending, 20);
assertEquals(si.config.consumer_limits?.inactive_threshold, nanos(60_000));
const ci = await jsm.consumers.add("map", {
durable_name: "map",
max_ack_pending: 19,
inactive_threshold: nanos(59_000),
});
assertEquals(ci.config.max_ack_pending, 19);
assertEquals(ci.config.inactive_threshold, nanos(59_000));
await assertRejects(
async () => {
await jsm.consumers.add("map", {
durable_name: "map",
max_ack_pending: 100,
});
},
Error,
"consumer max ack pending exceeds system limit of 20",
);
await cleanup(ns, nc);
});
Deno.test("jsm - stream consumer limits rejected on old servers", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const nci = nc as NatsConnectionImpl;
nci.features.update("2.9.0");
nci.info!.version = "2.9.0";
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.add({
name: "map",
subjects: ["foo"],
storage: StorageType.Memory,
consumer_limits: {
max_ack_pending: 20,
inactive_threshold: nanos(60_000),
},
});
},
Error,
"stream 'consumer_limits' requires server 2.10.0",
);
await cleanup(ns, nc);
});
Deno.test("jsm - api check not ok", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
let count = 0;
nc.subscribe("$JS.API.INFO", {
callback: () => {
count++;
},
});
await jetstreamManager(nc, { checkAPI: false });
await jetstream(nc).jetstreamManager(false);
await jetstream(nc, { checkAPI: false }).jetstreamManager();
await jetstream(nc, { checkAPI: false }).jetstreamManager(true);
await jetstream(nc).jetstreamManager();
await nc.flush();
assertEquals(count, 2);
await cleanup(ns, nc);
});
Deno.test("jsm - api check ok", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
let count = 0;
nc.subscribe("$JS.API.INFO", {
callback: () => {
count++;
},
});
await jetstreamManager(nc, {});
await jetstreamManager(nc, { checkAPI: true });
await jetstream(nc).jetstreamManager();
await jetstream(nc, { checkAPI: true }).jetstreamManager();
await jetstream(nc, { checkAPI: true }).jetstreamManager(false);
assertEquals(count, 4);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer create paused", 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: "A",
subjects: ["a.>"],
});
const tomorrow = Date.now() + 24 * 60 * 60 * 1000;
const ci = await jsm.consumers.add("A", {
durable_name: "a",
ack_policy: AckPolicy.None,
pause_until: new Date(tomorrow).toISOString(),
});
assertEquals(ci.paused, true);
await cleanup(ns, nc);
});
Deno.test("jsm - pause/unpause", 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: "A",
subjects: ["a.>"],
});
const ci = await jsm.consumers.add("A", {
durable_name: "a",
ack_policy: AckPolicy.None,
});
assertEquals(ci.paused, undefined);
let pi = await jsm.consumers.pause(
"A",
"a",
new Date(Date.now() + 24 * 60 * 60 * 1000),
);
assertEquals(pi.paused, true);
pi = await jsm.consumers.resume("A", "a");
assertEquals(pi.paused, false);
await cleanup(ns, nc);
});
Deno.test("jsm - consumer pedantic", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "A",
subjects: ["a"],
storage: StorageType.Memory,
consumer_limits: {
max_ack_pending: 10,
},
});
// this should work
await jsm.consumers.add("A", {
name: "a",
ack_policy: AckPolicy.Explicit,
});
// but this should reject
await assertRejects(
() => {
return jsm.consumers.add("A", {
name: "b",
ack_policy: AckPolicy.Explicit,
max_ack_pending: 0,
}, { pedantic: true });
},
Error,
"pedantic mode: max_ack_pending must be set if it's configured in stream limits",
);
await cleanup(ns, nc);
});
Deno.test("jsm - storage", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
let si = await jsm.streams.add({
name: "Mem",
subjects: ["a"],
storage: StorageType.Memory,
});
assertEquals(si.config.storage, StorageType.Memory);
let ci = await jsm.consumers.add("Mem", { name: "mc", mem_storage: true });
assertEquals(ci.config.mem_storage, true);
si = await jsm.streams.add({
name: "File",
subjects: ["b"],
storage: StorageType.File,
});
assertEquals(si.config.storage, StorageType.File);
ci = await jsm.consumers.add("File", { name: "fc", mem_storage: false });
assertEquals(ci.config.mem_storage, undefined);
await cleanup(ns, nc);
});
Deno.test("jsm - pull consumer priority groups", async (t) => {
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: "A",
subjects: [`a`],
});
await t.step("priority group is not an array", async () => {
await assertRejects(
() => {
return jsm.consumers.add("A", {
name: "a",
ack_policy: AckPolicy.None,
//@ts-ignore: testing
priority_groups: "hello",
});
},
Error,
"'priority_groups' must be an array",
);
});
await t.step("priority_group empty array", async () => {
await assertRejects(
() => {
return jsm.consumers.add("A", {
name: "a",
ack_policy: AckPolicy.None,
//@ts-ignore: testing
priority_groups: [],
});
},
Error,
"'priority_groups' must have at least one group",
);
});
await t.step("missing priority_policy", async () => {
await assertRejects(
() => {
return jsm.consumers.add("A", {
name: "a",
ack_policy: AckPolicy.None,
priority_groups: ["hello"],
});
},
Error,
"'priority_policy' must be 'none', 'prioritized', 'overflow', or 'pinned_client'",
);
});
await t.step("bad priority_policy ", async () => {
await assertRejects(
() => {
return jsm.consumers.add("A", {
name: "a",
ack_policy: AckPolicy.None,
priority_groups: ["hello"],
//@ts-ignore: test
priority_policy: "hello",
});
},
Error,
"'priority_policy' must be 'none', 'prioritized', 'overflow', or 'pinned_client'",
);
});
await t.step("check config", async () => {
const ci = await jsm.consumers.add("A", {
name: "a",
ack_policy: AckPolicy.None,
priority_groups: ["hello"],
priority_policy: PriorityPolicy.Overflow,
});
assertEquals(ci.config.priority_policy, PriorityPolicy.Overflow);
assertEquals(ci.config.priority_groups, ["hello"]);
});
await cleanup(ns, nc);
});
Deno.test("jsm - stream message ttls", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const si = await jsm.streams.add({
name: "A",
subjects: ["a"],
allow_msg_ttl: true,
subject_delete_marker_ttl: nanos(60_000),
});
assertEquals(si.config.allow_msg_ttl, true);
assertEquals(si.config.subject_delete_marker_ttl, nanos(60_000));
// server seems to have changed behaviour.
// https://github.com/nats-io/nats-server/issues/6872
// await assertRejects(
// () => {
// //@ts-expect-error: this is a test
// return jsm.streams.update("A", { allow_msg_ttl: false });
// },
// Error,
// "subject marker delete cannot be set if message TTLs are disabled",
// );
await jsm.streams.update("A", { subject_delete_marker_ttl: 0 });
await assertRejects(
() => {
//@ts-expect-error: this is a test
return jsm.streams.update("A", { allow_msg_ttl: false });
},
Error,
"message TTL status can not be disabled",
);
await cleanup(ns, nc);
});
Deno.test("jsm - message ttls", 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: "A",
subjects: ["a"],
allow_msg_ttl: true,
});
const js = jsm.jetstream();
const c = await js.consumers.get("A");
const iter = await c.consume();
(async () => {
for await (const m of iter) {
console.log(m.seq, m.headers);
}
})().then();
await js.publish("a", "hello", { ttl: "4s" });
const start = Date.now();
for (let i = 0;; i++) {
const m = await jsm.streams.getMessage("A", { last_by_subj: "a" });
if (m === null) {
break;
}
console.log(`${i} still here...`);
await delay(1000);
}
const end = Date.now() - start;
assertBetween(end, 4000, 4200);
await cleanup(ns, nc);
});
Deno.test("jsm - mirrors can be removed", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "A",
subjects: ["a"],
});
let si = await jsm.streams.add({
name: "B",
mirror: {
name: "A",
subject_transforms: [{ src: "a", dest: "b" }],
},
});
assertExists(si.config.mirror);
si = await jsm.streams.update("B", { mirror: undefined });
assertEquals(si.config.mirror, undefined);
await cleanup(ns, nc);
});
--- jetstream/tests/jsmsg_test.ts ---
/*
* 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 {
assert,
assertEquals,
assertExists,
assertNotEquals,
assertRejects,
fail,
} from "@std/assert";
import {
AckPolicy,
jetstream,
jetstreamManager,
StorageType,
} from "../src/mod.ts";
import { createInbox, Empty, nanos } from "@nats-io/nats-core";
import type { Msg, MsgImpl } from "@nats-io/nats-core/internal";
import type { JsMsgImpl } from "../src/jsmsg.ts";
import { parseInfo, toJsMsg } from "../src/jsmsg.ts";
import {
assertBetween,
cleanup,
connect,
jetstreamServerConf,
setup,
} from "test_helpers";
import type { JetStreamManagerImpl } from "../src/jsclient.ts";
import { errors } from "../../core/src/mod.ts";
Deno.test("jsmsg - parse", () => {
// "$JS.ACK.<stream>.<consumer>.<deliveryCount><streamSeq><deliverySequence>.<timestamp>.<pending>"
const rs = `$JS.ACK.streamname.consumername.2.3.4.${nanos(Date.now())}.100`;
const info = parseInfo(rs);
assertEquals(info.stream, "streamname");
assertEquals(info.consumer, "consumername");
assertEquals(info.deliveryCount, 2);
assertEquals(info.streamSequence, 3);
assertEquals(info.pending, 100);
});
Deno.test("jsmsg - parse long", () => {
// $JS.ACK.<domain>.<accounthash>.<stream>.<consumer>.<deliveryCount>.<streamSeq>.<deliverySequence>.<timestamp>.<pending>.<random>
const rs = `$JS.ACK.domain.account.streamname.consumername.2.3.4.${
nanos(Date.now())
}.100.rand`;
const info = parseInfo(rs);
assertEquals(info.domain, "domain");
assertEquals(info.account_hash, "account");
assertEquals(info.stream, "streamname");
assertEquals(info.consumer, "consumername");
assertEquals(info.deliveryCount, 2);
assertEquals(info.streamSequence, 3);
assertEquals(info.pending, 100);
});
Deno.test("jsmsg - parse rejects subject is not 9 tokens", () => {
const fn = (s: string, ok: boolean) => {
try {
parseInfo(s);
if (!ok) {
fail(`${s} should have failed to parse`);
}
} catch (err) {
if (ok) {
fail(`${s} shouldn't have failed to parse: ${(err as Error).message}`);
}
}
};
const chunks = `$JS.ACK.stream.consumer.1.2.3.4.5.6.7.8.9.10`.split(".");
for (let i = 1; i <= chunks.length; i++) {
fn(chunks.slice(0, i).join("."), i === 9 || i >= 12);
}
});
Deno.test("jsmsg - acks", async () => {
const nc = await connect({ servers: "demo.nats.io" });
const subj = createInbox();
// something that puts a reply that we can test
let counter = 1;
nc.subscribe(subj, {
callback: (err, msg) => {
if (err) {
fail(err.message);
}
msg.respond(Empty, {
// "$JS.ACK.<stream>.<consumer>.<deliveryCount><streamSeq><deliverySequence>.<timestamp>.<pending>"
reply:
`MY.TEST.streamname.consumername.1.${counter}.${counter}.${Date.now()}.0`,
});
counter++;
},
});
// something to collect the replies
const replies: Msg[] = [];
nc.subscribe("MY.TEST.*.*.*.*.*.*.*", {
callback: (err, msg) => {
if (err) {
fail(err.message);
}
replies.push(msg);
},
});
// nak
let msg = await nc.request(subj);
let js = toJsMsg(msg);
js.nak();
// working
msg = await nc.request(subj);
js = toJsMsg(msg);
js.working();
// working
msg = await nc.request(subj);
js = toJsMsg(msg);
js.term();
msg = await nc.request(subj);
js = toJsMsg(msg);
js.ack();
await nc.flush();
assertEquals(replies.length, 4);
const sc = new TextDecoder();
assertEquals(sc.decode(replies[0].data), "-NAK");
assertEquals(sc.decode(replies[1].data), "+WPI");
assertEquals(sc.decode(replies[2].data), "+TERM");
assertEquals(sc.decode(replies[3].data), "+ACK");
await nc.close();
});
Deno.test("jsmsg - no ack consumer is ackAck 503", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
await js.publish("a.a");
await jsm.consumers.add("A", { durable_name: "a" });
const c = await js.consumers.get("A", "a");
const jm = await c.next();
const err = await assertRejects(
(): Promise<boolean> => {
return jm!.ackAck();
},
errors.RequestError,
);
assert(err.isNoResponders());
await cleanup(ns, nc);
});
Deno.test("jsmsg - explicit consumer ackAck", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
await js.publish("a.a");
await jsm.consumers.add("A", {
durable_name: "a",
ack_policy: AckPolicy.Explicit,
});
const c = await js.consumers.get("A", "a");
const jm = await c.next();
assertEquals(await jm?.ackAck(), true);
assertEquals(await jm?.ackAck(), false);
await cleanup(ns, nc);
});
Deno.test("jsmsg - explicit consumer ackAck timeout", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
await js.publish("a.a");
await jsm.consumers.add("A", { durable_name: "a" });
const c = await js.consumers.get("A", "a");
const jm = await c.next();
// change the subject
((jm as JsMsgImpl).msg as MsgImpl)._reply = "xxxx";
nc.subscribe("xxxx");
const start = Date.now();
await assertRejects(
(): Promise<boolean> => {
return jm!.ackAck({ timeout: 1000 });
},
errors.TimeoutError,
);
assertBetween(Date.now() - start, 1000, 1500);
await cleanup(ns, nc);
});
Deno.test("jsmsg - ackAck js options timeout", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>"],
storage: StorageType.Memory,
allow_direct: true,
});
// default is 5000
const js = jetstream(nc, { timeout: 1500 });
await js.publish("a.a");
await jsm.consumers.add("A", { durable_name: "a" });
const c = await js.consumers.get("A", "a");
const jm = await c.next();
// change the subject
((jm as JsMsgImpl).msg as MsgImpl)._reply = "xxxx";
nc.subscribe("xxxx");
const start = Date.now();
await assertRejects(
(): Promise<boolean> => {
return jm!.ackAck();
},
errors.TimeoutError,
);
assertBetween(Date.now() - start, 1300, 1700);
await cleanup(ns, nc);
});
Deno.test("jsmsg - ackAck legacy timeout", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>"],
storage: StorageType.Memory,
allow_direct: true,
});
// default is 5000
const js = jetstream(nc, { timeout: 1500 });
await js.publish("a.a");
await jsm.consumers.add("A", { durable_name: "a" });
const c = await js.consumers.get("A", "a");
const jm = await c.next();
// change the subject
((jm as JsMsgImpl).msg as MsgImpl)._reply = "xxxx";
nc.subscribe("xxxx");
const start = Date.now();
await assertRejects(
(): Promise<boolean> => {
return jm!.ackAck();
},
errors.TimeoutError,
);
assertBetween(Date.now() - start, 1300, 1700);
await cleanup(ns, nc);
});
Deno.test("jsmsg - time and timestamp", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
await js.publish("a.a");
await jsm.consumers.add("A", { durable_name: "a" });
const oc = await js.consumers.get("A");
const m = await oc.next();
const date = m?.time;
assertExists(date);
assertEquals(date.toISOString(), m?.timestamp!);
await cleanup(ns, nc);
});
Deno.test("jsmsg - reply/sid", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc) as JetStreamManagerImpl;
await jsm.streams.add({
name: "A",
subjects: ["a.>"],
storage: StorageType.Memory,
allow_direct: true,
});
const js = jetstream(nc);
await js.publish("a.a", "hello");
await jsm.consumers.add("A", {
durable_name: "a",
ack_policy: AckPolicy.None,
});
const oc = await js.consumers.get("A");
const m = await oc.next() as JsMsgImpl;
assertNotEquals(m.reply, "");
assert(m.sid > 0);
assertEquals(m.data, new TextEncoder().encode("hello"));
await cleanup(ns, nc);
});
--- jetstream/tests/jstest_util.ts ---
/*
* 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 { AckPolicy, jetstream, jetstreamManager } from "../src/mod.ts";
import type { PubAck, StreamConfig } from "../src/mod.ts";
import { Empty, nanos, nuid } from "@nats-io/nats-core";
import type { NatsConnection } from "@nats-io/nats-core";
export async function initStream(
nc: NatsConnection,
stream: string = nuid.next(),
opts: Partial<StreamConfig> = {},
): Promise<{ stream: string; subj: string }> {
const jsm = await jetstreamManager(nc);
const subj = `${stream}.A`;
const sc = Object.assign({ name: stream, subjects: [subj] }, opts);
await jsm.streams.add(sc);
return { stream, subj };
}
export async function createConsumer(
nc: NatsConnection,
stream: string,
): Promise<string> {
const jsm = await jetstreamManager(nc);
const ci = await jsm.consumers.add(stream, {
name: nuid.next(),
inactive_threshold: nanos(2 * 60 * 1000),
ack_policy: AckPolicy.Explicit,
});
return ci.name;
}
export type FillOptions = {
randomize: boolean;
suffixes: string[];
payload: number;
};
export function fill(
nc: NatsConnection,
prefix: string,
count = 100,
opts: Partial<FillOptions> = {},
): Promise<PubAck[]> {
const js = jetstream(nc);
const options = Object.assign({}, {
randomize: false,
suffixes: "abcdefghijklmnopqrstuvwxyz".split(""),
payload: 0,
}, opts) as FillOptions;
function randomSuffix(): string {
const idx = Math.floor(Math.random() * options.suffixes.length);
return options.suffixes[idx];
}
const payload = options.payload === 0
? Empty
: new Uint8Array(options.payload);
const a = Array.from({ length: count }, (_, idx) => {
const subj = opts.randomize
? `${prefix}.${randomSuffix()}`
: `${prefix}.${options.suffixes[idx % options.suffixes.length]}`;
return js.publish(subj, payload);
});
return Promise.all(a);
}
export async function setupStreamAndConsumer(
nc: NatsConnection,
messages = 100,
): Promise<{ stream: string; consumer: string }> {
const stream = nuid.next();
await initStream(nc, stream, { subjects: [`${stream}.*`] });
await fill(nc, stream, messages, { randomize: true });
const consumer = await createConsumer(nc, stream);
return { stream, consumer };
}
--- jetstream/tests/leak.ts ---
import { connect } from "test_helpers";
import { jetstream } from "../src/mod.ts";
const nc = await connect();
const js = jetstream(nc);
// const jsm = await jetstreamManager(nc);
// await jsm.consumers.add("stream", {name: "test", ack_policy: AckPolicy.Explicit});
// const c = await js.consumers.get("stream", "test");
const c = await js.consumers.get("stream");
const iter = await c.consume({ max_messages: 100 });
for await (const m of iter) {
console.log(m.seq);
// m.ack();
}
await nc.close();
--- jetstream/tests/next_test.ts ---
/*
* Copyright 2022-2024 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 { cleanup, jetstreamServerConf, setup } from "test_helpers";
import { initStream } from "./jstest_util.ts";
import { AckPolicy, DeliverPolicy } from "../src/jsapi_types.ts";
import {
assert,
assertEquals,
assertExists,
assertRejects,
fail,
} from "@std/assert";
import { delay, nanos } from "@nats-io/nats-core";
import type { NatsConnectionImpl } from "@nats-io/nats-core/internal";
import { jetstream, JetStreamError, jetstreamManager } from "../src/mod.ts";
import { JetStreamStatusError } from "../src/jserrors.ts";
Deno.test("next - basics", 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: stream,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
const c = await js.consumers.get(stream, stream);
let ci = await c.info(true);
assertEquals(ci.num_pending, 0);
let m = await c.next({ expires: 1000 });
assertEquals(m, null);
await Promise.all([js.publish(subj), js.publish(subj)]);
ci = await c.info();
assertEquals(ci.num_pending, 2);
m = await c.next();
assertEquals(m?.seq, 1);
m?.ack();
await nc.flush();
ci = await c.info();
assertEquals(ci?.num_pending, 1);
m = await c.next();
assertEquals(m?.seq, 2);
m?.ack();
await cleanup(ns, nc);
});
Deno.test("next - sub leaks", 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);
await c.next({ expires: 1000 });
//@ts-ignore: test
assertEquals(nc.protocol.subscriptions.size(), 1);
await cleanup(ns, nc);
});
Deno.test("next - listener leaks", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["hello"] });
const js = jetstream(nc);
await js.publish("hello");
await jsm.consumers.add("messages", {
durable_name: "myconsumer",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
ack_wait: nanos(3000),
max_waiting: 500,
});
const nci = nc as NatsConnectionImpl;
const base = nci.protocol.listeners.length;
const consumer = await js.consumers.get("messages", "myconsumer");
while (true) {
const m = await consumer.next();
if (m) {
m.nak();
if (m.info?.deliveryCount > 100) {
break;
}
}
}
assertEquals(nci.protocol.listeners.length, base);
await cleanup(ns, nc);
});
Deno.test("next - deleted consumer", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["hello"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
const c = await js.consumers.get("A", "a");
const exited = assertRejects(
() => {
return c.next({ expires: 4000 });
},
JetStreamStatusError,
"consumer deleted",
);
await delay(1000);
await c.delete();
await exited;
await cleanup(ns, nc);
});
Deno.test("next - consumer bind", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.None,
});
const js = jetstream(nc);
await js.publish("a");
const c = await js.consumers.get("A", "a");
// listen to see if the client does a consumer info
const sub = nc.subscribe("$JS.API.CONSUMER.INFO.A.a", {
callback: () => {
fail("saw a consumer info");
},
});
let msg = await c.next({
expires: 1000,
bind: true,
});
assertExists(msg);
msg = await c.next({
expires: 1000,
bind: true,
});
assertEquals(msg, null);
await c.delete();
await assertRejects(
() => {
return c.next({
expires: 1000,
bind: true,
});
},
JetStreamError,
"no responders",
);
await nc.flush();
assertEquals(msg, null);
assertEquals(sub.getProcessed(), 0);
await cleanup(ns, nc);
});
Deno.test("next - delivery count", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["hello"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
max_deliver: 2,
ack_wait: nanos(1000),
});
const js = jetstream(nc);
await js.publish("hello");
const c = await js.consumers.get("A", "a");
let m = await c.next();
assertEquals(m?.info.deliveryCount, 1);
await delay(1500);
m = await c.next();
await delay(1500);
assertEquals(m?.info.deliveryCount, 2);
m = await c.next({ expires: 1000 });
assertEquals(m, null);
await cleanup(ns, nc);
});
Deno.test("next - connection close exits", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const js = jetstream(nc);
await js.publish("a");
const c = await js.consumers.get("A", "a");
assertRejects(
() => {
return c.next({ expires: 30_000 });
},
Error,
"closed connection",
);
await nc.close();
await cleanup(ns, nc);
});
Deno.test("next - stream not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
const c = await jsm.jetstream().consumers.get("messages", "c");
await jsm.streams.delete("messages");
await assertRejects(
() => {
return c.next({ expires: 5_000 });
},
Error,
"no responder",
);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
await jsm.jetstream().publish("m");
const m = await c.next();
assertExists(m);
await cleanup(ns, nc);
});
Deno.test("next - consumer not found and no responders", async () => {
const { ns, nc } = await setup(
jetstreamServerConf(),
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "messages", subjects: ["m"] });
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
const c = await jsm.jetstream().consumers.get("messages", "c");
await jsm.consumers.delete("messages", "c");
await assertRejects(
() => {
return c.next({ expires: 5_000 });
},
Error,
"no responders",
);
await jsm.consumers.add("messages", {
name: "c",
deliver_policy: DeliverPolicy.All,
});
await jsm.jetstream().publish("m");
const m = await c.next();
assertExists(m);
await cleanup(ns, nc);
});
Deno.test("next - max_bytes returns fast", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const js = jsm.jetstream();
await js.publish("a", new Uint8Array(32));
await jsm.consumers.add("A", {
durable_name: "a",
deliver_policy: DeliverPolicy.All,
ack_policy: AckPolicy.Explicit,
});
const c = await js.consumers.get("A", "a");
const start = Date.now();
let end = 0;
const iter = await c.fetch({ expires: 30_000, max_bytes: 8 });
try {
for await (const _ of iter) {
// nothing
}
} catch (_) {
end = Date.now() - start;
}
assert(1000 > end);
await cleanup(ns, nc);
});
--- jetstream/tests/pushconsumers_ordered_test.ts ---
/*
* Copyright 2023-2024 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 {
assertEquals,
assertExists,
assertFalse,
assertRejects,
} from "@std/assert";
import { DeliverPolicy, jetstream, jetstreamManager } from "../src/mod.ts";
import type { JsMsg } from "../src/mod.ts";
import {
cleanup,
flakyTest,
jetstreamServerConf,
notCompatible,
setup,
} from "test_helpers";
import type {
PushConsumerImpl,
PushConsumerMessagesImpl,
} from "../src/pushconsumer.ts";
import { delay } from "@nats-io/nats-core/internal";
import type { NatsConnectionImpl } from "@nats-io/nats-core/internal";
Deno.test("ordered push consumers - get", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
await assertRejects(
() => {
return js.consumers.getPushConsumer("a");
},
Error,
"stream not found",
);
const jsm = await js.jetstreamManager();
await jsm.streams.add({ name: "test", subjects: ["test"] });
await js.publish("test");
const oc = await js.consumers.getPushConsumer(
"test",
) as PushConsumerImpl;
assertExists(oc);
const ci = await oc.info();
assertEquals(ci.num_pending, 1);
assertEquals(ci.name, `${oc.opts.name_prefix}_${oc.serial}`);
await cleanup(ns, nc);
});
Deno.test(
"ordered push consumers - consume reset",
flakyTest(async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
await js.publish("test.a");
await js.publish("test.b");
await js.publish("test.c");
const oc = await js.consumers.getPushConsumer(
"test",
) as PushConsumerImpl;
assertExists(oc);
const seen: number[] = new Array(3).fill(0);
const iter = await oc.consume({
callback: (m: JsMsg) => {
const idx = m.seq - 1;
seen[idx]++;
// mess with the internals so we see these again
if (seen[idx] === 1) {
iter.cursor.deliver_seq--;
iter.cursor.stream_seq--;
}
if (m.info.pending === 0) {
iter.stop();
}
},
}) as PushConsumerMessagesImpl;
await iter.closed();
assertEquals(seen, [2, 2, 1]);
assertEquals(oc.serial, 3);
await cleanup(ns, nc);
}),
);
Deno.test("ordered push consumers - consume", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
await js.publish("test.a");
await js.publish("test.b");
await js.publish("test.c");
const oc = await js.consumers.getPushConsumer("test");
assertExists(oc);
const iter = await oc.consume();
for await (const m of iter) {
if (m.info.pending === 0) {
break;
}
}
assertEquals(iter.getProcessed(), 3);
await cleanup(ns, nc);
});
Deno.test("ordered push consumers - filters consume", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await js.publish("test.a");
await js.publish("test.b");
await js.publish("test.c");
const oc = await js.consumers.getPushConsumer("test", {
filter_subjects: ["test.b"],
});
assertExists(oc);
const iter = await oc.consume();
for await (const m of iter) {
assertEquals("test.b", m.subject);
if (m.info.pending === 0) {
break;
}
}
await iter.closed();
assertEquals(iter.getProcessed(), 1);
await cleanup(ns, nc);
});
Deno.test("ordered push consumers - last per subject", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.a"),
]);
const oc = await js.consumers.getPushConsumer("test", {
deliver_policy: DeliverPolicy.LastPerSubject,
});
const iter = await oc.consume();
await (async () => {
for await (const m of iter) {
assertEquals(m.info.streamSequence, 2);
break;
}
})();
await cleanup(ns, nc);
});
Deno.test("ordered push consumers - start sequence", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.b"),
]);
const oc = await js.consumers.getPushConsumer("test", {
opt_start_seq: 2,
});
const iter = await oc.consume();
await (async () => {
for await (const r of iter) {
assertEquals(r.info.streamSequence, 2);
assertEquals(r.subject, "test.b");
break;
}
})();
await cleanup(ns, nc);
});
Deno.test("ordered push consumers - sub leak", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
await Promise.all([
js.publish("test.a"),
js.publish("test.b"),
]);
const oc = await js.consumers.getPushConsumer("test");
const iter = await oc.consume() as PushConsumerMessagesImpl;
await (async () => {
for await (const r of iter) {
if (r.info.streamSequence === 2) {
break;
}
}
})();
await delay(1000);
const nci = nc as NatsConnectionImpl;
nci.protocol.subscriptions.subs.forEach((s) => {
console.log(">", s.subject);
});
await cleanup(ns, nc);
});
Deno.test("push consumers - flow control", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "test", subjects: ["test.*"] });
const js = jetstream(nc);
async function checkPushConsumer(c: PushConsumerImpl) {
const ci = await c.info(true);
assertEquals(c.ordered, true);
assertEquals(ci.config.flow_control, true);
}
await (checkPushConsumer(
await js.consumers.getPushConsumer("test") as PushConsumerImpl,
));
await (checkPushConsumer(
await js.consumers.getPushConsumer("test", {
headers_only: true,
}) as PushConsumerImpl,
));
let ci = await jsm.consumers.add("test", { deliver_subject: "foo" });
const c = await js.consumers.getPushConsumer(
"test",
ci.name,
) as PushConsumerImpl;
assertFalse(c.ordered);
ci = await c.info(true);
assertEquals(ci.config.flow_control, undefined);
assertEquals(ci.config.idle_heartbeat, undefined);
const buf = [];
nc.subscribe(`$JS.CONSUMER.CREATE.>`, {
callback: (_, msg) => {
buf.push(msg.subject);
},
});
await js.consumers.getBoundPushConsumer({ deliver_subject: "foo" });
await nc.flush();
assertEquals(buf.length, 0);
await cleanup(ns, nc);
});
--- jetstream/tests/schedules_test.ts ---
/*
* Copyright 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 { jetstreamManager } from "../src/jsclient.ts";
import { deferred, nanos } from "@nats-io/nats-core";
import {
cleanup,
jetstreamServerConf,
notCompatible,
setup,
} from "test_helpers";
Deno.test("schedules - basics", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "schedules",
allow_msg_schedules: true,
subjects: ["schedules.>", "target.>"],
allow_msg_ttl: true,
});
await jsm.consumers.add("schedules", {
flow_control: true,
idle_heartbeat: nanos(60_000),
deliver_subject: "cron",
filter_subject: "target.>",
});
const d3000 = deferred();
const d5000 = deferred();
nc.subscribe("cron", {
callback: (_, m) => {
if (m.subject.endsWith("5000")) {
d5000.resolve();
} else if (m.subject.endsWith("3000")) {
d3000.resolve();
}
},
});
const js = jsm.jetstream();
const specification = "@at " + new Date(Date.now() + 5000).toISOString();
await js.publish("schedules.a", "5000", {
schedule: {
specification,
target: "target.5000",
ttl: "5m",
},
});
await js.publish("schedules.b", "3000", {
schedule: {
specification: new Date(Date.now() + 3000),
target: "target.3000",
ttl: "5m",
},
});
await Promise.all([d3000, d5000]);
await cleanup(ns, nc);
});
--- jetstream/tests/status_test.ts ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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 { isMessageNotFound, JetStreamStatus } from "../src/jserrors.ts";
import type { StreamAPIImpl } from "../src/jsmstream_api.ts";
import { Empty, type Msg, type Payload } from "@nats-io/nats-core/internal";
import { assert, assertEquals, assertRejects } from "@std/assert";
import { MsgHdrsImpl } from "../../core/src/headers.ts";
import { cleanup, setup } from "test_helpers";
import { jetstreamServerConf } from "../../test_helpers/mod.ts";
import {
type JetStreamApiError,
jetstreamManager,
} from "../src/internal_mod.ts";
Deno.test("js status - basics", async (t) => {
function makeMsg(
code: number,
description: string,
payload: Payload = Empty,
hdrs: [string, string[]][] = [],
): Msg {
const h = new MsgHdrsImpl(code, description);
for (const [k, v] of hdrs) {
for (const e of v) {
h.append(k, e);
}
}
let data: Uint8Array = Empty;
if (typeof payload === "string") {
data = new TextEncoder().encode(payload);
} else {
data = payload;
}
return {
subject: "foo",
reply: "",
headers: h,
data,
sid: 1,
respond: function () {
return this.reply !== "";
},
string: function () {
return new TextDecoder().decode(this.data);
},
json: function () {
return JSON.parse(this.string());
},
};
}
function makeStatus(
code: number,
description: string,
payload: Payload = Empty,
hdrs?: [string, string[]][],
): JetStreamStatus {
return new JetStreamStatus(makeMsg(code, description, payload, hdrs));
}
await t.step("debug", () => {
const s = makeStatus(404, "not found");
s.debug();
});
await t.step("empty description", () => {
const s = makeStatus(404, "");
assertEquals(s.description, "unknown");
});
await t.step("idle heartbeat", () => {
const s = makeStatus(100, "idle heartbeat", Empty, [["Nats-Last-Consumer", [
"1",
]], ["Nats-Last-Stream", ["10"]]]);
assert(s.isIdleHeartbeat());
assertEquals(s.parseHeartbeat(), {
type: "heartbeat",
lastConsumerSequence: 1,
lastStreamSequence: 10,
});
assertEquals(s.description, "idle heartbeat");
});
await t.step("idle heartbeats missed", () => {
const s = makeStatus(409, "idle heartbeats missed");
assert(s.isIdleHeartbeatMissed());
});
await t.step("request timeout", () => {
const s = makeStatus(408, "request timeout");
assert(s.isRequestTimeout());
});
await t.step("bad request", () => {
const s = makeStatus(400, "");
assert(s.isBadRequest());
});
await t.step("stream deleted", () => {
const s = makeStatus(409, "stream deleted");
assert(s.isStreamDeleted());
});
await t.step("exceeded maxwaiting", () => {
const s = makeStatus(409, "exceeded maxwaiting");
assert(s.isMaxWaitingExceeded());
});
await t.step("consumer is push based", () => {
const s = makeStatus(409, "consumer is push based");
assert(s.isConsumerIsPushBased());
});
});
Deno.test("api error - basics", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "test",
subjects: ["foo"],
allow_direct: true,
});
const api = jsm.streams as StreamAPIImpl;
const err = await assertRejects(() => {
return api._request(`$JS.API.STREAM.MSG.GET.test`, { seq: 1 });
});
const apiErr = err as JetStreamApiError;
assert(isMessageNotFound(err as Error));
const data = apiErr.apiError();
assertEquals(data.code, 404);
assertEquals(data.err_code, 10037);
assertEquals(data.description, "no message found");
await cleanup(ns, nc);
});
--- jetstream/tests/streams_test.ts ---
/*
* Copyright 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 {
cleanup,
jetstreamServerConf,
notCompatible,
setup,
} from "test_helpers";
import {
AckPolicy,
jetstream,
jetstreamManager,
PersistMode,
} from "../src/mod.ts";
import { assertEquals, assertExists, assertRejects } from "@std/assert";
import { initStream } from "./jstest_util.ts";
import type { NatsConnectionImpl } from "@nats-io/nats-core/internal";
Deno.test("streams - get", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
await assertRejects(
async () => {
await js.streams.get("another");
},
Error,
"stream not found",
);
const jsm = await jetstreamManager(nc);
await jsm.streams.add({
name: "another",
subjects: ["a.>"],
});
const s = await js.streams.get("another");
assertExists(s);
assertEquals(s.name, "another");
await jsm.streams.delete("another");
await assertRejects(
async () => {
await s.info();
},
Error,
"stream not found",
);
await cleanup(ns, nc);
});
Deno.test("streams - consumers", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
// add a stream and a message
const { stream, subj } = await initStream(nc);
await js.publish(subj, JSON.stringify({ hello: "world" }));
// retrieve the stream
const s = await js.streams.get(stream);
assertExists(s);
assertEquals(s.name, stream);
// get a message
const sm = await s.getMessage({ seq: 1 });
assertExists(sm);
let d = sm.json<{ hello: string }>();
assertEquals(d.hello, "world");
// attempt to get a named consumer
await assertRejects(
async () => {
await s.getConsumer("a");
},
Error,
"consumer not found",
);
const jsm = await jetstreamManager(nc);
await jsm.consumers.add(s.name, {
durable_name: "a",
ack_policy: AckPolicy.Explicit,
});
const c = await s.getConsumer("a");
const jm = await c.next();
assertExists(jm);
d = jm?.json<{ hello: string }>();
assertEquals(d.hello, "world");
await cleanup(ns, nc);
});
Deno.test("streams - delete message", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
// add a stream and a message
const { stream, subj } = await initStream(nc);
await Promise.all([js.publish(subj), js.publish(subj), js.publish(subj)]);
// retrieve the stream
const s = await js.streams.get(stream);
assertExists(s);
assertEquals(s.name, stream);
// get a message
const sm = await s.getMessage({ seq: 2 });
assertExists(sm);
assertEquals(await s.deleteMessage(2, true), true);
assertEquals(await s.getMessage({ seq: 2 }), null);
const si = await s.info(false, { deleted_details: true });
assertEquals(si.state.deleted, [2]);
await cleanup(ns, nc);
});
Deno.test("streams - first_seq", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const jsm = await jetstreamManager(nc);
const si = await jsm.streams.add({
name: "test",
first_seq: 50,
subjects: ["foo"],
});
assertEquals(si.config.first_seq, 50);
const pa = await jetstream(nc).publish("foo");
assertEquals(pa.seq, 50);
await cleanup(ns, nc);
});
Deno.test("streams - first_seq fails if wrong server", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const nci = nc as NatsConnectionImpl;
nci.features.update("2.9.2");
const jsm = await jetstreamManager(nc);
await assertRejects(
async () => {
await jsm.streams.add({
name: "test",
first_seq: 50,
subjects: ["foo"],
});
},
Error,
"stream 'first_seq' requires server 2.10.0",
);
await cleanup(ns, nc);
});
Deno.test("streams - persist mode", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.12.0")) {
return;
}
const jsm = await jetstreamManager(nc);
let si = await jsm.streams.add({
name: "A",
subjects: ["a"],
persist_mode: PersistMode.Default,
});
assertEquals(si.config.persist_mode, undefined);
let md = si.config.metadata || {};
assertEquals(md["_nats.req.level"], "0");
si = await jsm.streams.add({
name: "B",
subjects: ["b"],
persist_mode: PersistMode.Async,
});
assertEquals(si.config.persist_mode, PersistMode.Async);
md = si.config.metadata || {};
assertEquals(md["_nats.req.level"], "2");
await assertRejects(
() => {
// @ts-expect-error: testing server rejection
return jsm.streams.update("B", { persist_mode: PersistMode.Default });
},
Error,
"can not change persist mode",
);
await cleanup(ns, nc);
});
--- jetstream/tests/util.ts ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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 { delay } from "@nats-io/nats-core";
import type { Consumer, Stream } from "../src/types.ts";
import { fail } from "@std/assert";
import { StreamImpl } from "../src/jsmstream_api.ts";
import { ConsumerNotFoundError, StreamNotFoundError } from "../src/jserrors.ts";
export function stripNatsMetadata(md?: Record<string, string>) {
if (md) {
for (const p in md) {
if (p.startsWith("_nats.")) {
delete md[p];
}
}
}
}
export async function delayUntilAssetNotFound(
a: Consumer | Stream,
): Promise<void> {
const expected = a instanceof StreamImpl ? "stream" : "consumer";
while (true) {
try {
await a.info();
await delay(20);
} catch (err) {
if (err instanceof ConsumerNotFoundError && expected === "consumer") {
await delay(1000);
break;
}
if (err instanceof StreamNotFoundError && expected === "stream") {
await delay(1000);
break;
}
fail((err as Error).message);
}
}
}
--- jetstream/README.md ---
[![License](https://img.shields.io/badge/Licence-Apache%202.0-blue.svg)](./LICENSE)
![jetstream](https://github.com/nats-io/nats.js/actions/workflows/test.yml/badge.svg)
[![JSDoc](https://img.shields.io/badge/JSDoc-reference-blue)](https://nats-io.github.io/nats.js/jetstream/index.html)
[![JSR](https://jsr.io/badges/@nats-io/jetstream)](https://jsr.io/@nats-io/jetstream)
[![JSR](https://jsr.io/badges/@nats-io/jetstream/score)](https://jsr.io/@nats-io/jetstream)
[![NPM Version](https://img.shields.io/npm/v/%40nats-io%2Fjetstream)](https://www.npmjs.com/package/@nats-io/jetstream)
![NPM Downloads](https://img.shields.io/npm/dt/%40nats-io%2Fjetstream)
![NPM Downloads](https://img.shields.io/npm/dm/%40nats-io%2Fjetstream)
# JetStream
The jetstream module implements the JetStream protocol functionality for
JavaScript clients. JetStream is the NATS persistence engine providing
streaming, message, and worker queues with At-Least-Once semantics.
To use JetStream simply install this library, and create a `jetstream(nc)` or
`jetstreamManager(nc)` with a connection provided by your chosen transport
module. JetStreamManager allows you to interact with the NATS server to manage
JetStream resources. The JetStream client allows you to interact with JetStream
resources.
## Installation
For a quick overview of the libraries and how to install them, see
[runtimes.md](../runtimes.md).
Note that this library is distributed in two different registries:
- npm a node-specific library supporting CJS (`require`) and ESM (`import`)
- jsr a node and other ESM (`import`) compatible runtimes (deno, browser, node)
If your application doesn't use `require`, you can simply depend on the JSR
version.
### NPM
The NPM registry hosts a node-only compatible version of the library
[@nats-io/jetstream](https://www.npmjs.com/package/@nats-io/jetstream)
supporting both CJS and ESM:
```bash
npm install @nats-io/jetstream
```
### JSR
The JSR registry hosts the ESM-only
[@nats-io/jetstream](https://jsr.io/@nats-io/jetstream) version of the library.
```bash
deno add jsr:@nats-io/jetstream
```
```bash
npx jsr add @nats-io/jetstream
```
```bash
yarn dlx jsr add @nats-io/jetstream
```
```bash
bunx jsr add @nats-io/jetstream
```
## Referencing the library
Once you import the library, you can reference in your code as:
```javascript
import { jetstream, jetstreamManager } from "@nats-io/jetstream";
// or in node (only when using CJS)
const { jetstream, jetstreamManager } = require("@nats-io/jetstream");
// using a nats connection:
const js = jetstream(nc);
// and/or
const jsm = await jetstreamManager(nc);
```
# JetStream
JetStream is the NATS persistence engine providing streaming, message, and
worker queues with At-Least-Once semantics. JetStream stores messages in
_streams_. A stream defines how messages are stored and limits such as how long
they persist or how many to keep. To store a message in JetStream, you simply
need to publish to a subject that is associated with a stream.
Messages are replayed from a stream by _consumers_. A consumer configuration
specifies which messages should be presented. For example a consumer may only be
interested in viewing messages from a specific sequence or starting from a
specific time, or having a specific subject. The configuration also specifies if
the server should require messages to be acknowledged and how long to wait for
acknowledgements. The consumer configuration also specifies options to control
the rate at which messages are presented to the client.
For more information about JetStream, please visit the
[JetStream docs](https://docs.nats.io/nats-concepts/jetstream).
## Migration
If you were using an embedded version of JetStream as provided by the npm
nats@^2.0.0 or nats.deno or nats.ws libraries, you will have to import this
library and replace your usages of `NatsConnection#jetstream()` or
`NatsConnection#jetstreamManager()` with `jetstream(nc)` or
`await jetstreamManager(nc)` where you pass your actual connection to the above
functions.
Also note that if you are using [KV](../kv/README.md) or
[ObjectStore](../obj/README.md), these APIs are now provided by a different
libraries `@nats-io/kv` and `@nats-io/obj` respectively. If you are only using
KV or ObjectStore, there's no need to reference this library directly unless you
need to do some specific JetStreamManager API, as both `@nats-io/kv` and
`@nats-io/obj` depend on this library already and use it under the hood.
## JetStreamManager (JSM)
The JetStreamManager provides CRUD functionality to manage streams and consumers
resources. To access a JetStream manager:
```typescript
const jsm = await jetstreamManager(nc);
for await (const si of jsm.streams.list()) {
console.log(si);
}
// add a stream - jetstream can capture nats core messages
const stream = "mystream";
const subj = `mystream.*`;
await jsm.streams.add({ name: stream, subjects: [subj] });
for (let i = 0; i < 100; i++) {
nc.publish(`${subj}.a`, Empty);
}
// find a stream that stores a specific subject:
const name = await jsm.streams.find("mystream.A");
// retrieve info about the stream by its name
const si = await jsm.streams.info(name);
// update a stream configuration
si.config.subjects?.push("a.b");
await jsm.streams.update(si.config);
// get a particular stored message in the stream by sequence
// this is not associated with a consumer
const sm = await jsm.streams.getMessage(stream, { seq: 1 });
console.log(sm.seq);
// delete the 5th message in the stream, securely erasing it
await jsm.streams.deleteMessage(stream, 5);
// purge all messages in the stream, the stream itself remains.
await jsm.streams.purge(stream);
// purge all messages with a specific subject (filter can be a wildcard)
await jsm.streams.purge(stream, { filter: "a.b" });
// purge messages with a specific subject keeping some messages
await jsm.streams.purge(stream, { filter: "a.c", keep: 5 });
// purge all messages with upto (not including seq)
await jsm.streams.purge(stream, { seq: 90 });
// purge all messages with upto sequence that have a matching subject
await jsm.streams.purge(stream, { filter: "a.d", seq: 100 });
// list all consumers for a stream:
const consumers = await jsm.consumers.list(stream).next();
consumers.forEach((ci) => {
console.log(ci);
});
// add a new durable consumer
await jsm.consumers.add(stream, {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
});
// retrieve a consumer's status and configuration
const ci = await jsm.consumers.info(stream, "me");
console.log(ci);
// delete a particular consumer
await jsm.consumers.delete(stream, "me");
```
## JetStream Client
The JetStream client presents an API for adding messages to a stream or
processing messages stored in a stream.
```typescript
// create the stream
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "a", subjects: ["a.*"] });
// create a jetstream client:
const js = jetstream(nc);
// publish a message received by a stream
let pa = await js.publish("a.b");
// jetstream returns an acknowledgement with the
// stream that captured the message, it's assigned sequence
// and whether the message is a duplicate.
const stream = pa.stream;
const seq = pa.seq;
const duplicate = pa.duplicate;
// More interesting is the ability to prevent duplicates
// on messages that are stored in the server. If
// you assign a message ID, the server will keep looking
// for the same ID for a configured amount of time (within a
// configurable time window), and reject messages that
// have the same ID:
await js.publish("a.b", Empty, { msgID: "a" });
// you can also specify constraints that should be satisfied.
// For example, you can request the message to have as its
// last sequence before accepting the new message:
await js.publish("a.b", Empty, { expect: { lastMsgID: "a" } });
await js.publish("a.b", Empty, { expect: { lastSequence: 3 } });
// save the last sequence for this publish
pa = await js.publish("a.b", Empty, { expect: { streamName: "a" } });
// you can also mix the above combinations
// this stream here accepts wildcards, you can assert that the
// last message sequence recorded on a particular subject matches:
const buf: Promise<PubAck>[] = [];
for (let i = 0; i < 100; i++) {
buf.push(js.publish("a.a", Empty));
}
await Promise.all(buf);
// if additional "a.b" has been recorded, this will fail
await js.publish("a.b", Empty, { expect: { lastSubjectSequence: pa.seq } });
```
### Processing Messages
The JetStream API provides different mechanisms for retrieving messages. Each
mechanism offers a different "buffering strategy" that provides advantages that
map to how your application works and processes messages.
#### Basics
To understand how these strategies differentiate, let's review some aspects of
processing a stream which will help you choose and design a strategy that works
for your application.
First and foremost, processing a stream is different from processing NATS core
messages:
In NATS core, you are presented with a message whenever a message is published
to a subject that you have subscribed to. When you process a stream you can
filter messages found on a stream to those matching subjects that interest you,
but the rate of delivery can be much higher, as the stream could store many more
messages that match your consumer than you would normally receive in a core NATS
subscription. When processing a stream, you can simulate the original rate at
which messages were ingested, but typically messages are processed "as fast as
possible". This means that a client could be overwhelmed by the number of
messages presented by the server.
In NATS core, if you want to ensure that your message was received as intended,
you publish a request. The receiving client can then respond an acknowledgement
or return some result. When processing a stream, the consumer configuration
dictates whether messages sent to the consumer should be acknowledged or not.
The server tracks acknowledged messages and knows which messages the consumer
has not seen or that may need to be resent due to a missing acknowledgement. By
default, clients have 30 seconds to respond or extend the acknowledgement. If a
message fails to be acknowledged in time, the server will resend the message
again. This functionality has a very important implications. Consumers should
not buffer more messages than they can process and acknowledge within the
acknowledgement window.
Lastly, the NATS server protects itself and when it detects that a client
connection is not draining data quickly enough, it disconnects it to prevent the
degradation from impacting other clients.
Given these competing conditions, the JetStream APIs allow a client to express
not only the buffering strategy for reading a stream at a pace the client can
sustain, but also how the reading happens.
JetStream allows the client to:
- Request the next message (one message at time)
- Request the next N messages
- Request and maintain a buffer of N messages
The first two options allow the client to control and manage its buffering
manually, when the client is done processing the messages, it can at its
discretion to request additional messages or not.
The last option auto buffers messages for the client controlling the message and
data rates. The client specifies how many messages it wants to receive, and as
it consumes them, the library requests additional messages in an effort to
prevent the consumer from stalling, and thus maximize performance.
#### Retrieving the Consumer
Before messages can be read from a stream, the consumer should have been created
using the JSM APIs as shown above. After the consumer exists, the client simply
retrieves it:
```typescript
const stream = "a";
const consumer = "a";
const js = jetstream(nc);
// retrieve an existing consumer
const c = await js.consumers.get(stream, consumer);
// getting an ordered consumer requires no name
// as the library will create it
const oc = await js.consumers.get(stream);
```
[full example](examples/01_consumers.js)
With the consumer in hand, the client can start reading messages using whatever
API is appropriate for the application.
#### JsMsg
All messages are `JsMsg`s, a `JsMsg` is a wrapped `Msg` - it has all the
standard fields in a NATS `Msg`, a `JsMsg` and provides functionality for
inspecting metadata encoded into the message's reply subject. This metadata
includes:
- sequence (`seq`)
- `redelivered`
- full info via info which shows the delivery sequence, and how many messages
are pending among other things.
- Multiple ways to acknowledge a message:
- `ack()`
- `nak(millis?)` - like ack, but tells the server you failed to process it,
and it should be resent. If a number is specified, the message will be
resent after the specified value. The additional argument only supported on
server versions 2.7.1 or greater
- `working()` - informs the server that you are still working on the message
and thus prevent receiving the message again as a redelivery.
- `term()` - specifies that you failed to process the message and instructs
the server to not send it again (to any consumer).
#### Requesting a single message
The simplest mechanism to process messages is to request a single message. This
requires sending a request to the server. When no messages are available, the
request will return a `null` message. Since the client is explicitly requesting
the message, it is in full control of when to ask for another.
The request will reject if there's an exceptional condition, such as when the
underlying consumer or stream is deleted after retrieving the consumer instance,
or by a change to the clients subject permissions that prevent interactions with
JetStream, or JetStream is not available.
```typescript
const m = await c.next();
if (m) {
console.log(m.subject);
m.ack();
} else {
console.log(`didn't get a message`);
}
```
[full example](examples/02_next.js)
The operation takes an optional argument. Currently, the only option is an
`expires` option which specifies the maximum number of milliseconds to wait for
a message. This is defaulted to 30 seconds. Note this default is a good value
because it gives the opportunity to retrieve a message without excessively
polling the server (which could affect the server performance).
`next()` should be your go-to API when implementing services that process
messages or work queue streams, as it allows you to horizontally scale your
processing simply by starting and managing multiple processes.
Keep in mind that you can also simulate `next()` in a loop and have the library
provide an iterator by using `consume()`. That API be explained later in the
document.
#### Fetching batch of messages
You can request multiple messages at time. The request is a _long_ poll. So it
remains open and keep dispatching you messages until the desired number of
messages is received, or the `expires` time triggers. This means that the number
of messages you request is only a hint and it is just the upper bound on the
number of messages you will receive. By default `fetch()` will retrieve 100
messages in a batch, but you can control it as shown in this example:
```typescript
for (let i = 0; i < 3; i++) {
let messages = await c.fetch({ max_messages: 4, expires: 2000 });
for await (const m of messages) {
m.ack();
}
console.log(`batch completed: ${messages.getProcessed()} msgs processed`);
}
```
[full example](examples/03_batch.js)
Fetching batches is useful if you parallelize a number of requests to take
advantage of the asynchronous processing of data with a number of workers for
example. To get a new batch simply fetch again.
#### Consuming messages
In the previous two sections messages were retrieved manually by your
application, and allowed you to remain in control of whether you wanted to
receive one or more messages with a single request.
A third option automates the process of re-requesting more messages. The library
will monitor messages it yields, and request additional messages to maintain
your processing momentum. The operation will continue until you `break` or call
`stop()` the iterator.
The `consume()` operation maintains an internal buffer of messages that auto
refreshes whenever 50% of the initial buffer is consumed. This allows the client
to process messages in a loop forever.
```typescript
const messages = await c.consume();
for await (const m of messages) {
console.log(m.seq);
m.ack();
}
```
[full example](examples/04_consume.js)
Note that it is possible to do an automatic version of `next()` by simply
setting the maximum number of messages to buffer to `1`:
```typescript
const messages = await c.consume({ max_messages: 1 });
for await (const m of messages) {
console.log(m.seq);
m.ack();
}
```
The API simply asks for one message, but as soon as that message is processed or
the request expires, another is requested.
#### Horizontally Scaling Consumers (Previously known as Queue Consumer)
Scaling processing in a consumer is simply calling `next()/fetch()/consume()` on
a shared consumer. When horizontally scaling limiting the number of buffered
messages will likely yield better results as requests will be mapped 1-1 with
the processes preventing some processes from booking more messages while others
are idle.
A more balanced approach is to simply use `next()` or
`consume({max_messages: 1})`. This makes it so that if you start or stop
processes you automatically scale your processing, and if there's a failure you
won't delay the redelivery of messages that were in flight but never processed
by the client.
#### Callbacks
The `consume()` API normally use iterators for processing. If you want to
specify a callback, you can:
```typescript
const c = await js.consumers.get(stream, consumer);
console.log("waiting for messages");
await c.consume({
callback: (m) => {
console.log(m.seq);
m.ack();
},
});
```
#### Iterators, Callbacks, and Concurrency
The `consume()` and `fetch()` APIs yield a `ConsumerMessages`. One thing to keep
in mind is that the iterator for processing messages will not yield a new
message until the body of the loop completes.
Compare:
```typescript
const msgs = await c.consume();
for await (const m of msgs) {
try {
// this is simplest but could also create a head-of-line blocking
// as no other message on the current buffer will be processed until
// this iteration completes
await asyncFn(m);
m.ack();
} catch (err) {
m.nack();
}
}
```
With
```typescript
const msgs = await c.consume();
for await (const m of msgs) {
// this potentially has the problem of generating a very large number
// of async operations which may exceed the limits of the runtime
asyncFn(m)
.then(() => {
m.ack();
})
.catch((err) => {
m.nack();
});
}
```
In the first scenario, the processing is sequential. The second scenario is
concurrent.
Both of these behaviors are standard JavaScript, but you can use this to your
advantage. You can improve latency by not awaiting, but that will require a more
complex handling as you'll need to restrict and limit how many concurrent
operations you create and thus avoid hitting limits in your runtime.
One possible strategy is to use `fetch()`, and process asynchronously without
awaiting as you process message you'll need to implement accounting to track
when you should re-fetch, but a far simpler solution is to use `next()`, process
asynchronously and scale by horizontally managing processes instead.
Here's a solution that introduces a rate limiter:
```typescript
const messages = await c.consume({ max_messages: 10 });
// this rate limiter is just example code, do not use in production
const rl = new SimpleMutex(5);
async function schedule(m: JsMsg): Promise<void> {
// pretend to do work
await delay(1000);
m.ack();
console.log(`${m.seq}`);
}
for await (const m of messages) {
// block reading messages until we have capacity
await rl.lock();
schedule(m)
.catch((err) => {
console.log(`failed processing: ${err.message}`);
m.nak();
})
.finally(() => {
// unblock, allowing a lock to resolve
rl.unlock();
});
}
```
[full example](examples/07_consume_jobs.js)
#### Processing a Stream
Here's a contrived example on how a process examines a stream. Once all the
messages in the stream are processed the consumer is deleted.
Our processing is simply creating a frequency table of the second token in the
subject, printing the results after it is done.
```typescript
const messages = await c.consume();
const data = new Map<string, number>();
for await (const m of messages) {
const chunks = m.subject.split(".");
const v = data.get(chunks[1]) || 0;
data.set(chunks[1], v + 1);
m.ack();
// if no pending, then we have processed the stream
// and we can break
if (m.info.pending === 0) {
break;
}
}
// we can safely delete the consumer
await c.delete();
// and print results
const keys = [];
for (const k of data.keys()) {
keys.push(k);
}
keys.sort();
keys.forEach((k) => {
console.log(`${k}: ${data.get(k)}`);
});
```
[full example](examples/08_consume_process.js)
### Heartbeats
Since JetStream is available through NATS it is possible for your network
connection to not be directly connected to the JetStream server providing you
with messages. In those cases, it is possible for a JetStream server to go away,
and for the client to not notice it. This would mean your client would sit idle
thinking there are no messages, when in reality the JetStream service may have
restarted elsewhere.
For most issues, the client will auto-recover, but if it doesn't, and it starts
reporting `HeartbeatsMissed` statuses, you will want to `stop()` the
`ConsumerMessages`, and recreate it. Note that in the example below this is done
in a loop for example purposes:
```typescript
while (true) {
const messages = await c.consume({ max_messages: 1 });
// watch the to see if the consume operation misses heartbeats
(async () => {
for await (const s of await messages.status()) {
switch (s.type) {
case "heartbeats_missed":
// you can decide how many heartbeats you are willing to miss
const n = s.data as number;
console.log(`${s.count} heartbeats missed`);
if (s.count === 2) {
// by calling `stop()` the message processing loop ends
// in this case this is wrapped by a loop, so it attempts
// to re-setup the consume
messages.stop();
}
}
}
})();
for await (const m of messages) {
console.log(`${m.seq} ${m?.subject}`);
m.ack();
}
}
```
[full example](examples/06_heartbeats.js)
Note that while the heartbeat interval is configurable, you shouldn't change it.
#### JetStream Ordered Consumers
An Ordered Consumer is a specialized consumer that ensures that messages are
presented in the correct order. If a message is out of order, the consumer is
recreated at the expected sequence.
The underlying consumer is created, managed and destroyed under the covers, so
you only have to specify the stream and possible startup options, or filtering:
```typescript
// note the name of the consumer is not specified
const a = await js.consumers.get(name);
const b = await js.consumers.get(name, { filterSubjects: [`${name}.a`] });
```
Note that uses of the consumer API for reading messages are checked for
concurrency preventing the ordered consumer from having operations initiated
with `fetch()` and `consume()` or `next()` while another is active.
--- kv/tests/kv_test.ts ---
/*
* Copyright 2021-2024 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 {
collect,
compare,
deadline,
deferred,
delay,
Empty,
nanos,
nuid,
parseSemVer,
syncIterator,
} from "@nats-io/nats-core/internal";
import type {
ConnectionOptions,
NatsConnection,
NatsConnectionImpl,
Payload,
QueuedIterator,
} from "@nats-io/nats-core/internal";
import {
DirectMsgHeaders,
DiscardPolicy,
jetstream,
jetstreamManager,
StorageType,
} from "@nats-io/jetstream";
import type {
JetStreamOptions,
PushConsumer,
} from "@nats-io/jetstream/internal";
import {
assert,
assertArrayIncludes,
assertEquals,
assertExists,
assertIsError,
assertRejects,
assertThrows,
} from "@std/assert";
import type { KV, KvEntry, KvOptions, KvWatchEntry } from "../src/types.ts";
import type { Bucket } from "../src/mod.ts";
import { KvWatchInclude } from "../src/types.ts";
import { Base64KeyCodec, NoopKvCodecs } from "../src/mod.ts";
import { kvPrefix, validateBucket, validateKey } from "../src/internal_mod.ts";
import {
cleanup,
connect,
jetstreamServerConf,
Lock,
NatsServer,
notCompatible,
setup,
} from "test_helpers";
import type { QueuedIteratorImpl } from "@nats-io/nats-core/internal";
import {
defaultBucketOpts,
hasWildcards,
Kvm,
validateSearchKey,
} from "../src/kv.ts";
import { assertBetween, flakyTest } from "../../test_helpers/mod.ts";
Deno.test("kv - Base64KeyCodec encodes and decodes", () => {
const codec = Base64KeyCodec();
// Simple key
const encoded = codec.encode("foo");
assertEquals(encoded, btoa("foo"));
assertEquals(codec.decode(encoded), "foo");
// Key with dots
const multiPart = "foo.bar.baz";
const encodedMulti = codec.encode(multiPart);
assertEquals(encodedMulti, `${btoa("foo")}.${btoa("bar")}.${btoa("baz")}`);
assertEquals(codec.decode(encodedMulti), multiPart);
// Wildcard * should not be encoded
const withStar = "foo.*.baz";
const encodedStar = codec.encode(withStar);
assertEquals(encodedStar, `${btoa("foo")}.*.${btoa("baz")}`);
assertEquals(codec.decode(encodedStar), withStar);
// Wildcard > should not be encoded
const withGt = "foo.bar.>";
const encodedGt = codec.encode(withGt);
assertEquals(encodedGt, `${btoa("foo")}.${btoa("bar")}.>`);
assertEquals(codec.decode(encodedGt), withGt);
// Multiple wildcards
const mixed = "foo.*.bar.>";
const encodedMixed = codec.encode(mixed);
assertEquals(encodedMixed, `${btoa("foo")}.*.${btoa("bar")}.>`);
assertEquals(codec.decode(encodedMixed), mixed);
});
Deno.test("kv - NoopKvCodecs passes through unchanged", () => {
const codecs = NoopKvCodecs();
// Key codec is noop
assertEquals(codecs.key.encode("foo"), "foo");
assertEquals(codecs.key.decode("bar"), "bar");
// Value codec converts string to Uint8Array
const strValue = "test";
const encoded = codecs.value.encode(strValue);
assert(encoded instanceof Uint8Array);
assertEquals(encoded, new TextEncoder().encode(strValue));
// Value codec passes through Uint8Array unchanged
const bytes = new Uint8Array([1, 2, 3]);
assertEquals(codecs.value.encode(bytes), bytes);
// Value decode returns Uint8Array unchanged
assertEquals(codecs.value.decode(bytes), bytes);
});
Deno.test("kv - defaultBucketOpts returns expected defaults", () => {
const opts = defaultBucketOpts();
assertEquals(opts.replicas, 1);
assertEquals(opts.history, 1);
assertEquals(opts.timeout, 2000);
assertEquals(opts.max_bytes, -1);
assertEquals(opts.maxValueSize, -1);
assertEquals(opts.storage, StorageType.File);
assertExists(opts.codec);
});
Deno.test("kv - hasWildcards detects wildcards", () => {
// No wildcards
assertEquals(hasWildcards("foo"), false);
assertEquals(hasWildcards("foo.bar"), false);
assertEquals(hasWildcards("foo.bar.baz"), false);
// With * wildcard
assertEquals(hasWildcards("foo.*"), true);
assertEquals(hasWildcards("*.bar"), true);
assertEquals(hasWildcards("foo.*.baz"), true);
// With > wildcard at end
assertEquals(hasWildcards("foo.>"), true);
assertEquals(hasWildcards("foo.bar.>"), true);
// Combined wildcards
assertEquals(hasWildcards("foo.*.>"), true);
// Invalid: > not at end
assertThrows(
() => hasWildcards("foo.>.bar"),
Error,
"invalid key",
);
// Invalid: starts with .
assertThrows(
() => hasWildcards(".foo"),
Error,
"invalid key",
);
// Invalid: ends with .
assertThrows(
() => hasWildcards("foo."),
Error,
"invalid key",
);
});
Deno.test("kv - validateSearchKey validates search keys", () => {
// Valid search keys (including wildcards)
const good = [
"foo",
"foo.bar",
"foo.*",
"*.bar",
"foo.>",
"foo.*.bar",
"a.b.>",
"*",
">",
];
for (const v of good) {
try {
validateSearchKey(v);
} catch (err) {
throw new Error(
`expected '${v}' to be a valid search key, but was rejected: ${
(err as Error).message
}`,
);
}
}
// Invalid search keys
const bad = [
".foo",
"foo.",
".foo.bar",
"foo.bar.",
"foo..bar",
".",
"foo!bar",
"foo$bar",
"foo bar",
];
for (const v of bad) {
assertThrows(
() => validateSearchKey(v),
Error,
"invalid key",
`expected '${v}' to be invalid search key`,
);
}
});
Deno.test("kv - decodeKey decodes encoded keys", () => {
const codec = Base64KeyCodec();
const bucket = {
codec: { key: codec },
decodeKey(ekey: string): string {
const chunks: string[] = [];
for (const t of ekey.split(".")) {
switch (t) {
case ">":
case "*":
chunks.push(t);
break;
default:
chunks.push(this.codec.key.decode(t));
break;
}
}
return chunks.join(".");
},
};
// Simple key
assertEquals(bucket.decodeKey(btoa("foo")), "foo");
// Multi-part key
const encoded = `${btoa("foo")}.${btoa("bar")}.${btoa("baz")}`;
assertEquals(bucket.decodeKey(encoded), "foo.bar.baz");
// Wildcards should not be decoded
const withStar = `${btoa("foo")}.*.${btoa("baz")}`;
assertEquals(bucket.decodeKey(withStar), "foo.*.baz");
const withGt = `${btoa("foo")}.${btoa("bar")}.>`;
assertEquals(bucket.decodeKey(withGt), "foo.bar.>");
// Mixed wildcards
const mixed = `${btoa("a")}.*.${btoa("b")}.>`;
assertEquals(bucket.decodeKey(mixed), "a.*.b.>");
});
Deno.test("kv - key validation", () => {
const bad = [
" x y",
"x ",
"x!",
"xx$",
"*",
">",
"x.>",
"x.*",
".",
".x",
".x.",
"x.",
];
for (const v of bad) {
assertThrows(
() => {
validateKey(v);
},
Error,
"invalid key",
`expected '${v}' to be invalid key`,
);
}
const good = [
"foo",
"_foo",
"-foo",
"_kv_foo",
"foo123",
"123",
"a/b/c",
"a.b.c",
];
for (const v of good) {
try {
validateKey(v);
} catch (err) {
throw new Error(
`expected '${v}' to be a valid key, but was rejected: ${
(err as Error).message
}`,
);
}
}
});
Deno.test("kv - bucket name validation", () => {
const bad = [" B", "!", "x/y", "x>", "x.x", "x.*", "x.>", "x*", "*", ">"];
for (const v of bad) {
assertThrows(
() => {
validateBucket(v);
},
Error,
"invalid bucket name",
`expected '${v}' to be invalid bucket name`,
);
}
const good = [
"B",
"b",
"123",
"1_2_3",
"1-2-3",
];
for (const v of good) {
try {
validateBucket(v);
} catch (err) {
throw new Error(
`expected '${v}' to be a valid bucket name, but was rejected: ${
(err as Error).message
}`,
);
}
}
});
Deno.test("kv - list kv", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const kvm = new Kvm(nc);
let kvs = await kvm.list().next();
assertEquals(kvs.length, 0);
await kvm.create("kv");
kvs = await kvm.list().next();
assertEquals(kvs.length, 1);
assertEquals(kvs[0].bucket, `kv`);
// test names as well
const names = await (await jsm.streams.names()).next();
assertEquals(names.length, 2);
assertArrayIncludes(names, ["A", "KV_kv"]);
await cleanup(ns, nc);
});
Deno.test("kv - init creates stream", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const jsm = await jetstreamManager(nc);
let streams = await jsm.streams.list().next();
assertEquals(streams.length, 0);
const n = nuid.next();
await new Kvm(nc).create(n);
streams = await jsm.streams.list().next();
assertEquals(streams.length, 1);
assertEquals(streams[0].config.name, `KV_${n}`);
await cleanup(ns, nc);
});
Deno.test("kv - bind to existing KV", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const jsm = await jetstreamManager(nc);
let streams = await jsm.streams.list().next();
assertEquals(streams.length, 0);
const n = nuid.next();
const js = jetstream(nc);
await new Kvm(js).create(n, { history: 10 });
streams = await jsm.streams.list().next();
assertEquals(streams.length, 1);
assertEquals(streams[0].config.name, `KV_${n}`);
const kv = await new Kvm(js).open(n);
const status = await kv.status();
assertEquals(status.bucket, `${n}`);
await crud(kv as Bucket);
await cleanup(ns, nc);
});
async function crud(bucket: Bucket): Promise<void> {
const status = await bucket.status();
assertEquals(status.values, 0);
assertEquals(status.history, 10);
assertEquals(status.bucket, bucket.bucket);
assertEquals(status.ttl, 0);
assertEquals(status.streamInfo.config.name, `${kvPrefix}${bucket.bucket}`);
assertEquals(status.storage, StorageType.File);
assertEquals(status.replicas, 1);
assertEquals(status.maxValueSize, -1);
assertEquals(status.placement.cluster, "");
assertEquals(status.republish.src, "");
assertEquals(status.republish.dest, "");
await bucket.put("k", "hello");
let r = await bucket.get("k");
assertEquals(r!.string(), "hello");
await bucket.put("k", "bye");
r = await bucket.get("k");
assertEquals(r!.string(), "bye");
await bucket.delete("k");
r = await bucket.get("k");
assert(r);
assertEquals(r.operation, "DEL");
const buf: string[] = [];
const values = await bucket.history();
for await (const r of values) {
buf.push(r.string());
}
assertEquals(values.getProcessed(), 3);
assertEquals(buf.length, 3);
assertEquals(buf[0], "hello");
assertEquals(buf[1], "bye");
assertEquals(buf[2], "");
const pr = await bucket.purgeBucket();
assertEquals(pr.purged, 3);
assert(pr.success);
const ok = await bucket.destroy();
assert(ok);
const streams = await bucket.jsm.streams.list().next();
assertEquals(streams.length, 0);
}
Deno.test("kv - crud", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const n = nuid.next();
const js = jetstream(nc);
const bucket = await new Kvm(js).create(n, { history: 10 }) as Bucket;
await crud(bucket);
await cleanup(ns, nc);
});
Deno.test("kv - codec crud", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const jsm = await jetstreamManager(nc);
const streams = await jsm.streams.list().next();
assertEquals(streams.length, 0);
const n = nuid.next();
const js = jetstream(nc);
const bucket = await new Kvm(js).create(n, {
history: 10,
codec: {
key: Base64KeyCodec(),
value: NoopKvCodecs().value,
},
}) as Bucket;
await crud(bucket);
await cleanup(ns, nc);
});
Deno.test("kv - history", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const n = nuid.next();
const js = jetstream(nc);
const bucket = await new Kvm(js).create(n, { history: 2 });
let status = await bucket.status();
assertEquals(status.values, 0);
assertEquals(status.history, 2);
await bucket.put("A", Empty);
await bucket.put("A", Empty);
await bucket.put("A", Empty);
await bucket.put("A", Empty);
status = await bucket.status();
assertEquals(status.values, 2);
await cleanup(ns, nc);
});
Deno.test("kv - history multiple keys", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const n = nuid.next();
const js = jetstream(nc);
const bucket = await new Kvm(js).create(n, { history: 2 });
await bucket.put("A", Empty);
await bucket.put("B", Empty);
await bucket.put("C", Empty);
await bucket.put("D", Empty);
const iter = await bucket.history({ key: ["A", "D"] });
const buf = [];
for await (const e of iter) {
buf.push(e.key);
}
assertEquals(buf.length, 2);
assertArrayIncludes(buf, ["A", "D"]);
await cleanup(ns, nc);
});
Deno.test("kv - cleanups/empty", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const nci = nc as NatsConnectionImpl;
const n = nuid.next();
const js = jetstream(nc);
const bucket = await new Kvm(js).create(n);
assertEquals(await bucket.get("x"), null);
const h = await bucket.history();
assertEquals(h.getReceived(), 0);
const keys = await collect(await bucket.keys());
assertEquals(keys.length, 0);
// mux should be created
const min = nci.protocol.subscriptions.getMux() ? 1 : 0;
assertEquals(nci.protocol.subscriptions.subs.size, min);
await cleanup(ns, nc);
});
Deno.test("kv - history and watch cleanup", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const n = nuid.next();
const js = jetstream(nc);
const bucket = await new Kvm(js).create(n);
await bucket.put("a", Empty);
await bucket.put("b", Empty);
await bucket.put("c", Empty);
const h = await bucket.history();
for await (const _e of h) {
// aborted
break;
}
const w = await bucket.watch({});
setTimeout(() => {
bucket.put("hello", "world");
}, 250);
for await (const e of w) {
if (e.isUpdate) {
break;
}
}
await delay(500);
// need to give some time for promises to be resolved
const nci = nc as NatsConnectionImpl;
const min = nci.protocol.subscriptions.getMux() ? 1 : 0;
assertEquals(nci.protocol.subscriptions.size(), min);
await cleanup(ns, nc);
});
Deno.test("kv - bucket watch", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const n = nuid.next();
const js = jetstream(nc);
const b = await new Kvm(js).create(n, { history: 10 });
const m: Map<string, string> = new Map();
const iter = await b.watch();
const done = (async () => {
for await (const r of iter) {
if (r.operation === "DEL") {
m.delete(r.key);
} else {
m.set(r.key, r.string());
}
if (r.key === "x") {
break;
}
}
})();
await b.put("a", "1");
await b.put("b", "2");
await b.put("c", "3");
await b.put("a", "2");
await b.put("b", "3");
await b.delete("c");
await b.put("x", Empty);
await done;
await delay(0);
assertEquals(iter.getProcessed(), 7);
assertEquals(m.get("a"), "2");
assertEquals(m.get("b"), "3");
assert(!m.has("c"));
assertEquals(m.get("x"), "");
const nci = nc as NatsConnectionImpl;
// mux should be created
const min = nci.protocol.subscriptions.getMux() ? 1 : 0;
assertEquals(nci.protocol.subscriptions.subs.size, min);
await cleanup(ns, nc);
});
async function keyWatch(bucket: Bucket): Promise<void> {
const m: Map<string, string> = new Map();
const iter = await bucket.watch({ key: "a.>" });
const done = (async () => {
for await (const r of iter) {
if (r.operation === "DEL") {
m.delete(r.key);
} else {
m.set(r.key, r.string());
}
if (r.key === "a.x") {
break;
}
}
})();
await bucket.put("a.b", "1");
await bucket.put("b.b", "2");
await bucket.put("c.b", "3");
await bucket.put("a.b", "2");
await bucket.put("b.b", "3");
await bucket.delete("c.b");
await bucket.put("a.x", Empty);
await done;
assertEquals(iter.getProcessed(), 3);
assertEquals(m.get("a.b"), "2");
assertEquals(m.get("a.x"), "");
}
Deno.test("kv - key watch", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const bucket = await new Kvm(js).create(nuid.next()) as Bucket;
await keyWatch(bucket);
await delay(0);
const nci = nc as NatsConnectionImpl;
const min = nci.protocol.subscriptions.getMux() ? 1 : 0;
assertEquals(nci.protocol.subscriptions.subs.size, min);
await cleanup(ns, nc);
});
Deno.test("kv - codec key watch", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const bucket = await new Kvm(js).create(nuid.next(), {
history: 10,
codec: {
key: Base64KeyCodec(),
value: NoopKvCodecs().value,
},
}) as Bucket;
await keyWatch(bucket);
await delay(0);
const nci = nc as NatsConnectionImpl;
const min = nci.protocol.subscriptions.getMux() ? 1 : 0;
assertEquals(nci.protocol.subscriptions.subs.size, min);
await cleanup(ns, nc);
});
async function keys(b: Bucket): Promise<void> {
await b.put("a", "1");
await b.put("b", "2");
await b.put("c.c.c", "3");
await b.put("a", "2");
await b.put("b", "3");
await b.delete("c.c.c");
await b.put("x", Empty);
const keys = await collect(await b.keys());
assertEquals(keys.length, 3);
assertArrayIncludes(keys, ["a", "b", "x"]);
}
Deno.test("kv - keys", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next());
await keys(b as Bucket);
const nci = nc as NatsConnectionImpl;
const min = nci.protocol.subscriptions.getMux() ? 1 : 0;
assertEquals(nci.protocol.subscriptions.subs.size, min);
await cleanup(ns, nc);
});
Deno.test("kv - codec keys", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next(), {
history: 10,
codec: {
key: Base64KeyCodec(),
value: NoopKvCodecs().value,
},
});
await keys(b as Bucket);
const nci = nc as NatsConnectionImpl;
const min = nci.protocol.subscriptions.getMux() ? 1 : 0;
assertEquals(nci.protocol.subscriptions.subs.size, min);
await cleanup(ns, nc);
});
Deno.test("kv - ttl", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next(), { ttl: 1000 }) as Bucket;
const jsm = await jetstreamManager(nc);
const si = await jsm.streams.info(b.stream);
assertEquals(si.config.max_age, nanos(1000));
assertEquals(await b.get("x"), null);
await b.put("x", "hello");
const e = await b.get("x");
assert(e);
assertEquals(e.string(), "hello");
await delay(5000);
assertEquals(await b.get("x"), null);
await cleanup(ns, nc);
});
Deno.test("kv - no ttl", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next()) as Bucket;
await b.put("x", "hello");
let e = await b.get("x");
assert(e);
assertEquals(e.string(), "hello");
await delay(1500);
e = await b.get("x");
assert(e);
assertEquals(e.string(), "hello");
await cleanup(ns, nc);
});
Deno.test("kv - complex key", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next()) as Bucket;
await b.put("x.y.z", "hello");
const e = await b.get("x.y.z");
assertEquals(e?.string(), "hello");
const d = deferred<KvEntry>();
let iter = await b.watch({ key: "x.y.>" });
await (async () => {
for await (const r of iter) {
d.resolve(r);
break;
}
})();
const vv = await d;
assertEquals(vv.string(), "hello");
const dd = deferred<KvEntry>();
iter = await b.history({ key: "x.y.>" });
await (async () => {
for await (const r of iter) {
dd.resolve(r);
break;
}
})();
const vvv = await dd;
assertEquals(vvv.string(), "hello");
await cleanup(ns, nc);
});
Deno.test("kv - remove key", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next()) as Bucket;
await b.put("a.b", "ab");
let v = await b.get("a.b");
assert(v);
assertEquals(v.string(), "ab");
await b.purge("a.b");
v = await b.get("a.b");
assert(v);
assertEquals(v.operation, "PURGE");
const status = await b.status();
// the purged value
assertEquals(status.values, 1);
await cleanup(ns, nc);
});
Deno.test("kv - remove subkey", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next()) as Bucket;
await b.put("a", Empty);
await b.put("a.b", Empty);
await b.put("a.c", Empty);
let keys = await collect(await b.keys());
assertEquals(keys.length, 3);
assertArrayIncludes(keys, ["a", "a.b", "a.c"]);
await b.delete("a.*");
keys = await collect(await b.keys());
assertEquals(keys.length, 1);
assertArrayIncludes(keys, ["a"]);
await cleanup(ns, nc);
});
Deno.test("kv - create key", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next()) as Bucket;
await b.create("a", Empty);
await assertRejects(
async () => {
await b.create("a", "a");
},
Error,
"wrong last sequence: 1",
undefined,
);
await cleanup(ns, nc);
});
Deno.test("kv - update key", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next()) as Bucket;
const seq = await b.create("a", Empty);
await assertRejects(
async () => {
await b.update("a", "a", 100);
},
Error,
"wrong last sequence: 1",
undefined,
);
await b.update("a", "b", seq);
await cleanup(ns, nc);
});
Deno.test("kv - internal consumer", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
async function getCount(name: string): Promise<number> {
const js = jetstream(nc);
const b = await new Kvm(js).create(name) as Bucket;
const watch = await b.watch() as QueuedIteratorImpl<unknown>;
const ci = await (watch._data as PushConsumer).info(true);
return ci.num_pending || 0;
}
const name = nuid.next();
const js = jetstream(nc);
const b = await new Kvm(js).create(name) as Bucket;
assertEquals(await getCount(name), 0);
await b.put("a", Empty);
assertEquals(await getCount(name), 1);
await cleanup(ns, nc);
});
Deno.test("kv - is wildcard delete implemented", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const name = nuid.next();
const js = jetstream(nc);
const b = await new Kvm(js).create(name, { history: 10 }) as Bucket;
await b.put("a", Empty);
await b.put("a.a", Empty);
await b.put("a.b", Empty);
await b.put("a.b.c", Empty);
let keys = await collect(await b.keys());
assertEquals(keys.length, 4);
await b.delete("a.*");
keys = await collect(await b.keys());
assertEquals(keys.length, 2);
// this was a manual delete, so we should have tombstones
// for all the deleted entries
let deleted = 0;
const w = await b.watch();
await (async () => {
for await (const e of w) {
if (e.operation === "DEL") {
deleted++;
}
if (e.delta === 0) {
break;
}
}
})();
assertEquals(deleted, 2);
await nc.close();
await ns.stop();
});
Deno.test("kv - delta", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const name = nuid.next();
const js = jetstream(nc);
const b = await new Kvm(js).create(name) as Bucket;
await b.put("a", Empty);
await b.put("a.a", Empty);
await b.put("a.b", Empty);
await b.put("a.b.c", Empty);
const w = await b.history();
await (async () => {
let i = 0;
let delta = 4;
for await (const e of w) {
assertEquals(e.revision, ++i);
assertEquals(e.delta, --delta);
if (e.delta === 0) {
break;
}
}
})();
await nc.close();
await ns.stop();
});
Deno.test("kv - watch and history headers only", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create("bucket") as Bucket;
await b.put("key1", "aaa");
async function getEntry(
qip: Promise<QueuedIterator<KvEntry>>,
): Promise<KvEntry> {
const iter = await qip;
const p = deferred<KvEntry>();
(async () => {
for await (const e of iter) {
p.resolve(e);
break;
}
})().then();
return p;
}
async function check(pe: Promise<KvEntry>): Promise<void> {
const e = await pe;
assertEquals(e.key, "key1");
assertEquals(e.value, Empty);
assertEquals(e.length, 3);
}
await check(getEntry(b.watch({ key: "key1", headers_only: true })));
await check(getEntry(b.history({ key: "key1", headers_only: true })));
await cleanup(ns, nc);
});
Deno.test("kv - mem and file", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const d = await new Kvm(js).create("default") as Bucket;
assertEquals((await d.status()).storage, StorageType.File);
const f = await new Kvm(js).create("file", {
storage: StorageType.File,
}) as Bucket;
assertEquals((await f.status()).storage, StorageType.File);
const m = await new Kvm(js).create("mem", {
storage: StorageType.Memory,
}) as Bucket;
assertEquals((await m.status()).storage, StorageType.Memory);
await cleanup(ns, nc);
});
Deno.test("kv - example", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("testing", { history: 5 });
// create an entry - this is similar to a put, but will fail if the
// key exists
await kv.create("hello.world", "hi");
// Values in KV are stored as KvEntries:
// {
// bucket: string,
// key: string,
// value: Uint8Array,
// created: Date,
// revision: number,
// delta?: number,
// operation: "PUT"|"DEL"|"PURGE"
// }
// The operation property specifies whether the value was
// updated (PUT), deleted (DEL) or purged (PURGE).
// you can monitor values modification in a KV by watching.
// You can watch specific key subset or everything.
// Watches start with the latest value for each key in the
// set of keys being watched - in this case all keys
const watch = await kv.watch();
(async () => {
for await (const _e of watch) {
// do something with the change
}
})().then();
// update the entry
await kv.put("hello.world", "world");
// retrieve the KvEntry storing the value
// returns null if the value is not found
const e = await kv.get("hello.world");
assert(e);
// initial value of "hi" was overwritten above
assertEquals(e.string(), "world");
const buf: string[] = [];
const keys = await kv.keys();
await (async () => {
for await (const k of keys) {
buf.push(k);
}
})();
assertEquals(buf.length, 1);
assertEquals(buf[0], "hello.world");
const h = await kv.history({ key: "hello.world" });
await (async () => {
for await (const _e of h) {
// do something with the historical value
// you can test e.operation for "PUT", "DEL", or "PURGE"
// to know if the entry is a marker for a value set
// or for a deletion or purge.
}
})();
// deletes the key - the delete is recorded
await kv.delete("hello.world");
// purge is like delete, but all history values
// are dropped and only the purge remains.
await kv.purge("hello.world");
// stop the watch operation above
watch.stop();
// danger: destroys all values in the KV!
await kv.destroy();
await cleanup(ns, nc);
});
function setupCrossAccount(): Promise<NatsServer> {
const conf = {
accounts: {
A: {
jetstream: true,
users: [{ user: "a", password: "a" }],
exports: [
{ service: "$JS.API.>" },
{ service: "$KV.>" },
{ stream: "forb.>" },
],
},
B: {
users: [{ user: "b", password: "b" }],
imports: [
{ service: { subject: "$KV.>", account: "A" }, to: "froma.$KV.>" },
{ service: { subject: "$JS.API.>", account: "A" }, to: "froma.>" },
{ stream: { subject: "forb.>", account: "A" } },
],
},
},
};
return NatsServer.start(jetstreamServerConf(conf));
}
async function makeKvAndClient(
opts: ConnectionOptions,
jsopts: Partial<JetStreamOptions> = {},
): Promise<{ nc: NatsConnection; kv: KV }> {
const nc = await connect(opts);
const js = jetstream(nc, jsopts);
const kv = await new Kvm(js).create("a");
return { nc, kv };
}
Deno.test("kv - cross account history", async () => {
const ns = await setupCrossAccount();
async function checkHistory(kv: KV, trace?: string): Promise<void> {
const ap = deferred();
const bp = deferred();
const cp = deferred();
const ita = await kv.history();
const done = (async () => {
for await (const e of ita) {
if (trace) {
console.log(`${trace}: ${e.key}`, e);
}
switch (e.key) {
case "A":
ap.resolve();
break;
case "B":
bp.resolve();
break;
case "C":
cp.resolve();
break;
default:
// nothing
}
}
})();
await Promise.all([ap, bp, cp]);
ita.stop();
await done;
}
const { nc: nca, kv: kva } = await makeKvAndClient({
port: ns.port,
user: "a",
pass: "a",
});
await kva.put("A", "A");
await kva.put("B", "B");
await kva.delete("B");
const { nc: ncb, kv: kvb } = await makeKvAndClient({
port: ns.port,
user: "b",
pass: "b",
inboxPrefix: "forb",
}, { apiPrefix: "froma" });
await kvb.put("C", "C");
await Promise.all([checkHistory(kva), checkHistory(kvb)]);
await cleanup(ns, nca, ncb);
});
Deno.test("kv - cross account watch", async () => {
const ns = await setupCrossAccount();
async function checkWatch(kv: KV, trace?: string): Promise<void> {
const ap = deferred();
const bp = deferred();
const cp = deferred();
const ita = await kv.watch();
const done = (async () => {
for await (const e of ita) {
if (trace) {
console.log(`${trace}: ${e.key}`, e);
}
switch (e.key) {
case "A":
ap.resolve();
break;
case "B":
bp.resolve();
break;
case "C":
cp.resolve();
break;
default:
// nothing
}
}
})();
await Promise.all([ap, bp, cp]);
ita.stop();
await done;
}
const { nc: nca, kv: kva } = await makeKvAndClient({
port: ns.port,
user: "a",
pass: "a",
});
const { nc: ncb, kv: kvb } = await makeKvAndClient({
port: ns.port,
user: "b",
pass: "b",
inboxPrefix: "forb",
}, { apiPrefix: "froma" });
const proms = [checkWatch(kva), checkWatch(kvb)];
await Promise.all([nca.flush(), ncb.flush()]);
await kva.put("A", "A");
await kva.put("B", "B");
await kvb.put("C", "C");
await Promise.all(proms);
await cleanup(ns, nca, ncb);
});
Deno.test("kv - watch iter stops", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create("a") as Bucket;
const watch = await b.watch();
const done = (async () => {
for await (const _e of watch) {
// do nothing
}
})();
watch.stop();
await done;
await cleanup(ns, nc);
});
Deno.test("kv - defaults to discard new - if server 2.7.2", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create("a") as Bucket;
const jsm = await jetstreamManager(nc);
const si = await jsm.streams.info(b.stream);
const v272 = parseSemVer("2.7.2");
const serv = (nc as NatsConnectionImpl).getServerVersion();
assert(serv !== undefined, "should have a server version");
const v = compare(serv, v272);
const discard = v >= 0 ? DiscardPolicy.New : DiscardPolicy.Old;
assertEquals(si.config.discard, discard);
await cleanup(ns, nc);
});
Deno.test("kv - initialized watch empty", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create("a") as Bucket;
const iter = await b.watch();
const done = (async () => {
for await (const _e of iter) {
// nothing
}
})();
await delay(250);
assertEquals(0, iter.getReceived());
iter.stop();
await done;
await cleanup(ns, nc);
});
Deno.test("kv - initialized watch with modifications", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create("a") as Bucket;
await b.put("A", Empty);
await b.put("B", Empty);
await b.put("C", Empty);
setTimeout(async () => {
for (let i = 0; i < 100; i++) {
await b.put(i.toString(), Empty);
}
});
const iter = await b.watch();
let history = 0;
// we are expecting 103
const lock = Lock(103);
(async () => {
for await (const e of iter) {
if (!e.isUpdate) {
history++;
}
lock.unlock();
}
})().then();
// we don't really know when this happened
assert(103 > history);
await lock;
//@ts-ignore: testing
const oc = iter._data as PushConsumer;
const ci = await oc.info();
assertEquals(ci.num_pending, 0);
assertEquals(ci.delivered.consumer_seq, 103);
await cleanup(ns, nc);
});
Deno.test("kv - get revision", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next(), { history: 3 }) as Bucket;
async function check(key: string, value: string | null, revision = 0) {
const e = await b.get(key, { revision });
if (value === null) {
assertEquals(e, null);
} else {
assertEquals(e!.string(), value);
}
}
await b.put("A", "a");
await b.put("A", "b");
await b.put("A", "c");
// expect null, as sequence 1, holds "A"
await check("B", null, 1);
await check("A", "c");
await check("A", "a", 1);
await check("A", "b", 2);
await b.put("A", "d");
await check("A", "d");
await check("A", null, 1);
await cleanup(ns, nc);
});
Deno.test("kv - purge deletes", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create("a") as Bucket;
// keep the marker if delete is younger
await b.put("a", Empty);
await b.put("b", Empty);
await b.put("c", Empty);
await b.delete("a");
await b.delete("c");
await delay(1000);
await b.delete("b");
const pr = await b.purgeDeletes(700);
assertEquals(pr.purged, 2);
assertEquals(await b.get("a"), null);
assertEquals(await b.get("c"), null);
const e = await b.get("b");
assertEquals(e?.operation, "DEL");
await cleanup(ns, nc);
});
Deno.test("kv - allow direct", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const js = jetstream(nc);
const jsm = await jetstreamManager(nc);
async function test(
name: string,
opts: Partial<KvOptions>,
direct: boolean,
): Promise<void> {
const kv = await new Kvm(js).create(name, opts) as Bucket;
assertEquals(kv.direct, direct);
const si = await jsm.streams.info(kv.bucketName());
assertEquals(si.config.allow_direct, direct);
}
// default is not specified but allowed by the server
await test(nuid.next(), { history: 1 }, true);
// user opted to no direct
await test(nuid.next(), { history: 1, allow_direct: false }, false);
// user opted for direct
await test(nuid.next(), { history: 1, allow_direct: true }, true);
// now we create a kv that enables it
const xkv = await new Kvm(js).create("X") as Bucket;
assertEquals(xkv.direct, true);
// but the client opts-out of the direct
const xc = await new Kvm(js).create("X", { allow_direct: false }) as Bucket;
assertEquals(xc.direct, false);
// now the creator disables it, but the client wants it
const ykv = await new Kvm(js).create("Y", { allow_direct: false }) as Bucket;
assertEquals(ykv.direct, false);
const yc = await new Kvm(js).create("Y", { allow_direct: true }) as Bucket;
assertEquals(yc.direct, false);
// now let's pretend we got a server that doesn't support it
const nci = nc as NatsConnectionImpl;
nci.features.update("2.8.0");
nci.info!.version = "2.8.0";
await assertRejects(
async () => {
await test(nuid.next(), { history: 1, allow_direct: true }, false);
},
Error,
"allow_direct is not available on server version",
);
await cleanup(ns, nc);
});
Deno.test("kv - direct message", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const js = jetstream(nc);
const kv = await new Kvm(js).create("a", { allow_direct: true, history: 3 });
assertEquals(await kv.get("a"), null);
await kv.put("a", "hello");
const m = await kv.get("a");
assert(m !== null);
assertEquals(m.key, "a");
assertEquals(m.delta, 0);
assertEquals(m.revision, 1);
assertEquals(m.operation, "PUT");
assertEquals(m.bucket, "a");
assertEquals(m.string(), "hello");
assertEquals(m.value, new TextEncoder().encode("hello"));
assertEquals(m.length, 5);
assertEquals(m.rawKey, "a");
assert(m.created instanceof Date);
await kv.delete("a");
const d = await kv.get("a");
assert(d !== null);
assertEquals(d.key, "a");
assertEquals(d.delta, 0);
assertEquals(d.revision, 2);
assertEquals(d.operation, "DEL");
assertEquals(d.bucket, "a");
await kv.put("c", "hi");
await kv.put("c", "hello");
// should not fail
await kv.get("c");
const o = await kv.get("c", { revision: 3 });
assert(o !== null);
assertEquals(o.revision, 3);
await cleanup(ns, nc);
});
Deno.test("kv - republish", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.9.0")) {
return;
}
const js = jetstream(nc);
const kv = await new Kvm(js).create("test", {
republish: {
src: ">",
dest: "republished-kv.>",
},
}) as Bucket;
const sub = nc.subscribe("republished-kv.>", { max: 1 });
(async () => {
for await (const m of sub) {
assertEquals(m.subject, `republished-kv.${kv.subjectForKey("hello")}`);
assertEquals(m.string(), "world");
assertEquals(m.headers?.get(DirectMsgHeaders.Stream), kv.bucketName());
}
})().then();
await kv.put("hello", "world");
await sub.closed;
await cleanup(ns, nc);
});
Deno.test("kv - ttl is in nanos", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create("a", { ttl: 1000 });
const status = await b.status();
assertEquals(status.ttl, 1000);
assertEquals(status.size, 0);
const jsm = await jetstreamManager(nc);
const si = await jsm.streams.info("KV_a");
assertEquals(si.config.max_age, nanos(1000));
await cleanup(ns, nc);
});
Deno.test("kv - size", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const b = await new Kvm(js).create("a", { ttl: 1000 });
let status = await b.status();
assertEquals(status.size, 0);
assertEquals(status.size, status.streamInfo.state.bytes);
await b.put("a", "hello");
status = await b.status();
assert(status.size > 0);
assertEquals(status.size, status.streamInfo.state.bytes);
await cleanup(ns, nc);
});
Deno.test(
"kv - mirror cross domain",
flakyTest(async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
server_name: "HUB",
jetstream: { domain: "HUB" },
}),
);
// the ports file doesn't report leaf node
const varz = await ns.varz() as unknown;
const { ns: lns, nc: lnc } = await setup(
jetstreamServerConf({
server_name: "LEAF",
jetstream: { domain: "LEAF" },
leafnodes: {
remotes: [
//@ts-ignore: direct query
{ url: `leaf://127.0.0.1:${varz.leaf.port}` },
],
},
}),
);
// setup a KV
const js = jetstream(nc);
const kv = await new Kvm(js).create("TEST");
const m = new Map<string, KvEntry[]>();
// watch notifications on a on "name"
async function watch(kv: KV, bucket: string, key: string) {
const iter = await kv.watch({ key });
const buf: KvEntry[] = [];
m.set(bucket, buf);
return (async () => {
for await (const e of iter) {
buf.push(e);
}
})().then();
}
watch(kv, "test", "name").then();
await kv.put("name", "derek");
await kv.put("age", "22");
await kv.put("v", "v");
await kv.delete("v");
await nc.flush();
let a = m.get("test");
assert(a);
assertEquals(a.length, 1);
assertEquals(a[0].string(), "derek");
const ljs = jetstream(lnc);
await new Kvm(ljs).create("MIRROR", {
mirror: { name: "TEST", domain: "HUB" },
});
// setup a Mirror
const ljsm = await jetstreamManager(lnc);
let si = await ljsm.streams.info("KV_MIRROR");
assertEquals(si.config.mirror_direct, true);
for (let i = 0; i < 2000; i += 500) {
si = await ljsm.streams.info("KV_MIRROR");
if (si.state.messages === 3) {
break;
}
await delay(500);
}
assertEquals(si.state.messages, 3);
async function checkEntry(kv: KV, key: string, value: string, op: string) {
const e = await kv.get(key);
assert(e);
assertEquals(e.operation, op);
if (value !== "") {
assertEquals(e.string(), value);
}
}
async function t(kv: KV, name: string, old?: string) {
const histIter = await kv.history();
const hist: string[] = [];
for await (const e of histIter) {
hist.push(e.key);
}
if (old) {
await checkEntry(kv, "name", old, "PUT");
assertEquals(hist.length, 3);
assertArrayIncludes(hist, ["name", "age", "v"]);
} else {
assertEquals(hist.length, 0);
}
await kv.put("name", name);
await checkEntry(kv, "name", name, "PUT");
await kv.put("v", "v");
await checkEntry(kv, "v", "v", "PUT");
await kv.delete("v");
await checkEntry(kv, "v", "", "DEL");
const keysIter = await kv.keys();
const keys: string[] = [];
for await (const k of keysIter) {
keys.push(k);
}
assertEquals(keys.length, 2);
assertArrayIncludes(keys, ["name", "age"]);
}
const mkv = await new Kvm(ljs).create("MIRROR");
watch(mkv, "mirror", "name").then();
await t(mkv, "rip", "derek");
a = m.get("mirror");
assert(a);
assertEquals(a.length, 2);
assertEquals(a[1].string(), "rip");
// access the origin kv via the leafnode
const rjs = jetstream(lnc, { domain: "HUB" });
const rkv = await new Kvm(rjs).create("TEST") as Bucket;
assertEquals(rkv.prefix, "$KV.TEST");
watch(rkv, "origin", "name").then();
await t(rkv, "ivan", "rip");
await delay(1000);
a = m.get("origin");
assert(a);
assertEquals(a.length, 2);
assertEquals(a[1].string(), "ivan");
// shutdown the server
await cleanup(ns, nc);
await checkEntry(mkv, "name", "ivan", "PUT");
await cleanup(lns, lnc);
}),
);
Deno.test("kv - previous sequence", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const kv = await new Kvm(js).create("K");
assertEquals(await kv.put("A", Empty, { previousSeq: 0 }), 1);
assertEquals(await kv.put("B", Empty, { previousSeq: 0 }), 2);
assertEquals(await kv.put("A", Empty, { previousSeq: 1 }), 3);
assertEquals(await kv.put("A", Empty, { previousSeq: 3 }), 4);
await assertRejects(async () => {
await kv.put("A", Empty, { previousSeq: 1 });
});
assertEquals(await kv.put("B", Empty, { previousSeq: 2 }), 5);
await cleanup(ns, nc);
});
Deno.test("kv - encoded entry", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const kv = await new Kvm(js).create("K");
await kv.put("a", "hello");
await kv.put("b", JSON.stringify(5));
await kv.put("c", JSON.stringify(["hello", 5]));
assertEquals((await kv.get("a"))?.string(), "hello");
assertEquals((await kv.get("b"))?.json(), 5);
assertEquals((await kv.get("c"))?.json(), ["hello", 5]);
await cleanup(ns, nc);
});
Deno.test("kv - create after delete", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("K");
await kv.create("a", Empty);
await assertRejects(() => {
return kv.create("a", Empty);
});
await kv.delete("a");
await kv.create("a", Empty);
await kv.purge("a");
await kv.create("a", Empty);
await cleanup(ns, nc);
});
Deno.test("kv - get non-existing non-direct", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("K", { allow_direct: false });
const v = await kv.get("hello");
assertEquals(v, null);
await cleanup(ns, nc);
});
Deno.test("kv - get non-existing direct", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("K", { allow_direct: true });
assertEquals(await kv.get("hello"), null);
await cleanup(ns, nc);
});
Deno.test("kv - string payloads", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("K");
await kv.create("a", "b");
let entry = await kv.get("a");
assertExists(entry);
assertEquals(entry?.string(), "b");
await kv.put("a", "c");
entry = await kv.get("a");
assertExists(entry);
assertEquals(entry?.string(), "c");
await kv.update("a", "d", entry!.revision);
entry = await kv.get("a");
assertExists(entry);
assertEquals(entry?.string(), "d");
await cleanup(ns, nc);
});
Deno.test("kv - metadata", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const js = jetstream(nc);
const kv = await new Kvm(js).create("K", { metadata: { hello: "world" } });
const status = await kv.status();
assertEquals(status.metadata?.hello, "world");
await cleanup(ns, nc);
});
Deno.test("kv - watch updates only", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("K");
await kv.put("a", "a");
await kv.put("b", "b");
const iter = await kv.watch({
include: KvWatchInclude.UpdatesOnly,
});
const notifications: KvWatchEntry[] = [];
const done = (async () => {
for await (const e of iter) {
notifications.push(e);
if (e.isUpdate) {
break;
}
}
})();
await kv.put("c", "c");
await done;
assertEquals(notifications.length, 1);
assertEquals(notifications[0].isUpdate, true);
assertEquals(notifications[0].key, "c");
await cleanup(ns, nc);
});
Deno.test("kv - watch multiple keys", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("K");
await kv.put("a", "a");
await kv.put("b", "b");
await kv.put("c", "c");
const iter = await kv.watch({
key: ["a", "c"],
});
const notifications: string[] = [];
await (async () => {
for await (const e of iter) {
notifications.push(e.key);
if (e.delta === 0) {
break;
}
}
})();
assertEquals(notifications.length, 2);
assertArrayIncludes(notifications, ["a", "c"]);
await cleanup(ns, nc);
});
Deno.test("kv - watch history", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("K", { history: 10 });
await kv.put("a", "a");
await kv.put("a", "aa");
await kv.put("a", "aaa");
await kv.delete("a");
const iter = await kv.watch({
include: KvWatchInclude.AllHistory,
});
const notifications: string[] = [];
(async () => {
for await (const e of iter) {
console.log(e.operation);
if (e.operation === "DEL") {
notifications.push(`${e.key}=del`);
} else {
notifications.push(`${e.key}=${e.string()}`);
}
}
})().then();
await kv.put("c", "c");
await delay(1000);
assertEquals(notifications.length, 5);
assertEquals(notifications[0], "a=a");
assertEquals(notifications[1], "a=aa");
assertEquals(notifications[2], "a=aaa");
assertEquals(notifications[3], "a=del");
assertEquals(notifications[4], "c=c");
await cleanup(ns, nc);
});
Deno.test("kv - watch history no deletes", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("K", { history: 10 });
await kv.put("a", "a");
await kv.put("a", "aa");
await kv.put("a", "aaa");
await kv.delete("a");
const iter = await kv.watch({
include: KvWatchInclude.AllHistory,
ignoreDeletes: true,
});
const notifications: string[] = [];
(async () => {
for await (const e of iter) {
if (e.operation === "DEL") {
notifications.push(`${e.key}=del`);
} else {
notifications.push(`${e.key}=${e.string()}`);
}
}
})().then();
await kv.put("c", "c");
await kv.delete("c");
await delay(1000);
assertEquals(notifications.length, 4);
assertEquals(notifications[0], "a=a");
assertEquals(notifications[1], "a=aa");
assertEquals(notifications[2], "a=aaa");
assertEquals(notifications[3], "c=c");
await cleanup(ns, nc);
});
Deno.test("kv - republish header handling", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const jsm = await jetstreamManager(nc);
const n = nuid.next();
await jsm.streams.add({
name: n,
subjects: ["A.>"],
storage: StorageType.Memory,
republish: {
src: ">",
dest: `$KV.${n}.>`,
},
});
const js = jetstream(nc);
const kv = await new Kvm(js).create(n);
nc.publish("A.orange", "hey");
await js.publish("A.tomato", "hello");
await kv.put("A.potato", "yo");
async function check(allow_direct = false): Promise<void> {
const B = await new Kvm(js).create(n, { allow_direct });
let e = await B.get("A.orange");
assertExists(e);
e = await B.get("A.tomato");
assertExists(e);
e = await B.get("A.potato");
assertExists(e);
}
await check();
await check(true);
await cleanup(ns, nc);
});
Deno.test("kv - compression", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const s2 = await new Kvm(js).create("compressed", {
compression: true,
});
let status = await s2.status();
assertEquals(status.compression, true);
const none = await new Kvm(js).create("none");
status = await none.status();
assertEquals(status.compression, false);
await cleanup(ns, nc);
});
Deno.test("kv - watch start at", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const js = jetstream(nc);
const kv = await new Kvm(js).create("a");
await kv.put("a", "1");
await kv.put("b", "2");
await kv.put("c", "3");
const iter = await kv.watch({ resumeFromRevision: 2 });
await (async () => {
for await (const o of iter) {
// expect first key to be "b"
assertEquals(o.key, "b");
assertEquals(o.revision, 2);
break;
}
})();
await cleanup(ns, nc);
});
Deno.test("kv - delete key if revision", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next());
const seq = await b.create("a", Empty);
await assertRejects(
async () => {
await b.delete("a", { previousSeq: 100 });
},
Error,
"wrong last sequence: 1",
undefined,
);
await b.delete("a", { previousSeq: seq });
await cleanup(ns, nc);
});
Deno.test("kv - purge key if revision", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const b = await new Kvm(js).create(nuid.next());
const seq = await b.create("a", Empty);
await assertRejects(
async () => {
await b.purge("a", { previousSeq: 2 });
},
Error,
"wrong last sequence: 1",
undefined,
);
await b.purge("a", { previousSeq: seq });
await cleanup(ns, nc);
});
Deno.test("kv - bind no info", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
await new Kvm(js).create("A");
const d = deferred();
nc.subscribe("$JS.API.STREAM.INFO.>", {
callback: () => {
d.reject(new Error("saw stream info"));
},
});
const kv = await new Kvm(js).create("A", {
bindOnly: true,
allow_direct: true,
});
await kv.put("a", "hello");
const e = await kv.get("a");
assertEquals(e?.string(), "hello");
await kv.delete("a");
d.resolve();
// shouldn't have rejected earlier
await d;
await cleanup(ns, nc);
});
Deno.test("kv - watcher will name and filter", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const kv = await new Kvm(js).create("A");
const sub = syncIterator(nc.subscribe("$JS.API.>"));
const iter = await kv.watch({ key: "a.>" });
const m = await sub.next();
assert(m?.subject.startsWith("$JS.API.CONSUMER.CREATE.KV_A."));
assert(m?.subject.endsWith("$KV.A.a.>"));
iter.stop();
await cleanup(ns, nc);
});
Deno.test("kv - honors checkAPI option", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const sub = nc.subscribe("$JS.API.INFO");
const si = syncIterator(sub);
await new Kvm(js).create("A");
assertExists(await si.next());
const js2 = jetstream(nc, { checkAPI: false });
await new Kvm(js2).create("B");
await sub.drain();
assertEquals(await si.next(), null);
await cleanup(ns, nc);
});
Deno.test("kv - watcher on server restart", async () => {
let { ns, nc } = await setup(jetstreamServerConf({}));
const js = jetstream(nc);
const kv = await new Kvm(js).create("A");
const iter = await kv.watch();
const d = deferred<KvEntry>();
(async () => {
for await (const e of iter) {
d.resolve(e);
break;
}
})().then();
ns = await ns.restart();
for (let i = 0; i < 10; i++) {
try {
await kv.put("hello", "world");
break;
} catch {
await delay(500);
}
}
await d;
await cleanup(ns, nc);
});
Deno.test("kv - kv rejects in older servers", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
const nci = nc as NatsConnectionImpl;
const js = jetstream(nc);
async function t(version: string, ok: boolean): Promise<void> {
nci.features.update(version);
if (!ok) {
await assertRejects(
async () => {
await new Kvm(js).create(nuid.next());
},
Error,
`kv is only supported on servers 2.6.2 or better`,
);
} else {
await new Kvm(js).create(nuid.next());
}
}
await t("2.6.1", false);
await t("2.6.2", true);
await cleanup(ns, nc);
});
Deno.test("kv - maxBucketSize doesn't override max_bytes", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({}),
);
const kvm = new Kvm(nc);
const kv = await kvm.create("A", { max_bytes: 100 });
const info = await kv.status();
assertEquals(info.max_bytes, 100);
await cleanup(ns, nc);
});
Deno.test("kv - keys filter", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const kvm = new Kvm(nc);
const b = await kvm.create(nuid.next());
await Promise.all([b.put("A", "a"), b.put("B", "b"), b.put("C", "c")]);
const buf = [];
for await (const e of await b.keys()) {
buf.push(e);
}
assertEquals(buf.length, 3);
assertArrayIncludes(buf, ["A", "B", "C"]);
buf.length = 0;
for await (const e of await b.keys("A")) {
buf.push(e);
}
assertEquals(buf.length, 1);
assertArrayIncludes(buf, ["A"]);
buf.length = 0;
for await (const e of await b.keys(["A", "C"])) {
buf.push(e);
}
assertEquals(buf.length, 2);
assertArrayIncludes(buf, ["A", "C"]);
await cleanup(ns, nc);
});
Deno.test("kv - replicas", async () => {
const servers = await NatsServer.jetstreamCluster(3);
const nc = await connect({ port: servers[0].port });
const js = jetstream(nc);
const b = await new Kvm(js).create("a", { replicas: 3 });
const status = await b.status();
const jsm = await jetstreamManager(nc);
let si = await jsm.streams.info(status.streamInfo.config.name);
assertEquals(si.config.num_replicas, 3);
si = await jsm.streams.update(status.streamInfo.config.name, {
num_replicas: 1,
});
assertEquals(si.config.num_replicas, 1);
await nc.close();
await NatsServer.stopAll(servers, true);
});
Deno.test("kv - sourced", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const kvm = await new Kvm(js);
const source = await kvm.create("source");
const target = await kvm.create("target", {
sources: [{ name: "source" }],
});
await source.put("hello", "world");
for (let i = 0; i < 10; i++) {
const v = await target.get("hello");
if (v === null) {
await delay(250);
continue;
}
assertEquals(v.string(), "world");
}
await cleanup(ns, nc);
});
Deno.test("kv - watch isUpdate", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const js = jetstream(nc);
const kvm = await new Kvm(js);
const kv = await kvm.create("A");
await kv.put("a", "hello");
await kv.delete("a");
const iter = await kv.watch({ ignoreDeletes: true });
const done = (async () => {
for await (const e of iter) {
if (e.key === "b") {
assertEquals(e.isUpdate, true);
break;
}
}
})();
await kv.put("b", "hello");
await done;
await cleanup(ns, nc);
});
Deno.test("kv - entries ttl markerTTL", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({}),
);
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const kvm = await new Kvm(nc);
const kv = await kvm.create("A", { markerTTL: 2000 });
const si = await kv.status();
assertEquals(si.markerTTL, 2000);
const nttl = await kvm.create("B");
const si2 = await nttl.status();
assertEquals(si2.markerTTL, 0);
await cleanup(ns, nc);
});
Deno.test("kv - get entry ttl markerTTL from bucket", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({}),
);
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const kvm = await new Kvm(nc);
const kv = await kvm.create("A", { markerTTL: 1000 });
await kv.create("a", Empty, "2s");
await kv.delete("a");
let e = await kv.get("a");
assertEquals(e?.operation, "DEL");
await kv.purge("a", { ttl: "2s" });
e = await kv.get("a");
assertEquals(e?.operation, "PURGE");
await delay(2500);
e = await kv.get("a");
assertEquals(e, null);
await cleanup(ns, nc);
});
Deno.test("kv - entries ttl", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({}),
);
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const kvm = await new Kvm(nc);
const kv = await kvm.create("A", { markerTTL: 2000, history: 3 });
const si = await kv.status();
assertEquals(si.markerTTL, 2000);
const iter = await kv.watch();
(async () => {
for await (const e of iter) {
console.log(e.revision, e.operation);
//@ts-expect-error: test
const jsMsg = e.sm;
console.log(jsMsg.headers);
}
})().then();
await kv.create("a", "hello");
await kv.delete("a");
await kv.purge("a", { ttl: "2s" });
// await done;
const start = Date.now();
for (let i = 0;; i++) {
const b = await kv.get("a");
if (b === null) {
break;
}
console.log("still there", i);
await delay(1000);
}
const end = Date.now() - start;
assert(end > 2000);
await cleanup(ns, nc);
});
Deno.test("kv - watcher ttl", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({}),
);
if (await notCompatible(ns, nc, "2.11.0")) {
return;
}
const kvm = await new Kvm(nc);
const kv = await kvm.create("A", { markerTTL: 2000, history: 3, ttl: 2000 });
const si = await kv.status();
assertEquals(si.markerTTL, 2000);
const iter = await kv.watch();
const done = (async () => {
for await (const e of iter) {
console.log(e.revision, e.operation);
//@ts-expect-error: test
const jsMsg = e.sm;
console.log(jsMsg.headers);
if (e.operation === "PURGE") {
break;
}
}
})().then();
await kv.create("a", "hello", "2s");
await deadline(done, 3000);
await cleanup(ns, nc);
});
Deno.test("kv - bind doesn't check jetstream", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const apiInfo = nc.subscribe("$JS.API.INFO", {
callback: (_, m) => {
console.log(m.subject);
},
});
const streamInfo = nc.subscribe("$JS.API.STREAM.INFO.*", {
callback: (_, m) => {
console.log(m.subject);
},
});
let kvm = new Kvm(jetstream(nc, { checkAPI: false }));
await kvm.create("A", { bindOnly: true });
assertEquals(apiInfo.getReceived(), 0);
assertEquals(streamInfo.getReceived(), 0);
await kvm.open("A", { bindOnly: true });
assertEquals(apiInfo.getReceived(), 0);
assertEquals(streamInfo.getReceived(), 0);
kvm = new Kvm(jetstream(nc));
await kvm.create("B", { bindOnly: true });
assertEquals(apiInfo.getReceived(), 0);
assertEquals(streamInfo.getReceived(), 0);
kvm = new Kvm(jetstream(nc));
await kvm.create("B");
assertEquals(apiInfo.getReceived(), 1);
assertEquals(streamInfo.getReceived(), 1);
await cleanup(ns, nc);
});
Deno.test("kv - put/update timeouts", async () => {
const { ns, nc } = await setup();
// allow a timeout
nc.subscribe("$KV.A.>", { callback: () => {} });
const kvm = new Kvm(jetstream(nc, { checkAPI: false }));
const kv = await kvm.open("A", { bindOnly: true });
let start = Date.now();
try {
await kv.put("A", "hello", { timeout: 1_500 });
} catch (err) {
assertIsError(err, Error, "timeout");
assertBetween(Date.now() - start, 1_300, 1_700);
}
start = Date.now();
try {
await kv.update("A", "hello", 1, 1_500);
} catch (err) {
assertIsError(err, Error, "timeout");
assertBetween(Date.now() - start, 1_300, 1_700);
}
// check default timeout 5s
start = Date.now();
try {
await kv.put("A", "a");
} catch (err) {
assertIsError(err, Error, "timeout");
assertBetween(Date.now() - start, 4_900, 5_100);
}
await cleanup(ns, nc);
});
Deno.test("kv - encoder", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const subjectEncoder = (key: string): string => {
const chunks: string[] = [];
for (const t of key.split(".")) {
switch (t) {
case ">":
case "*":
chunks.push(t);
break;
default:
chunks.push(t.split("").reverse().join(""));
break;
}
}
return chunks.join(".");
};
const reverseSubjectCodec = {
encode: subjectEncoder,
decode: subjectEncoder,
};
const payloadEncoder = (v: Payload): Uint8Array => {
if (typeof v === "string") {
return new TextEncoder().encode(v.split("").reverse().join(""));
}
let vv = new TextDecoder().decode(v);
vv = vv.split("").reverse().join("");
return new TextEncoder().encode(vv);
};
const reverseCodec = {
encode: payloadEncoder,
decode: payloadEncoder,
};
const kvm = new Kvm(jetstream(nc));
const kv = await kvm.create("encoded", {
codec: {
key: reverseSubjectCodec,
value: reverseCodec,
},
});
await kv.put("abc.hello.one", "hello");
const e = await kv.get("abc.hello.one");
assertExists(e);
assertEquals(e.string(), "hello");
const raw = await kvm.open("encoded", { bindOnly: true });
const e2 = await raw.get("cba.olleh.eno");
assertExists(e2);
assertEquals(e2.string(), "olleh");
const w = await kv.watch({ key: "abc.>" });
await (async () => {
for await (const e of w) {
assertEquals(e.key, "abc.hello.one");
assertEquals(e.string(), "hello");
assertEquals(e.rawKey, "cba.olleh.eno");
break;
}
})().then();
await cleanup(ns, nc);
});
--- kv/README.md ---
[![License](https://img.shields.io/badge/Licence-Apache%202.0-blue.svg)](./LICENSE)
![kv](https://github.com/nats-io/nats.js/actions/workflows/test.yml/badge.svg)
[![JSDoc](https://img.shields.io/badge/JSDoc-reference-blue)](https://nats-io.github.io/nats.js/kv/index.html)
[![JSR](https://jsr.io/badges/@nats-io/kv)](https://jsr.io/@nats-io/kv)
[![JSR](https://jsr.io/badges/@nats-io/kv/score)](https://jsr.io/@nats-io/kv)
[![NPM Version](https://img.shields.io/npm/v/%40nats-io%2Fkv)](https://www.npmjs.com/package/@nats-io/kv)
![NPM Downloads](https://img.shields.io/npm/dt/%40nats-io%2Fkv)
![NPM Downloads](https://img.shields.io/npm/dm/%40nats-io%2Fkv)
# kv
The kv module implements the NATS KV functionality using JetStream for
JavaScript clients. JetStream clients can use streams to store and access data.
KV is materialized view that presents a different _API_ to interact with the
data stored in a stream using the API for a Key-Value store which should be
familiar to many application developers.
## Installation
For a quick overview of the libraries and how to install them, see
[runtimes.md](../runtimes.md).
Note that this library is distributed in two different registries:
- npm a node-specific library supporting CJS (`require`) and ESM (`import`)
- jsr a node and other ESM (`import`) compatible runtimes (deno, browser, node)
If your application doesn't use `require`, you can simply depend on the JSR
version.
### NPM
The NPM registry hosts a node-only compatible version of the library
[@nats-io/kv](https://www.npmjs.com/package/@nats-io/kv) supporting both CJS and
ESM:
```bash
npm install @nats-io/kv
```
### JSR
The JSR registry hosts the ESM-only [@nats-io/kv](https://jsr.io/@nats-io/kv)
version of the library.
```bash
deno jsr:add @nats-io/kv
```
```bash
npx jsr add @nats-io/kv
```
```bash
yarn dlx jsr add @nats-io/kv
```
```bash
bunx jsr add @nats-io/kv
```
## Referencing the library
Once you import the library, you can reference in your code as:
```javascript
import { Kvm } from "@nats-io/kv";
// or in node (only when using CJS)
const { Kvm } = require("@nats-io/kv");
// using a nats connection:
const kvm = new Kvm(nc);
await kvm.list();
await kvm.create("mykv");
```
If you want to customize some of the JetStream options when working with KV, you
can:
```typescript
import { jetStream } from "@nats-io/jetstream";
import { Kvm } from "@nats-io/kv";
const js = jetstream(nc, { timeout: 10_000 });
// KV will inherit all the options from the JetStream client
const kvm = new Kvm(js);
```
```typescript
// create the named KV or bind to it if it exists:
const kvm = new Kvm(nc);
const kv = await kvm.create("testing", { history: 5 });
// if the kv is expected to exist:
// const kv = await kvm.open("testing");
// create an entry - this is similar to a put, but will fail if the
// key exists
const hw = await kv.create("hello.world", "hi");
// Values in KV are stored as KvEntries:
// {
// bucket: string,
// key: string,
// value: Uint8Array,
// created: Date,
// revision: number,
// delta?: number,
// operation: "PUT"|"DEL"|"PURGE"
// }
// The operation property specifies whether the value was
// updated (PUT), deleted (DEL) or purged (PURGE).
// you can monitor values modification in a KV by watching.
// You can watch specific key subset or everything.
// Watches start with the latest value for each key in the
// set of keys being watched - in this case all keys
const watch = await kv.watch();
(async () => {
for await (const e of watch) {
// do something with the change
console.log(
`watch: ${e.key}: ${e.operation} ${e.value ? e.string() : ""}`,
);
}
})().then();
// update the entry
await kv.put("hello.world", "world");
// retrieve the KvEntry storing the value
// returns null if the value is not found
const e = await kv.get("hello.world");
// initial value of "hi" was overwritten above
console.log(`value for get ${e?.string()}`);
const buf: string[] = [];
const keys = await kv.keys();
await (async () => {
for await (const k of keys) {
buf.push(k);
}
})();
console.log(`keys contains hello.world: ${buf[0] === "hello.world"}`);
let h = await kv.history({ key: "hello.world" });
await (async () => {
for await (const e of h) {
// do something with the historical value
// you can test e.operation for "PUT", "DEL", or "PURGE"
// to know if the entry is a marker for a value set
// or for a deletion or purge.
console.log(
`history: ${e.key}: ${e.operation} ${e.value ? sc.decode(e.value) : ""}`,
);
}
})();
// deletes the key - the delete is recorded
await kv.delete("hello.world");
// purge is like delete, but all history values
// are dropped and only the purge remains.
await kv.purge("hello.world");
// stop the watch operation above
watch.stop();
// danger: destroys all values in the KV!
await kv.destroy();
```
--- obj/tests/b64_test.ts ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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 {
Base64Codec,
Base64UrlCodec,
Base64UrlPaddedCodec,
} from "../src/base64.ts";
import { assert, assertEquals, assertFalse } from "@std/assert";
Deno.test("b64 - Base64Codec", () => {
// should match btoa
assertEquals(Base64Codec.encode("hello"), btoa("hello"));
assertEquals(Base64Codec.decode(btoa("hello")), "hello");
// binary input
const bin = new TextEncoder().encode("hello");
assertEquals(Base64Codec.encode(bin), btoa("hello"));
assertEquals(Base64Codec.decode(Base64Codec.encode(bin), true), bin);
});
Deno.test("b64 - Base64UrlCodec", () => {
// URL encoding removes padding
const v = btoa(encodeURI("hello/world/one+two")).replaceAll("=", "");
assertEquals(Base64UrlCodec.encode("hello/world/one+two"), v);
assertEquals(Base64UrlCodec.decode(v), "hello/world/one+two");
assertFalse(v.endsWith("=="), "expected padded");
// binary input
const bin = new TextEncoder().encode("hello/world/one+two");
assertEquals(Base64UrlCodec.encode(bin), v);
assertEquals(Base64UrlCodec.decode(v, true), bin);
});
Deno.test("b64 - Base64UrlPaddedCodec", () => {
// URL encoding removes padding
const v = btoa(encodeURI("hello/world/one+two"));
assert(v.endsWith("=="), "expected padded");
assertEquals(Base64UrlPaddedCodec.encode("hello/world/one+two"), v);
assertEquals(Base64UrlPaddedCodec.decode(v), "hello/world/one+two");
// binary input
const bin = new TextEncoder().encode("hello/world/one+two");
assertEquals(Base64UrlPaddedCodec.encode(bin), v);
assertEquals(Base64UrlPaddedCodec.decode(v, true), bin);
});
--- obj/tests/objectstore_test.ts ---
/*
* 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 {
cleanup,
connect,
jetstreamServerConf,
NatsServer,
notCompatible,
setup,
} from "test_helpers";
import {
assert,
assertEquals,
assertExists,
assertRejects,
equal,
} from "@std/assert";
import {
DataBuffer,
Empty,
headers,
nanos,
nuid,
} from "@nats-io/nats-core/internal";
import type { NatsConnectionImpl } from "@nats-io/nats-core/internal";
import type { ObjectInfo, ObjectStoreMeta } from "../src/types.ts";
import { jetstreamManager, StorageType } from "@nats-io/jetstream";
import { equals } from "@std/bytes";
import { digestType, Objm } from "../src/objectstore.ts";
import { Base64UrlPaddedCodec } from "../src/base64.ts";
import { sha256 } from "js-sha256";
function readableStreamFrom(data: Uint8Array): ReadableStream<Uint8Array> {
return new ReadableStream<Uint8Array>({
pull(controller) {
controller.enqueue(data);
controller.close();
},
});
}
async function fromReadableStream(
rs: ReadableStream<Uint8Array>,
): Promise<Uint8Array> {
const buf = new DataBuffer();
const reader = rs.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
return buf.drain();
}
if (value && value.length) {
buf.fill(value);
}
}
}
function makeData(n: number): Uint8Array {
const data = new Uint8Array(n);
let index = 0;
let bytes = n;
while (true) {
if (bytes === 0) {
break;
}
const len = bytes > 65536 ? 65536 : bytes;
bytes -= len;
const buf = new Uint8Array(len);
crypto.getRandomValues(buf);
data.set(buf, index);
index += buf.length;
}
return data;
}
function digest(data: Uint8Array): string {
const sha = sha256.create();
sha.update(data);
const digest = Base64UrlPaddedCodec.encode(Uint8Array.from(sha.digest()));
return `${digestType}${digest}`;
}
Deno.test("objectstore - list stores", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const jsm = await jetstreamManager(nc);
await jsm.streams.add({ name: "A", subjects: ["a"] });
const objm = new Objm(nc);
let stores = await objm.list().next();
assertEquals(stores.length, 0);
await objm.create("store1");
stores = await objm.list().next();
assertEquals(stores.length, 1);
assertEquals(stores[0].bucket, "store1");
await objm.create("store2");
stores = await objm.list().next();
assertEquals(stores.length, 2);
const names = stores.map((s) => s.bucket).sort();
assertEquals(names, ["store1", "store2"]);
await cleanup(ns, nc);
});
Deno.test("objectstore - basics", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const blob = new Uint8Array(65536);
crypto.getRandomValues(blob);
const objm = new Objm(nc);
const os = await objm.create("OBJS", { description: "testing" });
const info = await os.status();
assertEquals(info.description, "testing");
assertEquals(info.ttl, 0);
assertEquals(info.replicas, 1);
assertEquals(info.streamInfo.config.name, "OBJ_OBJS");
const oi = await os.put(
{ name: "BLOB", description: "myblob" },
readableStreamFrom(blob),
);
assertEquals(oi.bucket, "OBJS");
assertEquals(oi.nuid.length, 22);
assertEquals(oi.name, "BLOB");
assertEquals(oi.digest, digest(blob));
assertEquals(oi.description, "myblob");
assertEquals(oi.deleted, false);
assert(typeof oi.mtime === "string");
const jsm = await jetstreamManager(nc);
const si = await jsm.streams.info("OBJ_OBJS");
assertExists(si);
const osi = await os.seal();
assertEquals(osi.sealed, true);
assert(osi.size > blob.length);
assertEquals(osi.storage, StorageType.File);
assertEquals(osi.description, "testing");
let or = await os.get("foo");
assertEquals(or, null);
or = await os.get("BLOB");
assertExists(or);
const read = await fromReadableStream(or!.data);
equal(read, blob);
assertEquals(await os.destroy(), true);
await assertRejects(
async () => {
await jsm.streams.info("OBJ_OBJS");
},
Error,
"stream not found",
);
await cleanup(ns, nc);
});
Deno.test("objectstore - default status", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test", { description: "testing" });
const blob = new Uint8Array(65536);
crypto.getRandomValues(blob);
await os.put({ name: "BLOB" }, readableStreamFrom(blob));
const status = await os.status();
assertEquals(status.backingStore, "JetStream");
assertEquals(status.bucket, "test");
assertEquals(status.streamInfo.config.name, "OBJ_test");
await cleanup(ns, nc);
});
Deno.test("objectstore - chunked content", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
jetstream: {
max_memory_store: 10 * 1024 * 1024 + 33,
},
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test", { storage: StorageType.Memory });
const data = makeData(nc.info!.max_payload * 3);
await os.put(
{ name: "blob", options: { max_chunk_size: nc.info!.max_payload } },
readableStreamFrom(data),
);
const d = await os.get("blob");
assertEquals(d!.info.digest, digest(data));
const vv = await fromReadableStream(d!.data);
equals(vv, data);
await cleanup(ns, nc);
});
Deno.test("objectstore - multi content", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test", { storage: StorageType.Memory });
const a = makeData(128);
await os.put(
{ name: "a.js", options: { max_chunk_size: 1 } },
readableStreamFrom(a),
);
const b = new TextEncoder().encode("hello world from object store");
await os.put(
{ name: "b.js", options: { max_chunk_size: nc.info!.max_payload } },
readableStreamFrom(b),
);
let d = await os.get("a.js");
let vv = await fromReadableStream(d!.data);
equals(vv, a);
d = await os.get("b.js");
vv = await fromReadableStream(d!.data);
equals(vv, b);
await cleanup(ns, nc);
});
Deno.test("objectstore - delete markers", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test", { storage: StorageType.Memory });
const a = makeData(128);
await os.put(
{ name: "a", options: { max_chunk_size: 10 } },
readableStreamFrom(a),
);
const p = await os.delete("a");
assertEquals(p.purged, 13);
const info = await os.info("a");
assertExists(info);
assertEquals(info!.deleted, true);
await cleanup(ns, nc);
});
Deno.test("objectstore - get on deleted returns error", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test", { storage: StorageType.Memory });
const a = makeData(128);
await os.put(
{ name: "a", options: { max_chunk_size: 10 } },
readableStreamFrom(a),
);
const p = await os.delete("a");
assertEquals(p.purged, 13);
const info = await os.info("a");
assertExists(info);
assertEquals(info!.deleted, true);
const r = await os.get("a");
assertEquals(r, null);
await cleanup(ns, nc);
});
Deno.test("objectstore - multi with delete", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test", { storage: StorageType.Memory });
await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("a!")),
);
const si = await os.status({ subjects_filter: ">" });
await os.put(
{ name: "b", options: { max_chunk_size: nc.info!.max_payload } },
readableStreamFrom(new TextEncoder().encode("b!")),
);
await os.get("b");
await os.delete("b");
const s2 = await os.status({ subjects_filter: ">" });
// should have the tumbstone for the deleted subject
assertEquals(s2.streamInfo.state.messages, si.streamInfo.state.messages + 1);
await cleanup(ns, nc);
});
Deno.test("objectstore - object names", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test", { storage: StorageType.Memory });
await os.put(
{ name: "blob.txt" },
readableStreamFrom(new TextEncoder().encode("A")),
);
await os.put(
{ name: "foo bar" },
readableStreamFrom(new TextEncoder().encode("A")),
);
await os.put(
{ name: " " },
readableStreamFrom(new TextEncoder().encode("A")),
);
await os.put(
{ name: "*" },
readableStreamFrom(new TextEncoder().encode("A")),
);
await os.put(
{ name: ">" },
readableStreamFrom(new TextEncoder().encode("A")),
);
await assertRejects(async () => {
await os.put(
{ name: "" },
readableStreamFrom(new TextEncoder().encode("A")),
);
});
await cleanup(ns, nc);
});
Deno.test("objectstore - metadata", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test", { storage: StorageType.Memory });
await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("A")),
);
// rename a
let meta = { name: "b" } as ObjectStoreMeta;
await os.update("a", meta);
let info = await os.info("b");
assertExists(info);
assertEquals(info!.name, "b");
// add some headers
meta = {} as ObjectStoreMeta;
meta.headers = headers();
meta.headers.set("color", "red");
await os.update("b", meta);
info = await os.info("b");
assertExists(info);
assertEquals(info!.headers?.get("color"), "red");
await cleanup(ns, nc);
});
Deno.test("objectstore - empty entry", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("empty");
const oi = await os.put(
{ name: "empty" },
readableStreamFrom(new Uint8Array(0)),
);
assertEquals(oi.nuid.length, 22);
assertEquals(oi.name, "empty");
assertEquals(oi.digest, digest(new Uint8Array(0)));
assertEquals(oi.chunks, 0);
const or = await os.get("empty");
assert(or !== null);
assertEquals(await or.error, null);
const v = await fromReadableStream(or.data);
assertEquals(v.length, 0);
await cleanup(ns, nc);
});
Deno.test("objectstore - list", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
let infos = await os.list();
assertEquals(infos.length, 0);
await os.put(
{ name: "a" },
readableStreamFrom(new Uint8Array(0)),
);
infos = await os.list();
assertEquals(infos.length, 1);
assertEquals(infos[0].name, "a");
await cleanup(ns, nc);
});
Deno.test("objectstore - list no updates", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
let infos = await os.list();
assertEquals(infos.length, 0);
await os.put({ name: "a" }, readableStreamFrom(new Uint8Array(0)));
infos = await os.list();
assertEquals(infos.length, 1);
await cleanup(ns, nc);
});
Deno.test("objectstore - watch isUpdate", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
await os.put({ name: "a" }, readableStreamFrom(new Uint8Array(0)));
const watches = await os.watch();
await os.put({ name: "b" }, readableStreamFrom(new Uint8Array(0)));
for await (const e of watches) {
if (e.name === "b") {
assertEquals(e.isUpdate, true);
break;
} else {
assertEquals(e.isUpdate, false);
}
}
await cleanup(ns, nc);
});
Deno.test("objectstore - watch initially empty", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
const buf: ObjectInfo[] = [];
const iter = await os.watch({ includeHistory: true });
const done = (async () => {
for await (const info of iter) {
if (info === null) {
assertEquals(buf.length, 0);
} else {
buf.push(info);
if (buf.length === 3) {
break;
}
}
}
})();
const infos = await os.list();
assertEquals(infos.length, 0);
await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("a")),
);
await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("aa")),
);
await os.put(
{ name: "b" },
readableStreamFrom(new TextEncoder().encode("b")),
);
await done;
assertEquals(buf.length, 3);
assertEquals(buf[0].name, "a");
assertEquals(buf[0].size, 1);
assertEquals(buf[1].name, "a");
assertEquals(buf[1].size, 2);
assertEquals(buf[2].name, "b");
assertEquals(buf[2].size, 1);
await cleanup(ns, nc);
});
Deno.test("objectstore - watch skip history", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("a")),
);
await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("aa")),
);
const buf: ObjectInfo[] = [];
const iter = await os.watch({ includeHistory: false });
const done = (async () => {
for await (const info of iter) {
if (info === null) {
assertEquals(buf.length, 1);
} else {
buf.push(info);
if (buf.length === 1) {
break;
}
}
}
})();
await os.put(
{ name: "c" },
readableStreamFrom(new TextEncoder().encode("c")),
);
await done;
assertEquals(buf.length, 1);
assertEquals(buf[0].name, "c");
assertEquals(buf[0].size, 1);
await cleanup(ns, nc);
});
Deno.test("objectstore - watch history", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("a")),
);
await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("aa")),
);
const buf: ObjectInfo[] = [];
const iter = await os.watch({ includeHistory: true });
const done = (async () => {
for await (const info of iter) {
if (info === null) {
assertEquals(buf.length, 1);
} else {
buf.push(info);
if (buf.length === 2) {
break;
}
}
}
})();
await os.put(
{ name: "c" },
readableStreamFrom(new TextEncoder().encode("c")),
);
await done;
assertEquals(buf.length, 2);
assertEquals(buf[0].name, "a");
assertEquals(buf[0].size, 2);
assertEquals(buf[1].name, "c");
assertEquals(buf[1].size, 1);
await cleanup(ns, nc);
});
Deno.test("objectstore - same store link", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
const src = await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("a")),
);
const oi = await os.link("ref", src);
assertEquals(oi.options?.link?.bucket, src.bucket);
assertEquals(oi.options?.link?.name, "a");
const a = await os.list();
assertEquals(a.length, 2);
assertEquals(a[0].name, "a");
assertEquals(a[1].name, "ref");
const data = await os.getBlob("ref");
assertEquals(new TextDecoder().decode(data!), "a");
await cleanup(ns, nc);
});
Deno.test("objectstore - link of link rejected", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
const src = await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("a")),
);
const link = await os.link("ref", src);
await assertRejects(
async () => {
await os.link("ref2", link);
},
Error,
"src object is a link",
);
await cleanup(ns, nc);
});
Deno.test("objectstore - external link", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
const src = await os.put(
{ name: "a" },
readableStreamFrom(new TextEncoder().encode("a")),
);
const os2 = await objm.create("another");
const io = await os2.link("ref", src);
assertExists(io.options?.link);
assertEquals(io.options?.link?.bucket, "test");
assertEquals(io.options?.link?.name, "a");
const data = await os2.getBlob("ref");
assertEquals(new TextDecoder().decode(data!), "a");
await cleanup(ns, nc);
});
Deno.test("objectstore - store link", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
const os2 = await objm.create("another");
const si = await os2.linkStore("src", os);
assertExists(si.options?.link);
assertEquals(si.options?.link?.bucket, "test");
await cleanup(ns, nc);
});
Deno.test("objectstore - max chunk is max payload", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 8 * 1024,
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
assertEquals(nc.info?.max_payload, 8 * 1024);
const objm = new Objm(nc);
const os = await objm.create("test");
const rs = readableStreamFrom(makeData(32 * 1024));
const info = await os.put({ name: "t" }, rs);
assertEquals(info.size, 32 * 1024);
assertEquals(info.chunks, 4);
assertEquals(info.options?.max_chunk_size, 8 * 1024);
await cleanup(ns, nc);
});
Deno.test("objectstore - default chunk is 128k", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
assertEquals(nc.info?.max_payload, 1024 * 1024);
const objm = new Objm(nc);
const os = await objm.create("test");
const rs = readableStreamFrom(makeData(129 * 1024));
const info = await os.put({ name: "t" }, rs);
assertEquals(info.size, 129 * 1024);
assertEquals(info.chunks, 2);
assertEquals(info.options?.max_chunk_size, 128 * 1024);
await cleanup(ns, nc);
});
Deno.test("objectstore - sanitize", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
await os.put({ name: "has.dots.here" }, readableStreamFrom(makeData(1)));
await os.put(
{ name: "the spaces are here" },
readableStreamFrom(makeData(1)),
);
const info = await os.status({
subjects_filter: ">",
});
const subjects = info.streamInfo.state?.subjects || {};
assertEquals(
subjects[`$O.test.M.${Base64UrlPaddedCodec.encode("has.dots.here")}`],
1,
);
assertEquals(
subjects[
`$O.test.M.${Base64UrlPaddedCodec.encode("the spaces are here")}`
],
1,
);
await cleanup(ns, nc);
});
Deno.test("objectstore - partials", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
const data = new TextEncoder().encode("".padStart(7, "a"));
const info = await os.put(
{ name: "test", options: { max_chunk_size: 2 } },
readableStreamFrom(data),
);
assertEquals(info.chunks, 4);
assertEquals(info.digest, digest(data));
const rs = await os.get("test");
const reader = rs!.data.getReader();
let i = 0;
while (true) {
i++;
const { done, value } = await reader.read();
if (done) {
assertEquals(i, 5);
break;
}
if (i === 4) {
assertEquals(value!.length, 1);
} else {
assertEquals(value!.length, 2);
}
}
await cleanup(ns, nc);
});
Deno.test("objectstore - no store", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
await os.put({ name: "test" }, readableStreamFrom(Empty));
await os.delete("test");
const oi = await os.info("test");
await assertRejects(
async () => {
await os.link("bar", oi!);
},
Error,
"object is deleted",
);
const r = await os.delete("foo");
assertEquals(r, { purged: 0, success: false });
await assertRejects(
async () => {
await os.update("baz", oi!);
},
Error,
"object not found",
);
const jsm = await jetstreamManager(nc);
await jsm.streams.delete("OBJ_test");
await assertRejects(
async () => {
await os.seal();
},
Error,
"object store not found",
);
await assertRejects(
async () => {
await os.status();
},
Error,
"object store not found",
);
await assertRejects(
async () => {
await os.put({ name: "foo" }, readableStreamFrom(Empty));
},
Error,
"stream not found",
);
await cleanup(ns, nc);
});
Deno.test("objectstore - hashtests", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("hashes");
const base =
"https://raw.githubusercontent.com/nats-io/nats.client.deps/main/digester_test/";
const tests: { hash: string; file: string }[] = [{
hash: "IdgP4UYMGt47rgecOqFoLrd24AXukHf5-SVzqQ5Psg8=",
file: "digester_test_bytes_000100.txt",
}, {
hash: "DZj4RnBpuEukzFIY0ueZ-xjnHY4Rt9XWn4Dh8nkNfnI=",
file: "digester_test_bytes_001000.txt",
}, {
hash: "RgaJ-VSJtjNvgXcujCKIvaheiX_6GRCcfdRYnAcVy38=",
file: "digester_test_bytes_010000.txt",
}, {
hash: "yan7pwBVnC1yORqqgBfd64_qAw6q9fNA60_KRiMMooE=",
file: "digester_test_bytes_100000.txt",
}];
for (let i = 0; i < tests.length; i++) {
const t = tests[i];
const r = await fetch(`${base}${t.file}`);
const rs = await r.blob();
const oi = await os.put(
{ name: t.hash, options: { max_chunk_size: 9 } },
rs.stream(),
{ timeout: 20_000 },
);
assertEquals(oi.digest, `${digestType}${t.hash}`);
}
await cleanup(ns, nc);
});
Deno.test("objectstore - meta update", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
// cannot update meta of an object that doesn't exist
await assertRejects(
async () => {
await os.update("A", { name: "B" });
},
Error,
"object not found",
);
// cannot update the meta of a deleted object
await os.put({ name: "D" }, readableStreamFrom(makeData(1)));
await os.delete("D");
await assertRejects(
async () => {
await os.update("D", { name: "DD" });
},
Error,
"cannot update meta for a deleted object",
);
// cannot update the meta to an object that already exists
await os.put({ name: "A" }, readableStreamFrom(makeData(1)));
await os.put({ name: "B" }, readableStreamFrom(makeData(1)));
await assertRejects(
async () => {
await os.update("A", { name: "B" });
},
Error,
"an object already exists with that name",
);
await cleanup(ns, nc);
});
Deno.test("objectstore - cannot put links", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("test");
const link = { bucket: "test", name: "a" };
const mm = {
name: "ref",
options: { link: link },
} as ObjectStoreMeta;
await assertRejects(
async () => {
await os.put(mm, readableStreamFrom(new TextEncoder().encode("a")));
},
Error,
"link cannot be set when putting the object in bucket",
);
await cleanup(ns, nc);
});
Deno.test("objectstore - put purges old entries", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("OBJS", { description: "testing" });
// we expect 10 messages per put
const t = async (first: number, last: number) => {
const status = await os.status();
const si = status.streamInfo;
assertEquals(si.state.first_seq, first);
assertEquals(si.state.last_seq, last);
};
const blob = new Uint8Array(9);
let oi = await os.put(
{ name: "BLOB", description: "myblob", options: { max_chunk_size: 1 } },
readableStreamFrom(crypto.getRandomValues(blob)),
);
assertEquals(oi.revision, 10);
await t(1, 10);
oi = await os.put(
{ name: "BLOB", description: "myblob", options: { max_chunk_size: 1 } },
readableStreamFrom(crypto.getRandomValues(blob)),
);
assertEquals(oi.revision, 20);
await t(11, 20);
await cleanup(ns, nc);
});
Deno.test("objectstore - put previous sequences", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("OBJS", { description: "testing" });
// putting the first
let oi = await os.put(
{ name: "A", options: { max_chunk_size: 1 } },
readableStreamFrom(crypto.getRandomValues(new Uint8Array(9))),
{ previousRevision: 0 },
);
assertEquals(oi.revision, 10);
// putting another value, but the first value for the key - so previousRevision is 0
oi = await os.put(
{ name: "B", options: { max_chunk_size: 1 } },
readableStreamFrom(crypto.getRandomValues(new Uint8Array(3))),
{ previousRevision: 0 },
);
assertEquals(oi.revision, 14);
// update A, previous A is found at 10
oi = await os.put(
{ name: "A", options: { max_chunk_size: 1 } },
readableStreamFrom(crypto.getRandomValues(new Uint8Array(3))),
{ previousRevision: 10 },
);
assertEquals(oi.revision, 18);
// update A, previous A is found at 18
oi = await os.put(
{ name: "A", options: { max_chunk_size: 1 } },
readableStreamFrom(Empty),
{ previousRevision: 18 },
);
assertEquals(oi.revision, 19);
await cleanup(ns, nc);
});
Deno.test("objectstore - put/get blob", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("OBJS", { description: "testing" });
const payload = new Uint8Array(9);
// putting the first
await os.put(
{ name: "A", options: { max_chunk_size: 1 } },
readableStreamFrom(payload),
{ previousRevision: 0 },
);
let bg = await os.getBlob("A");
assertExists(bg);
assertEquals(bg.length, payload.length);
assertEquals(bg, payload);
await os.putBlob({ name: "B", options: { max_chunk_size: 1 } }, payload);
bg = await os.getBlob("B");
assertExists(bg);
assertEquals(bg.length, payload.length);
assertEquals(bg, payload);
await cleanup(ns, nc);
});
Deno.test("objectstore - ttl", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const ttl = nanos(60 * 1000);
const os = await objm.create("OBJS", { ttl });
const status = await os.status();
assertEquals(status.ttl, ttl);
await cleanup(ns, nc);
});
Deno.test("objectstore - allow direct", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.6.3")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("OBJS");
const status = await os.status();
assertEquals(status.streamInfo.config.allow_direct, true);
await cleanup(ns, nc);
});
Deno.test("objectstore - stream metadata and entry metadata", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
if (await notCompatible(ns, nc, "2.10.0")) {
return;
}
const objm = new Objm(nc);
const os = await objm.create("OBJS", {
description: "testing",
metadata: { hello: "world" },
});
const status = await os.status();
assertEquals(status.metadata?.hello, "world");
const info = await os.putBlob(
{ name: "hello", metadata: { world: "hello" } },
Empty,
);
assertEquals(info.metadata?.world, "hello");
const hi = await os.info("hello");
assertExists(hi);
assertEquals(hi.metadata?.world, "hello");
await cleanup(ns, nc);
});
Deno.test("os - compression", async () => {
const { ns, nc } = await setup(jetstreamServerConf());
const objm = new Objm(nc);
const s2 = await objm.create("compressed", {
compression: true,
});
let status = await s2.status();
assertEquals(status.compression, true);
const none = await objm.create("none");
status = await none.status();
assertEquals(status.compression, false);
await cleanup(ns, nc);
});
Deno.test("os - os rejects in older servers", async () => {
const { ns, nc } = await setup(
jetstreamServerConf({
max_payload: 1024 * 1024,
}),
);
const nci = nc as NatsConnectionImpl;
const objm = new Objm(nc);
async function t(version: string, ok: boolean): Promise<void> {
nci.features.update(version);
if (!ok) {
await assertRejects(
async () => {
await objm.create(nuid.next());
},
Error,
`objectstore is only supported on servers 2.6.3 or better`,
);
} else {
await objm.create(nuid.next());
}
}
await t("2.6.2", false);
await t("2.6.3", true);
await cleanup(ns, nc);
});
Deno.test("os - objm open", async () => {
const { ns, nc } = await setup(jetstreamServerConf({}));
const objm = new Objm(nc);
await assertRejects(
() => {
return objm.open("hello");
},
Error,
"object store not found",
);
let obj = await objm.open("hello", false);
await assertRejects(
() => {
return obj.get("hello");
},
Error,
"stream not found",
);
await assertRejects(
() => {
return obj.put({ name: "hi" }, readableStreamFrom(Empty));
},
Error,
"stream not found",
);
await objm.create("hello");
obj = await objm.open("hello");
const oi = await obj.put({ name: "hello" }, readableStreamFrom(Empty));
assertEquals(oi.name, "hello");
await cleanup(ns, nc);
});
Deno.test("os - objm creates right number of replicas", async () => {
const servers = await NatsServer.jetstreamCluster(3);
const nc = await connect({ port: servers[0].port });
const objm = new Objm(nc);
const obj = await objm.create("test", { replicas: 3 });
const status = await obj.status();
assertEquals(status.replicas, 3);
await nc.close();
await NatsServer.stopAll(servers, true);
});
--- obj/tests/sha_digest_test.ts ---
/*
* Copyright 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 { assert, assertEquals, assertFalse } from "@std/assert";
import { checkSha256, parseSha256 } from "../src/sha_digest.parser.ts";
// Known SHA256 hash of "hello world" for testing
const HELLO_WORLD_SHA256 = new Uint8Array([
0xb9,
0x4d,
0x27,
0xb9,
0x93,
0x4d,
0x3e,
0x08,
0xa5,
0x2e,
0x52,
0xd7,
0xda,
0x7d,
0xab,
0xfa,
0xc4,
0x84,
0xef,
0xe3,
0x7a,
0x53,
0x80,
0xee,
0x90,
0x88,
0xf7,
0xac,
0xe2,
0xef,
0xcd,
0xe9,
]);
const HELLO_WORLD_HEX_LOWER =
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
const HELLO_WORLD_HEX_UPPER =
"B94D27B9934D3E08A52E52D7DA7DABFAC484EFE37A5380EE9088F7ACE2EFCDE9";
const HELLO_WORLD_BASE64 = "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=";
const HELLO_WORLD_BASE64_URL = "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4DukIj3rOLvzek=";
Deno.test("sha_digest - parseSha256 from all formats", () => {
assertEquals(parseSha256(HELLO_WORLD_HEX_LOWER), HELLO_WORLD_SHA256);
assertEquals(parseSha256(HELLO_WORLD_HEX_UPPER), HELLO_WORLD_SHA256);
assertEquals(parseSha256(HELLO_WORLD_BASE64), HELLO_WORLD_SHA256);
assertEquals(parseSha256(HELLO_WORLD_BASE64_URL), HELLO_WORLD_SHA256);
});
Deno.test("sha_digest - parseSha256 mixed case hex falls back to base64", () => {
// Mixed case hex fails hex validation, tries base64 instead
const mixedCase =
"B94d27b9934D3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
const result = parseSha256(mixedCase);
// Compute expected: base64 decode of the mixed case string
const expected = Uint8Array.from(atob(mixedCase), (c) => c.charCodeAt(0));
assertEquals(result, expected);
});
Deno.test("sha_digest - parseSha256 odd length hex falls back to base64", () => {
// Odd length fails hex validation, tries base64 instead
const oddLength =
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde";
const result = parseSha256(oddLength);
// Compute expected: base64 decode of the odd length string
const expected = Uint8Array.from(atob(oddLength), (c) => c.charCodeAt(0));
assertEquals(result, expected);
});
Deno.test("sha_digest - parseSha256 valid base64-like string", () => {
// "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=" is base64 for "abcdefghijklmnopqrstuvwxyz"
const validBase64Chars = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=";
const result = parseSha256(validBase64Chars);
const expected = new TextEncoder().encode("abcdefghijklmnopqrstuvwxyz");
assertEquals(result, expected);
});
Deno.test("sha_digest - parseSha256 empty string returns empty array", () => {
const result = parseSha256("");
assert(result !== null);
assertEquals(result.length, 0);
});
Deno.test("sha_digest - checkSha256 with matching hex strings (lowercase)", () => {
const result = checkSha256(HELLO_WORLD_HEX_LOWER, HELLO_WORLD_HEX_LOWER);
assert(result);
});
Deno.test("sha_digest - checkSha256 with matching hex strings (uppercase)", () => {
const result = checkSha256(HELLO_WORLD_HEX_UPPER, HELLO_WORLD_HEX_UPPER);
assert(result);
});
Deno.test("sha_digest - checkSha256 with matching different case hex", () => {
const result = checkSha256(HELLO_WORLD_HEX_LOWER, HELLO_WORLD_HEX_UPPER);
assert(result);
});
Deno.test("sha_digest - checkSha256 with matching base64 strings", () => {
const result = checkSha256(HELLO_WORLD_BASE64, HELLO_WORLD_BASE64);
assert(result);
});
Deno.test("sha_digest - checkSha256 with hex and base64 (same hash)", () => {
const result = checkSha256(HELLO_WORLD_HEX_LOWER, HELLO_WORLD_BASE64);
assert(result);
});
Deno.test("sha_digest - checkSha256 with base64 and hex (same hash)", () => {
const result = checkSha256(HELLO_WORLD_BASE64, HELLO_WORLD_HEX_UPPER);
assert(result);
});
Deno.test("sha_digest - checkSha256 with Uint8Array inputs", () => {
const bytes1 = parseSha256(HELLO_WORLD_HEX_LOWER)!;
const bytes2 = parseSha256(HELLO_WORLD_BASE64)!;
const result = checkSha256(bytes1, bytes2);
assert(result);
});
Deno.test("sha_digest - checkSha256 with mixed string and Uint8Array", () => {
const bytes = parseSha256(HELLO_WORLD_HEX_LOWER)!;
const result = checkSha256(bytes, HELLO_WORLD_BASE64);
assert(result);
});
Deno.test("sha_digest - checkSha256 returns false for different hashes", () => {
const other =
"a94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
const result = checkSha256(HELLO_WORLD_HEX_LOWER, other);
assertFalse(result);
});
Deno.test("sha_digest - checkSha256 returns false for invalid input", () => {
const result = checkSha256("invalid", HELLO_WORLD_HEX_LOWER);
assertFalse(result);
});
Deno.test("sha_digest - checkSha256 returns false for different length arrays", () => {
const short = new Uint8Array([0xb9, 0x4d]);
const long = parseSha256(HELLO_WORLD_HEX_LOWER)!;
const result = checkSha256(short, long);
assertFalse(result);
});
Deno.test("sha_digest - checkSha256 returns false when first byte differs", () => {
const original = parseSha256(HELLO_WORLD_HEX_LOWER)!;
const modified = new Uint8Array(original);
modified[0] = 0x00; // Change first byte
const result = checkSha256(original, modified);
assertFalse(result);
});
Deno.test("sha_digest - checkSha256 returns false when last byte differs", () => {
const original = parseSha256(HELLO_WORLD_HEX_LOWER)!;
const modified = new Uint8Array(original);
modified[31] = 0x00; // Change last byte
const result = checkSha256(original, modified);
assertFalse(result);
});
Deno.test("sha_digest - parseSha256 rejects odd length hex", () => {
// Hex parsing should fail on odd length, fallback to base64
const oddHex = "abc"; // 3 chars (odd)
const result = parseSha256(oddHex);
// It will try base64 decode instead
const expected = Uint8Array.from(atob(oddHex), (c) => c.charCodeAt(0));
assertEquals(result, expected);
});
Deno.test("sha_digest - parseSha256 returns null for unrecognized format", () => {
// String with special characters that fails both hex and base64 detection
const invalid = "!@#$%^&*()";
const result = parseSha256(invalid);
assertEquals(result, null);
});
Deno.test("sha_digest - checkSha256 returns false when parseSha256 returns null", () => {
// Both inputs invalid
const invalid1 = "!@#$";
const invalid2 = "^&*()";
assertFalse(checkSha256(invalid1, invalid2));
// One invalid, one valid
assertFalse(checkSha256(invalid1, HELLO_WORLD_HEX_LOWER));
assertFalse(checkSha256(HELLO_WORLD_HEX_LOWER, invalid2));
});
--- obj/README.md ---
[![License](https://img.shields.io/badge/Licence-Apache%202.0-blue.svg)](./LICENSE)
![obj](https://github.com/nats-io/nats.js/actions/workflows/test.yml/badge.svg)
[![JSDoc](https://img.shields.io/badge/JSDoc-reference-blue)](https://nats-io.github.io/nats.js/obj/index.html)
[![JSR](https://jsr.io/badges/@nats-io/obj)](https://jsr.io/@nats-io/obj)
[![JSR](https://jsr.io/badges/@nats-io/obj/score)](https://jsr.io/@nats-io/obj)
[![NPM Version](https://img.shields.io/npm/v/%40nats-io%2Fobj)](https://www.npmjs.com/package/@nats-io/obj)
![NPM Downloads](https://img.shields.io/npm/dt/%40nats-io%2Fobj)
![NPM Downloads](https://img.shields.io/npm/dm/%40nats-io%2Fobj)
# Obj
The obj module implements the NATS ObjectStore functionality using JetStream for
JavaScript clients. JetStream clients can use streams to store and access data.
Obj is materialized view that presents a different _API_ to interact with the
data stored in a stream using the API for an ObjectStore which should be
familiar to many application developers.
## Installation
For a quick overview of the libraries and how to install them, see
[runtimes.md](../runtimes.md).
Note that this library is distributed in two different registries:
- npm a node-specific library supporting CJS (`require`) and ESM (`import`)
- jsr a node and other ESM (`import`) compatible runtimes (deno, browser, node)
If your application doesn't use `require`, you can simply depend on the JSR
version.
### NPM
The NPM registry hosts a node-only compatible version of the library
[@nats-io/obj](https://www.npmjs.com/package/@nats-io/obj) supporting both CJS
and ESM:
```bash
npm install @nats-io/obj
```
### JSR
The JSR registry hosts the EMS-only [@nats-io/obj](https://jsr.io/@nats-io/obj)
version of the library.
```bash
deno add jsr:@nats-io/obj
```
```bash
npx jsr add @nats-io/obj
```
```bash
yarn dlx jsr add @nats-io/obj
```
```bash
bunx jsr add @nats-io/obj
```
## Referencing the library
Once you import the library, you can reference in your code as:
```javascript
import { Objm } from "@nats-io/obj";
// or in node (only when using CJS)
const { Objm } = require("@nats-io/obj");
// using a nats connection:
const objm = new Objm(nc);
await objm.list();
await objm.create("myobj");
```
If you want to customize some of the JetStream options when working with KV, you
can:
```typescript
import { jetStream } from "@nats-io/jetstream";
import { Objm } from "@nats-io/obj";
const js = jetstream(nc, { timeout: 10_000 });
// KV will inherit all the options from the JetStream client
const objm = new Objm(js);
```
```typescript
// create the named ObjectStore or bind to it if it exists:
const objm = new Objm(nc);
const os = await objm.create("testing", { storage: StorageType.File });
// ReadableStreams allows JavaScript to work with large data without
// necessarily keeping it all in memory.
//
// ObjectStore reads and writes to JetStream via ReadableStreams
// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
// You can easily create ReadableStreams from static data or iterators
// here's an example of creating a readable stream from static data
function readableStreamFrom(data: Uint8Array): ReadableStream<Uint8Array> {
return new ReadableStream<Uint8Array>({
pull(controller) {
// the readable stream adds data
controller.enqueue(data);
controller.close();
},
});
}
// reading from a ReadableStream is similar to working with an async iterator:
async function fromReadableStream(
rs: ReadableStream<Uint8Array>,
) {
let i = 1;
const reader = rs.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
if (value && value.length) {
// do something with the accumulated data
console.log(`chunk ${i++}: ${sc.decode(value)}`);
}
}
}
let e = await os.get("hello");
console.log(`hello entry exists? ${e !== null}`);
// watch notifies when a change in the object store happens
const watch = await os.watch();
(async () => {
for await (const i of watch) {
// when asking for history you get a null
// that tells you when all the existing values
// are provided
if (i === null) {
continue;
}
console.log(`watch: ${i!.name} deleted?: ${i!.deleted}`);
}
})();
// putting an object returns an info describing the object
const info = await os.put({
name: "hello",
description: "first entry",
options: {
max_chunk_size: 1,
},
}, readableStreamFrom(sc.encode("hello world")));
console.log(
`object size: ${info.size} number of chunks: ${info.size} deleted: ${info.deleted}`,
);
// reading it back:
const r = await os.get("hello");
// it is possible while we read the ReadableStream that something goes wrong
// the error property on the result will resolve to null if there's no error
// otherwise to the error from the ReadableStream
r?.error.then((err) => {
if (err) {
console.error("reading the readable stream failed:", err);
}
});
// use our sample stream reader to process output to the console
// chunk 1: h
// chunk 2: e
// ...
// chunk 11: d
await fromReadableStream(r!.data);
// list all the entries in the object store
// returns the info for each entry
const list = await os.list();
list.forEach((i) => {
console.log(`list: ${i.name}`);
});
// you can also get info on the object store as a whole:
const status = await os.status();
console.log(`bucket: '${status.bucket}' size in bytes: ${status.size}`);
// you can prevent additional modifications by sealing it
const final = await os.seal();
console.log(`bucket: '${final.bucket}' sealed: ${final.sealed}`);
// only other thing that you can do is destroy it
// this gets rid of the objectstore
const destroyed = await os.destroy();
console.log(`destroyed: ${destroyed}`);
```
--- services/examples/01_services.ts ---
/*
* Copyright 2022-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, type QueuedIterator } from "@nats-io/transport-deno";
import {
ServiceError,
ServiceErrorCodeHeader,
ServiceErrorHeader,
Svcm,
} from "../src/mod.ts";
import type { ServiceMsg, ServiceStats } from "../src/mod.ts";
import { assertEquals } from "@std/assert";
// connect to NATS on demo.nats.io
const nc = await connect({ servers: ["demo.nats.io"] });
// All services have some basic stats that are collected like the
// number of requests processed, etc. Your service can accumulate
// other stats, and aggregate them to the stats report that you
// can retrieve via the monitoring stats() api.
// In this example, the service keeps track of the largest number
// it has seen and defines a custom statsHandler that aggregates
// it to the standard report
let maxMax = 0;
const statsHandler = (): Promise<unknown> => {
return Promise.resolve({ max: maxMax });
};
// create a service - using the statsHandler
const svc = new Svcm(nc);
const service = await svc.add({
name: "max",
version: "0.0.1",
description: "returns max number in a request",
statsHandler,
});
// add an endpoint listening on "max"
const max = service.addEndpoint("max");
// a service has the `stopped` property - which is a promise that
// resolves to null or an error (not rejects). This promise resolves
// whenever the service stops, so you can use a handler like this
// to activate some logic such as logging the service stopping
// when that happens.
service.stopped.then((err: Error | null) => {
console.log(`service stopped ${err ? "because: " + err.message : ""}`);
});
// starting the service as an async function so that
// we can have this example be in a single file
(async () => {
for await (const r of max) {
// most of the logic is about validating the input
// and returning an error to the client if the input
// is not what we expect.
decoder(r)
.then((a) => {
// we were able to parse an array of numbers from the request
// with at least one entry, we sort in reverse order
a.sort((a, b) => b - a);
// and first entry has the largest number
const max = a[0];
// since our service also tracks the largest number ever seen
// we update our largest number
maxMax = Math.max(maxMax, max);
console.log(
`${service.info().name} calculated a response of ${max} from ${a.length} values`,
);
// finally we respond with a JSON number payload with the maximum value
r.respond(JSON.stringify(max));
})
.catch((err) => {
// if we are here, the initial processing of the array failed
// the message presented to the service is wrapped as a "ServiceMsg"
// which adds a simple way to represent errors to your clients
console.log(`${service.info().name} got a bad request: ${err.message}`);
// respondError sets the `Nats-Service-Error-Code` and `Nats-Service-Error`
// headers on the message. This allows a client to check if the response
// is an error
r.respondError(
(err as ServiceError).code || 400,
err.message,
JSON.stringify(0),
);
});
}
})();
// decoder extracts a JSON payload and expects it to be an array of numbers
function decoder(r: ServiceMsg): Promise<number[]> {
try {
// decode JSON
const a = r.json();
// if not an array, this is bad input
if (!Array.isArray(a)) {
return Promise.reject(
new ServiceError(400, "input must be an array"),
);
}
// if we don't have at least one number, this is bad input
if (a.length < 1) {
return Promise.reject(
new ServiceError(400, "input must have more than one element"),
);
}
// if we find an entry in the array that is not a number, we have bad input
const bad = a.find((e) => {
return typeof e !== "number";
});
if (bad) {
return Promise.reject(
new ServiceError(400, "input contains invalid types"),
);
}
// otherwise we are good
return Promise.resolve(a);
} catch (err) {
// this is JSON.parse() - failing to parse JSON
return Promise.reject(new ServiceError(400, (err as Error).message));
}
}
// Now we switch gears and look at a client making a request:
// we call the service without any payload and expect some errors
await nc.request("max").then((r: ServiceMsg) => {
// errors are really these two headers set on the message
assertEquals(r.headers?.get(ServiceErrorHeader), "Bad JSON");
assertEquals(r.headers?.get(ServiceErrorCodeHeader), "400");
});
// call it with an empty array also expecting an error response
await nc.request("max", JSON.stringify([])).then((r: ServiceMsg) => {
// Here's an alternative way of checking if the response is an error response
assertEquals(ServiceError.isServiceError(r), true);
const se = ServiceError.toServiceError(r);
assertEquals(se?.message, "input must have more than one element");
assertEquals(se?.code, 400);
});
// call it with valid arguments
await nc.request("max", JSON.stringify([1, 10, 100])).then((r: ServiceMsg) => {
// no error headers
assertEquals(ServiceError.isServiceError(r), false);
// and the response is on the payload, so we process the JSON we
// got from the service
assertEquals(r.json<number>(), 100);
});
// Monitoring
// The monitoring APIs return a promise, so that the client can start
// processing responses as they come in
// collect() simply waits for the iterator to stop (when we think we have
// all the responses)
async function collect<T>(p: Promise<QueuedIterator<T>>): Promise<T[]> {
const iter = await p;
const buf: T[] = [];
for await (const v of iter) {
buf.push(v);
}
return buf;
}
const m = svc.client();
// discover
const found = await collect(m.ping());
assertEquals(found.length, 1);
assertEquals(found[0].name, "max");
// get stats
await collect(m.stats("max", found[0].id));
// The monitoring API is made using specific subjects
// We can do standard request reply, however that will return
// only the first response we get - if you have multiple services
// the first one that responded wins. In this particular case
// we are only expecting a single response because the request is
// addressed to a specific service instance.
// All monitoring subjects have the format:
// $SRV.<VERB>.<name>.<id>
// where the verb can be 'PING', 'INFO', or 'STATS'
// the name is optional but matches a service name.
// The id is also optional, but you must know it (from ping or one of
// other requests that allowed you to discover the service) to
// target the service specifically as we do here:
const r = await nc.request(`$SRV.STATS.max.${found[0].id}`);
const stats = r.json<ServiceStats>();
assertEquals(stats.name, "max");
assertEquals(stats.endpoints?.[0].num_requests, 3);
assertEquals((stats.endpoints?.[0].data as { max: number }).max, 100);
// stop the service
await service.stop();
// and close the connection
await nc.close();
--- services/examples/02_multiple_endpoints.ts ---
/*
* Copyright 2022-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 } from "@nats-io/transport-deno";
import { ServiceError, Svcm } from "../src/mod.ts";
import type { ServiceMsg } from "../src/mod.ts";
// connect to NATS on demo.nats.io
const nc = await connect({ servers: ["demo.nats.io"] });
// create a service - using the statsHandler and decoder
const svc = new Svcm(nc);
const calc = await svc.add({
name: "calc",
version: "0.0.1",
description: "example calculator service",
metadata: {
"example": "entry",
},
});
// For this example the thing we want to showcase is how you can
// create service that has multiple endpoints.
// The service will have `sum`, `max`, `average` and `min` operations.
// While we could create a service that listens on `sum`, `max`, etc.,
// creating a complex hierarchy will allow you to carve the subject
// name space to allow for better access control, and organize your
// services better.
// In the service API, you `addGroup()` to the service, which will
// introduce a prefix for the calculator's endpoints. The group name
// can be any valid subject that can be prefixed into another.
const g = calc.addGroup("calc");
// We can now add endpoints under this which will augment the subject
// space, adding an endpoint. Endpoints can only have a simple name
// and can specify an optional callback:
// this is the simplest endpoint - returns an iterator
// additional options such as a handler, subject, or schema can be
// specified.
// this endpoint is accessible as `calc.sum`
const sums = g.addEndpoint("sum", {
metadata: {
"input": "JSON number array",
"output": "JSON number with the sum",
},
});
(async () => {
for await (const m of sums) {
const numbers = decode(m);
const s = numbers.reduce((sum, v) => {
return sum + v;
});
m.respond(JSON.stringify(s));
}
})().then();
// Here's another implemented using a callback, will be accessible by `calc.average`:
g.addEndpoint("average", {
handler: (err, m) => {
if (err) {
calc.stop(err);
return;
}
const numbers = decode(m);
const sum = numbers.reduce((sum, v) => {
return sum + v;
});
m.respond(JSON.stringify(sum / numbers.length));
},
metadata: {
"input": "JSON number array",
"output": "JSON number average value found in the array",
},
});
// and another using a callback, and specifying our schema:
g.addEndpoint("min", {
handler: (err, m) => {
if (err) {
calc.stop(err);
return;
}
const numbers = decode(m);
const min = numbers.reduce((n, v) => {
return Math.min(n, v);
});
m.respond(JSON.stringify(min));
},
metadata: {
"input": "JSON number array",
"output": "JSON number min value found in the array",
},
});
g.addEndpoint("max", {
handler: (err, m) => {
if (err) {
calc.stop(err);
return;
}
const numbers = decode(m);
const max = numbers.reduce((n, v) => {
return Math.max(n, v);
});
m.respond(JSON.stringify(max));
},
metadata: {
"input": "JSON number array",
"output": "JSON number max value found in the array",
},
});
calc.stopped.then((err: Error | null) => {
console.log(`calc stopped ${err ? "because: " + err.message : ""}`);
});
// Now we switch gears and look at a client making a request
async function calculate(op: string, a: number[]): Promise<void> {
const r = await nc.request(`calc.${op}`, JSON.stringify(a));
if (ServiceError.isServiceError(r)) {
console.log(ServiceError.toServiceError(r));
return;
}
const ans = r.json<number>();
console.log(`${op} ${a.join(", ")} = ${ans}`);
}
await Promise.all([
calculate("sum", [5, 10, 15]),
calculate("average", [5, 10, 15]),
calculate("min", [5, 10, 15]),
calculate("max", [5, 10, 15]),
]);
// stop the service
await calc.stop();
// and close the connection
await nc.close();
// a simple decoder that tosses a ServiceError if the input is not what we want.
function decode(m: ServiceMsg): number[] {
const a = m.json<number[]>();
if (!Array.isArray(a)) {
throw new ServiceError(400, "input requires array");
}
if (a.length === 0) {
throw new ServiceError(400, "array must have at least one number");
}
a.forEach((v) => {
if (typeof v !== "number") {
throw new ServiceError(400, "array elements must be numbers");
}
});
return a;
}
--- services/examples/03_bigdata-client.ts ---
/*
* Copyright 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 { parse } from "@std/flags";
import { connect, type ConnectionOptions } from "@nats-io/transport-deno";
import { humanizeBytes } from "./03_util.ts";
const argv = parse(
Deno.args,
{
alias: {
"s": ["server"],
"a": ["asset"],
"c": ["chunk"],
},
default: {
s: "demo.nats.io",
c: 0,
a: 1024 * 1024,
},
string: ["server"],
},
);
const copts = { servers: argv.s } as ConnectionOptions;
const nc = await connect(copts);
const max_chunk = argv.c;
const size = argv.a as number;
console.log(`requesting ${humanizeBytes(size)}`);
const start = performance.now();
const iter = await nc.requestMany(
"data",
JSON.stringify({ max_chunk, size }),
{
strategy: "sentinel",
maxWait: 10000,
},
);
let received = 0;
let count = 0;
for await (const m of iter) {
count++;
received += m.data.length;
if (m.data.length === 0) {
// sentinel
count--;
}
}
console.log(performance.now() - start, "ms");
console.log(`received ${count} responses: ${humanizeBytes(received)} bytes`);
await nc.close();
--- services/examples/03_bigdata.ts ---
/*
* Copyright 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 } from "@nats-io/transport-deno";
import { Svcm } from "../src/mod.ts";
import { humanizeBytes } from "./03_util.ts";
import type { DataRequest } from "./03_util.ts";
const nc = await connect({ servers: "demo.nats.io" });
const svc = new Svcm(nc);
const srv = await svc.add({
name: "big-data",
version: "0.0.1",
});
srv.addEndpoint("data", (_err, msg) => {
queueMicrotask(() => {
if (msg.data.length === 0) {
msg.respondError(400, "missing request options");
return;
}
const max = nc.info?.max_payload ?? 1024 * 1024;
const r = msg.json<DataRequest>();
const size = r.size || max;
let chunk = r.max_chunk || max;
chunk = chunk > size ? size : chunk;
const full = Math.floor(size / chunk);
const partial = size % chunk;
console.log(
"size of request",
humanizeBytes(size),
"chunk",
humanizeBytes(chunk),
);
console.log(
"full buffers:",
full,
"partial:",
partial,
"bytes",
"empty:",
1,
);
const payload = new Uint8Array(chunk);
for (let i = 0; i < full; i++) {
msg.respond(payload);
}
if (partial) {
msg.respond(payload.subarray(0, partial));
}
// sentinel
msg.respond();
});
});
console.log(srv.info());
--- services/examples/03_util.ts ---
/*
* Copyright 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.
*/
export function humanizeBytes(bytes: number, si = false): string {
const base = si ? 1000 : 1024;
const pre = si
? ["k", "M", "G", "T", "P", "E"]
: ["K", "M", "G", "T", "P", "E"];
const post = si ? "iB" : "B";
if (bytes < base) {
return `${bytes.toFixed(2)} ${post}`;
}
const exp = parseInt(Math.log(bytes) / Math.log(base) + "");
const index = parseInt((exp - 1) + "");
return `${(bytes / Math.pow(base, exp)).toFixed(2)} ${pre[index]}${post}`;
}
export type DataRequest = {
// generate data that is this size
size: number;
// send using multiple messages - note server will reject if too big
max_chunk?: number;
};
--- services/tests/service_test.ts ---
/*
* Copyright 2022-2024 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 { cleanup, setup } from "test_helpers";
import { ServiceImpl } from "../src/service.ts";
import {
assert,
assertArrayIncludes,
assertEquals,
assertExists,
assertInstanceOf,
assertRejects,
assertThrows,
fail,
} from "@std/assert";
import {
collect,
createInbox,
deadline,
delay,
errors,
} from "@nats-io/nats-core/internal";
import type {
Msg,
NatsConnection,
NatsConnectionImpl,
QueuedIterator,
SubscriptionImpl,
} from "@nats-io/nats-core/internal";
import type {
EndpointInfo,
Service,
ServiceConfig,
ServiceIdentity,
ServiceInfo,
ServiceStats,
} from "../src/mod.ts";
import {
ServiceError,
ServiceErrorCodeHeader,
ServiceErrorHeader,
ServiceResponseType,
ServiceVerb,
Svcm,
} from "../src/mod.ts";
Deno.test("service - control subject", () => {
const test = (verb: ServiceVerb) => {
assertEquals(ServiceImpl.controlSubject(verb), `$SRV.${verb}`);
assertEquals(ServiceImpl.controlSubject(verb, "NamE"), `$SRV.${verb}.NamE`);
assertEquals(
ServiceImpl.controlSubject(verb, "nAmE", "Id"),
`$SRV.${verb}.nAmE.Id`,
);
assertEquals(
ServiceImpl.controlSubject(verb, "nAMe", "iD", "hello.service"),
`hello.service.${verb}.nAMe.iD`,
);
};
[ServiceVerb.INFO, ServiceVerb.PING, ServiceVerb.STATS]
.forEach((v) => {
test(v);
});
});
Deno.test("service - bad name", async () => {
const { ns, nc } = await setup({}, {});
const svc = new Svcm(nc);
const t = async (name: string, msg: string) => {
await assertRejects(
async () => {
await svc.add({
name: name,
version: "1.0.0",
});
},
Error,
msg,
);
};
await t("/", "name cannot contain '/'");
await t(" ", "name cannot contain ' '");
await t(">", "name cannot contain '>'");
await t("", "name required");
await cleanup(ns, nc);
});
Deno.test("service - client", async () => {
const { ns, nc } = await setup({}, {});
const subj = createInbox();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "test",
version: "1.0.0",
description: "responds with hello",
}) as ServiceImpl;
srv.addEndpoint("hello", {
handler: (_err, msg) => {
msg?.respond("hello");
},
subject: subj,
});
await nc.request(subj);
await nc.request(subj);
const m = svc.client();
function verifyIdentity(ids: ServiceIdentity[]) {
assertEquals(ids.length, 1);
const e = ids[0];
assertEquals(e.id, srv.id);
assertEquals(e.name, srv.name);
assertEquals(e.version, srv.version);
}
function verifyPing(pings: ServiceIdentity[]) {
verifyIdentity(pings);
const ping = pings[0];
assertEquals(ping.type, ServiceResponseType.PING);
const r = ping as unknown as Record<string, unknown>;
delete r.version;
delete r.name;
delete r.id;
delete r.type;
assertEquals(Object.keys(r).length, 0, JSON.stringify(r));
}
verifyPing(await collect(await m.ping()));
verifyPing(await collect(await m.ping("test")));
verifyPing(await collect(await m.ping("test", srv.id)));
function verifyInfo(infos: ServiceInfo[]) {
verifyIdentity(infos);
const info = infos[0];
assertEquals(info.type, ServiceResponseType.INFO);
assertEquals(info.description, srv.description);
assertEquals(info.endpoints.length, srv.endpoints().length);
assertArrayIncludes(
info.endpoints.map((e) => {
return e.subject;
}),
srv.subjects,
);
const r = info as unknown as Record<string, unknown>;
delete r.type;
delete r.version;
delete r.name;
delete r.id;
delete r.description;
delete r.endpoints;
assertEquals(Object.keys(r).length, 0, JSON.stringify(r));
}
// info
verifyInfo(await collect(await m.info()));
verifyInfo(await collect(await m.info("test")));
verifyInfo(await collect(await m.info("test", srv.id)));
function verifyStats(stats: ServiceStats[]) {
verifyIdentity(stats);
const stat = stats[0];
assertEquals(stat.type, ServiceResponseType.STATS);
assert(Date.parse(stat.started) > 0);
const s = stat.endpoints?.[0]!;
assertEquals(s.num_requests, 2);
assertEquals(s.num_errors, 0);
assertEquals(typeof s.processing_time, "number");
assertEquals(typeof s.average_processing_time, "number");
// assert(Date.parse(stat.started) - Date.now() > 0, JSON.stringify(stat));
const r = stat as unknown as Record<string, unknown>;
delete r.type;
delete r.version;
delete r.name;
delete r.id;
delete r.started;
delete r.endpoints;
assertEquals(Object.keys(r).length, 0, JSON.stringify(r));
}
verifyStats(await collect(await m.stats()));
verifyStats(await collect(await m.stats("test")));
verifyStats(await collect(await m.stats("test", srv.id)));
await cleanup(ns, nc);
});
Deno.test("service - basics", async () => {
const { ns, nc } = await setup({}, {});
const svc = new Svcm(nc);
const conf: ServiceConfig = {
name: "test",
version: "0.0.0",
};
const srvA = await svc.add(conf) as ServiceImpl;
srvA.addEndpoint("foo", (_err: Error | null, msg: Msg) => {
msg?.respond();
});
const srvB = await svc.add(conf) as ServiceImpl;
srvB.addEndpoint("foo", (_err: Error | null, msg: Msg) => {
msg?.respond();
});
const m = svc.client();
const count = async (
p: Promise<QueuedIterator<unknown>>,
): Promise<number> => {
return (await collect(await p)).length;
};
assertEquals(await count(m.ping()), 2);
assertEquals(await count(m.ping("test")), 2);
assertEquals(await count(m.ping("test", srvA.id)), 1);
await assertRejects(
async () => {
await collect(await m.ping("test", "c"));
},
errors.NoRespondersError,
);
assertEquals(await count(m.info()), 2);
assertEquals(await count(m.info("test")), 2);
assertEquals(await count(m.info("test", srvB.id)), 1);
await assertRejects(
async () => {
await collect(await m.info("test", "c"));
},
errors.NoRespondersError,
);
assertEquals(await count(m.stats()), 2);
assertEquals(await count(m.stats("test")), 2);
assertEquals(await count(m.stats("test", srvB.id)), 1);
await assertRejects(
async () => {
await collect(await m.stats("test", "c"));
},
errors.NoRespondersError,
);
await srvA.stop();
await srvB.stop();
await cleanup(ns, nc);
});
Deno.test("service - stop error", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permissions: {
subscribe: {
deny: "fail",
},
},
}],
},
}, { user: "a", pass: "a" });
const svc = new Svcm(nc);
const service = await svc.add({
name: "test",
version: "2.0.0",
});
service.addEndpoint("fail", () => {
if (err) {
service.stop(err);
return;
}
fail("shouldn't have subscribed");
});
const err = await service.stopped as Error;
assertInstanceOf(err, errors.PermissionViolationError);
await cleanup(ns, nc);
});
Deno.test("service - start error", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "a",
password: "a",
permissions: {
subscribe: {
deny: "fail",
},
},
}],
},
}, { user: "a", pass: "a" });
const svc = new Svcm(nc);
const service = await svc.add({
name: "test",
version: "2.0.0",
});
service.addEndpoint("fail", (_err, msg) => {
msg?.respond();
});
const err = await service.stopped as Error;
assertInstanceOf(err, errors.PermissionViolationError);
await cleanup(ns, nc);
});
Deno.test("service - callback error", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "test",
version: "2.0.0",
});
srv.addEndpoint("fail", (err) => {
if (err === null) {
throw new Error("boom");
}
});
const m = await nc.request("fail");
assertEquals(m.headers?.get(ServiceErrorHeader), "boom");
assertEquals(m.headers?.get(ServiceErrorCodeHeader), "500");
await cleanup(ns, nc);
});
Deno.test("service - service error is headers", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "test",
version: "2.0.0",
});
srv.addEndpoint("fail", (): void => {
// tossing service error should have the code/description
throw new ServiceError(1210, "something");
});
const m = await nc.request("fail");
assertEquals(m.headers?.get(ServiceErrorHeader), "something");
assertEquals(m.headers?.get(ServiceErrorCodeHeader), "1210");
await cleanup(ns, nc);
});
Deno.test("service - sub stop", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const service = await svc.add({
name: "test",
version: "2.0.0",
});
service.addEndpoint("q", (_err, m) => {
m.respond();
});
const nci = nc as NatsConnectionImpl;
for (const s of nci.protocol.subscriptions.subs.values()) {
if (s.subject === "q") {
s.close();
break;
}
}
const err = await service.stopped as Error;
assertEquals(err.message, "required subscription q stopped");
await cleanup(ns, nc);
});
Deno.test("service - monitoring sub stop", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const service = await svc.add({
name: "test",
version: "2.0.0",
});
service.addEndpoint("q", (_err, m) => {
m.respond();
});
const nci = nc as NatsConnectionImpl;
for (const s of nci.protocol.subscriptions.subs.values()) {
if (s.subject === "$SRV.PING") {
s.close();
break;
}
}
const err = await service.stopped as Error;
assertEquals(err.message, "required subscription $SRV.PING stopped");
await cleanup(ns, nc);
});
Deno.test("service - custom stats handler", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "test",
version: "2.0.0",
statsHandler: (): Promise<unknown> => {
return Promise.resolve({ hello: "world" });
},
});
srv.addEndpoint("q", (_err, m) => {
m.respond();
});
const m = svc.client();
const stats = await collect(await m.stats());
assertEquals(stats.length, 1);
assertEquals(
(stats[0].endpoints?.[0].data as Record<string, unknown>).hello,
"world",
);
await cleanup(ns, nc);
});
Deno.test("service - bad stats handler", async () => {
const { ns, nc } = await setup();
const config = {
name: "test",
version: "2.0.0",
// @ts-ignore: test
statsHandler: "hello world",
};
const svc = new Svcm(nc);
const srv = await svc.add(config as unknown as ServiceConfig);
srv.addEndpoint("q", (_err, m) => {
m.respond();
});
const m = svc.client();
const stats = await collect(await m.stats());
assertEquals(stats.length, 1);
assertEquals(stats[0].endpoints?.[0].data, undefined);
await cleanup(ns, nc);
});
Deno.test("service - stats handler error", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "test",
version: "2.0.0",
statsHandler: (): Promise<unknown> => {
throw new Error("bad stats handler");
},
});
srv.addEndpoint("q", (_err, m) => {
m.respond();
});
const m = svc.client();
const stats = await collect(await m.stats());
assertEquals(stats.length, 1);
assertEquals(stats[0].endpoints?.length, 1);
const s = stats[0].endpoints?.[0]!;
assertEquals(s.data, undefined);
assertEquals(s.last_error, "bad stats handler");
assertEquals(s.num_errors, 1);
await cleanup(ns, nc);
});
Deno.test("service - reset", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const service = await svc.add({
name: "test",
version: "2.0.0",
}) as ServiceImpl;
service.addEndpoint("q", (_err, m) => {
m.respond();
});
await nc.request("q");
await nc.request("q");
service.handlers[0].stats.countError(new Error("hello"));
const m = svc.client();
let stats = await collect(await m.stats());
assertEquals(stats[0].endpoints?.length, 1);
assertEquals(stats.length, 1);
let stat = stats[0].endpoints?.[0]!;
assert(stat.processing_time >= 0);
assert(stat.average_processing_time >= 0);
assertEquals(stat.num_errors, 1);
assertEquals(stat.last_error, "hello");
service.reset();
stats = await collect(await m.stats());
assertEquals(stats.length, 1);
assertEquals(stats[0].endpoints?.length, 1);
stat = stats[0].endpoints?.[0]!;
assertEquals(stat.num_requests, 0);
assertEquals(stat.processing_time, 0);
assertEquals(stat.average_processing_time, 0);
assertEquals(stat.num_errors, 0);
assertEquals(stat.last_error, undefined);
await cleanup(ns, nc);
});
Deno.test("service - iter", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const service = await svc.add({
name: "test",
version: "2.0.0",
}) as ServiceImpl;
const iter = service.addEndpoint("q");
(async () => {
for await (const m of iter) {
await delay(500);
m.respond();
}
})().then();
await nc.request("q");
await nc.request("q");
service.handlers[0].stats.countError(new Error("hello"));
const m = svc.client();
let stats = await collect(await m.stats());
assertEquals(stats.length, 1);
assertEquals(stats[0].endpoints?.length, 1);
const stat = stats[0].endpoints?.[0]!;
assert(stat.processing_time >= 0);
assert(stat.average_processing_time >= 0);
assertEquals(stat.num_errors, 1);
assertEquals(stat.last_error, "hello");
service.reset();
stats = await collect(await m.stats());
assertEquals(stats.length, 1);
await cleanup(ns, nc);
});
Deno.test("service - iter closed", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const service = await svc.add({
name: "test",
version: "2.0.0",
});
const iter = service.addEndpoint("q");
(async () => {
for await (const m of iter) {
m.respond();
break;
}
})().then();
await nc.request("q");
const err = await service.stopped;
assertEquals(err, null);
await cleanup(ns, nc);
});
Deno.test("service - version must be semver", async () => {
const { ns, nc } = await setup();
const test = (v?: string): Promise<Service> => {
const svc = new Svcm(nc);
return svc.add({
name: "test",
version: v!,
});
};
await assertRejects(
async () => {
await test();
},
Error,
"'' is not a semver value",
);
await assertRejects(
async () => {
await test("a.b.c");
},
Error,
"'a.b.c' is not a semver value",
);
const srv = await test("v1.2.3-hello") as ServiceImpl;
const info = srv.info();
assertEquals(info.id, srv.id);
assertEquals(info.name, srv.name);
assertEquals(info.version, "v1.2.3-hello");
assertEquals(info.description, srv.description);
assertEquals(info.endpoints.length, 0);
await cleanup(ns, nc);
});
Deno.test("service - service errors", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "test",
version: "2.0.0",
});
const iter = srv.addEndpoint("q");
(async () => {
for await (const m of iter) {
m.data.length ? m.respond() : m.respondError(411, "data required");
}
})().then();
let r = await nc.request("q");
assertEquals(ServiceError.isServiceError(r), true);
const serr = ServiceError.toServiceError(r);
assertEquals(serr?.code, 411);
assertEquals(serr?.message, "data required");
r = await nc.request("q", new Uint8Array(1));
assertEquals(ServiceError.isServiceError(r), false);
assertEquals(ServiceError.toServiceError(r), null);
await cleanup(ns, nc);
});
// Deno.test("service - cross platform service test", async () => {
// const nc = await connect({ servers: "demo.nats.io" });
// const name = `echo_${nuid.next()}`;
//
// const conf: ServiceConfig = {
// name,
// version: "0.0.1",
// statsHandler: (): Promise<unknown> => {
// return Promise.resolve("hello world");
// },
// metadata: {
// service: name,
// },
// };
//
// const svc = new Svc(nc);
// const srv = await svc.add(conf);
// srv.addEndpoint("test", {
// subject: createInbox(),
// handler: (_err, m): void => {
// if (m.data.length === 0) {
// m.respondError(400, "need a string", JSON.stringify(""));
// } else {
// if (m.string() === "error") {
// throw new Error("service asked to throw an error");
// }
// m.respond(m.data);
// }
// },
// metadata: {
// endpoint: "a",
// },
// });
//
// // running from root?
// const scheck = Deno.cwd().endsWith("nats.js")
// ? "./services/tests/service-check.ts"
// : "./tests/service-check.ts";
//
// const args = [
// "run",
// "-A",
// scheck,
// "--name",
// name,
// "--server",
// "demo.nats.io",
// ];
//
// const cmd = new Deno.Command(Deno.execPath(), {
// args,
// stderr: "piped",
// stdout: "piped",
// });
// const { success, stderr, stdout } = await cmd.output();
//
// if (!success) {
// console.log(new TextDecoder().decode(stdout));
// console.log(new TextDecoder().decode(stderr));
// fail(new TextDecoder().decode(stderr));
// }
//
// await nc.close();
// });
Deno.test("service - stats name respects assigned name", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const test = await svc.add({
name: "tEsT",
// @ts-ignore: testing
version: "0.0.1",
});
test.addEndpoint("q", (_err, msg) => {
msg?.respond();
});
const stats = await test.stats();
assertEquals(stats.name, "tEsT");
const r = await nc.request(`$SRV.PING.tEsT`);
const si = r.json<ServiceIdentity>();
assertEquals(si.name, "tEsT");
await cleanup(ns, nc);
});
Deno.test("service - multiple endpoints", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const ms = await svc.add({
name: "multi",
version: "0.0.1",
});
ms.addEndpoint("hey", (_err, m) => {
m.respond("hi");
});
ms.addGroup("service").addEndpoint("echo", (_err, m) => {
m.respond(m.data);
});
let r = await nc.request(`hey`);
assertEquals(r.string(), "hi");
r = await nc.request(`service.echo`, "yo!");
assertEquals(r.string(), "yo!");
r = await nc.request(`$SRV.STATS`);
const stats = r.json<ServiceStats>();
function t(name: string) {
const v = stats.endpoints?.find((n) => {
return n.name === name;
});
assertExists(v);
assertEquals(v.num_requests, 1);
}
t("hey");
t("echo");
await cleanup(ns, nc);
});
Deno.test("service - multi cb/iterator", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
});
srv.addGroup("cb").addGroup("b").addGroup("c").addEndpoint(
"test",
(_err, msg) => {
msg?.respond();
},
);
await nc.request("cb.b.c.test");
const iter = srv.addGroup("iter.b.c").addEndpoint("test");
(async () => {
for await (const m of iter) {
m.respond();
}
})().then();
await nc.request("iter.b.c.test");
await cleanup(ns, nc);
});
Deno.test("service - group and endpoint names", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
});
const t = (group: string, endpoint: string, expect: string) => {
assertThrows(
() => {
srv.addGroup(group).addEndpoint(endpoint);
},
Error,
expect,
);
};
t("", "", "endpoint name required");
t("", "*", "endpoint name cannot contain '*'");
t("", ">", "endpoint name cannot contain '>'");
t("", " ", "endpoint name cannot contain ' '");
t("", "hello.world", "endpoint name cannot contain '.'");
t("a.>", "hello", "service group name cannot contain internal '>'");
t(">", "hello", "service group name cannot contain internal '>'");
await cleanup(ns, nc);
});
Deno.test("service - group subs", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
});
const t = (subject: string) => {
const sub = (nc as NatsConnectionImpl).protocol.subscriptions.all().find(
(s) => {
return s.subject === subject;
},
);
assertExists(sub);
};
srv.addGroup("").addEndpoint("root");
t("root");
srv.addGroup("a").addEndpoint("add");
t("a.add");
srv.addGroup("b").addEndpoint("add");
t("b.add");
srv.addGroup("one.*.three").addEndpoint("add");
t("one.*.three.add");
srv.addGroup("$SYS.SOMETHING.OR.OTHER").addEndpoint("wild", { subject: "*" });
t("$SYS.SOMETHING.OR.OTHER.*");
await cleanup(ns, nc);
});
Deno.test("service - metadata", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1" },
});
srv.addGroup("group").addEndpoint("endpoint", {
handler: (_err, msg) => {
msg.respond();
},
metadata: {
endpoint: "endpoint",
},
});
const info = srv.info();
assertEquals(info.metadata, { service: "1" });
const stats = await srv.stats();
assertEquals(stats.endpoints?.length, 1);
await cleanup(ns, nc);
});
Deno.test("service - schema metadata", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1" },
});
srv.addGroup("group").addEndpoint("endpoint", {
handler: (_err, msg) => {
msg.respond();
},
metadata: {
endpoint: "endpoint",
},
});
await cleanup(ns, nc);
});
Deno.test("service - json reviver", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1" },
});
srv.addGroup("group").addEndpoint("endpoint", {
handler: (_err, msg) => {
const d = msg.json<{ date: Date }>((k, v) => {
if (k === "date") {
return new Date(v);
}
return v;
});
assert(d.date instanceof Date);
msg.respond();
},
metadata: {
endpoint: "endpoint",
},
});
await nc.request("group.endpoint", JSON.stringify({ date: Date.now() }));
await cleanup(ns, nc);
});
async function testQueueName(nc: NatsConnection, subj: string, q?: string) {
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1" },
queue: q,
});
srv.addEndpoint(subj, {
handler: (_err, msg) => {
msg.respond();
},
});
const nci = nc as NatsConnectionImpl;
const sub = nci.protocol.subscriptions.all().find((s) => {
return s.subject === subj;
});
assertExists(sub);
assertEquals(sub.queue, q !== undefined ? q : "q");
}
Deno.test("service - custom queue group", async () => {
const { ns, nc } = await setup();
await testQueueName(nc, "a");
await testQueueName(nc, "b", "q1");
await assertRejects(
async () => {
await testQueueName(nc, "c", "one two");
},
Error,
"invalid queue name - queue name cannot contain ' '",
);
await assertRejects(
async () => {
await testQueueName(nc, "d", " ");
},
Error,
"invalid queue name - queue name cannot contain ' '",
);
await cleanup(ns, nc);
});
function getSubscriptionBySubject(
nc: NatsConnection,
subject: string,
): SubscriptionImpl | undefined {
const nci = nc as NatsConnectionImpl;
return nci.protocol.subscriptions.all().find((v) => {
return v.subject === subject;
});
}
function getEndpointInfo(
srv: ServiceImpl,
subject: string,
): EndpointInfo | undefined {
return srv.endpoints().find((v) => {
return v.subject === subject;
});
}
function checkQueueGroup(srv: Service, subj: string, queue: string) {
const service = srv as ServiceImpl;
const si = getSubscriptionBySubject(service.nc, subj);
assertExists(si);
assertEquals(si.queue, queue);
const ei = getEndpointInfo(service, subj);
assertExists(ei);
assertEquals(ei.queue_group, queue);
}
Deno.test("service - endpoint default queue group", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1" },
}) as ServiceImpl;
// svc config doesn't specify a queue group so we expect q
srv.addEndpoint("a");
checkQueueGroup(srv, "a", "q");
// we add another group, no queue
const dg = srv.addGroup("G");
dg.addEndpoint("a");
checkQueueGroup(srv, "G.a", "q");
// now a group with a queue - we expect endpoints/and subgroups
// to use this unless they override
const g = srv.addGroup("g", "qq");
g.addEndpoint("a");
checkQueueGroup(srv, "g.a", "qq");
// override
g.addEndpoint("b", { queue: "bb" });
checkQueueGroup(srv, "g.b", "bb");
// add a subgroup without, should inherit
const g2 = g.addGroup("g");
g2.addEndpoint("a");
checkQueueGroup(srv, "g.g.a", "qq");
// and override
g2.addEndpoint("b", { queue: "bb" });
checkQueueGroup(srv, "g.g.b", "bb");
await cleanup(ns, nc);
});
Deno.test("service - endpoint no queue group", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1" },
// no queue
queue: "",
}) as ServiceImpl;
// svc config doesn't specify a queue group so we expect q
srv.addEndpoint("a");
checkQueueGroup(srv, "a", "");
// we add another group, no queue
const dg = srv.addGroup("G");
dg.addEndpoint("a");
checkQueueGroup(srv, "G.a", "");
// the above have no queue, no override, and set a queue
const g = srv.addGroup("g", "qq");
g.addEndpoint("a");
checkQueueGroup(srv, "g.a", "qq");
// override
g.addEndpoint("b", { queue: "bb" });
checkQueueGroup(srv, "g.b", "bb");
// add a subgroup without, should inherit
const g2 = g.addGroup("g");
g2.addEndpoint("a");
checkQueueGroup(srv, "g.g.a", "qq");
await cleanup(ns, nc);
});
Deno.test("service - metadata is not editable", async () => {
const { ns, nc } = await setup();
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1", hello: "world" },
queue: "",
}) as ServiceImpl;
assertThrows(
() => {
srv.config.metadata!.hello = "hello";
},
TypeError,
"Cannot assign to read only property",
);
await cleanup(ns, nc);
});
Deno.test("service - close listener check", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
//@ts-ignore: internal
assertEquals(nci.closeListeners, undefined);
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1", hello: "world" },
queue: "",
});
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 1);
await srv.stop();
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 0);
await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1", hello: "world" },
queue: "",
});
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 1);
await nc.close();
//@ts-ignore: internal
assertEquals(nci.closeListeners.listeners.length, 0);
await cleanup(ns, nc);
});
Deno.test("service - stop should stop iterators", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
//@ts-ignore: internal
assertEquals(nci.closeListeners, undefined);
const svc = new Svcm(nc);
const srv = await svc.add({
name: "example",
version: "0.0.1",
metadata: { service: "1", hello: "world" },
queue: "",
});
const iter = srv.addEndpoint("hello");
const done = (async () => {
for await (const _ of iter) {
// nothing
}
})().then();
await srv.stop();
await deadline(done, 5_000);
await cleanup(ns, nc);
});
--- services/tests/service-check.ts ---
/*
* Copyright 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 { cli } from "@aricart/cobra";
import { connect } from "@nats-io/transport-deno";
import { collect, parseSemVer } from "@nats-io/nats-core/internal";
import type { NatsConnection } from "@nats-io/nats-core/internal";
import type { ServiceIdentity, ServiceInfo, ServiceStats } from "../src/mod.ts";
import {
ServiceError,
ServiceResponseType,
ServiceVerb,
Svcm,
} from "../src/mod.ts";
import type { ServiceClientImpl } from "../src/serviceclient.ts";
import type { JSONSchemaType, ValidateFunction } from "ajv";
import { Ajv } from "ajv";
const ajv = new Ajv();
const root = cli({
use: "service-check [--name name] [--server host:port]",
run: async (cmd, _args, flags): Promise<number> => {
const servers = [flags.value<string>("server")];
const name = flags.value<string>("name");
let error;
let nc: NatsConnection | null = null;
try {
nc = await connect({ servers });
await invoke(nc!, name);
await checkPing(nc!, name);
await checkInfo(nc!, name);
await checkStats(nc!, name);
} catch (err) {
cmd.stderr(err.message);
console.log(err);
error = err;
} finally {
await nc?.close();
}
return error ? Promise.resolve(1) : Promise.resolve(0);
},
});
root.addFlag({
short: "n",
name: "name",
type: "string",
usage: "service name to filter on",
default: "",
persistent: true,
required: true,
});
root.addFlag({
name: "server",
type: "string",
usage: "NATS server to connect to",
default: "localhost:4222",
persistent: true,
});
function filter<T extends ServiceIdentity>(name: string, responses: T[]): T[] {
return responses.filter((r) => {
return r.name === name;
});
}
function filterExpectingOnly<T extends ServiceIdentity>(
tag: string,
name: string,
responses: T[],
): T[] {
const filtered = filter(name, responses);
assertEquals(
filtered.length,
responses.length,
`expected ${tag} to have only services named ${name}`,
);
return filtered;
}
function checkResponse<T extends ServiceIdentity>(
tag: string,
responses: T[],
validator: ValidateFunction<T>,
) {
assert(responses.length > 0, `expected responses for ${tag}`);
responses.forEach((r) => {
const valid = validator(r);
if (!valid) {
console.log(r);
console.log(validator.errors);
throw new Error(tag + " " + validator.errors?.[0].message);
}
});
}
//
async function checkStats(nc: NatsConnection, name: string) {
const validateFn = ajv.compile(statsSchema);
await check<ServiceStats>(nc, ServiceVerb.STATS, name, validateFn, (v) => {
assertEquals(v.type, ServiceResponseType.STATS);
parseSemVer(v.version);
});
}
async function checkInfo(nc: NatsConnection, name: string) {
const validateFn = ajv.compile(infoSchema);
await check<ServiceInfo>(nc, ServiceVerb.INFO, name, validateFn, (v) => {
assertEquals(v.type, ServiceResponseType.INFO);
parseSemVer(v.version);
});
}
async function checkPing(nc: NatsConnection, name: string) {
const validateFn = ajv.compile(pingSchema);
await check<ServiceIdentity>(nc, ServiceVerb.PING, name, validateFn, (v) => {
assertEquals(v.type, ServiceResponseType.PING);
parseSemVer(v.version);
});
}
async function invoke(nc: NatsConnection, name: string): Promise<void> {
const svc = new Svcm(nc);
const sc = svc.client();
const infos = await collect(await sc.info(name));
let proms = infos.map((v) => {
return nc.request(v.endpoints[0].subject);
});
let responses = await Promise.all(proms);
responses.forEach((m) => {
assertEquals(
ServiceError.isServiceError(m),
true,
"expected service without payload to return error",
);
});
// the service should throw/register an error if "error" is specified as payload
proms = infos.map((v) => {
return nc.request(v.endpoints[0].subject, "error");
});
responses = await Promise.all(proms);
responses.forEach((m) => {
assertEquals(
ServiceError.isServiceError(m),
true,
"expected service without payload to return error",
);
});
proms = infos.map((v, idx) => {
return nc.request(
v.endpoints[0].subject,
`hello ${idx}`,
);
});
responses = await Promise.all(proms);
responses.forEach((m, idx) => {
const r = `hello ${idx}`;
assertEquals(
m.string(),
r,
`expected service response ${r}`,
);
});
}
async function check<T extends ServiceIdentity>(
nc: NatsConnection,
verb: ServiceVerb,
name: string,
validateFn: ValidateFunction<T>,
check?: (v: T) => void,
) {
const fn = (d: T[]): void => {
if (check) {
try {
d.forEach(check);
} catch (err) {
throw new Error(`${verb} check: ${err.message}`);
}
}
};
const svc = new Svcm(nc);
const sc = svc.client() as ServiceClientImpl;
// all
let responses = filter(
name,
await collect(await sc.q<T>(verb)),
);
assert(responses.length >= 1, `expected at least 1 response to ${verb}`);
fn(responses);
checkResponse(`${verb}()`, responses, validateFn);
// just matching name
responses = filterExpectingOnly(
`${verb}(${name})`,
name,
await collect(await sc.q<ServiceIdentity>(verb, name)),
);
assert(
responses.length >= 1,
`expected at least 1 response to ${verb}.${name}`,
);
fn(responses);
checkResponse(`${verb}(${name})`, responses, validateFn);
// specific service
responses = filterExpectingOnly(
`${verb}(${name})`,
name,
await collect(await sc.q<ServiceIdentity>(verb, name, responses[0].id)),
);
assert(
responses.length === 1,
`expected at least 1 response to ${verb}.${name}.${responses[0].id}`,
);
fn(responses);
checkResponse(`${verb}(${name})`, responses, validateFn);
}
function assert(v: unknown, msg?: string) {
if (typeof v === undefined || typeof v === null || v === false) {
throw new Error(msg || "expected value to be truthy");
}
}
function assertEquals(v: unknown, expected: unknown, msg?: string) {
if (v !== expected) {
throw new Error(msg || `expected ${v} === ${expected}`);
}
}
const statsSchema: JSONSchemaType<ServiceStats> = {
type: "object",
properties: {
type: { type: "string" },
name: { type: "string" },
id: { type: "string" },
version: { type: "string" },
started: { type: "string" },
metadata: {
type: "object",
minProperties: 1,
},
endpoints: {
type: "array",
items: {
type: "object",
properties: {
num_requests: { type: "number" },
num_errors: { type: "number" },
last_error: { type: "string" },
processing_time: { type: "number" },
average_processing_time: { type: "number" },
data: { type: "string" },
queue_group: { type: "string" },
},
required: [
"num_requests",
"num_errors",
"last_error",
"processing_time",
"average_processing_time",
"data",
"queue_group",
],
},
},
},
required: [
"type",
"name",
"id",
"version",
"started",
"metadata",
],
additionalProperties: false,
};
const infoSchema: JSONSchemaType<ServiceInfo> = {
type: "object",
properties: {
type: { type: "string" },
name: { type: "string" },
id: { type: "string" },
version: { type: "string" },
description: { type: "string" },
endpoints: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
subject: { type: "string" },
metadata: { type: "object", minProperties: 1 },
},
},
},
metadata: {
type: "object",
minProperties: 1,
},
},
required: ["type", "name", "id", "version", "metadata", "endpoints"],
additionalProperties: false,
};
const pingSchema: JSONSchemaType<ServiceIdentity> = {
type: "object",
properties: {
type: { type: "string" },
name: { type: "string" },
id: { type: "string" },
version: { type: "string" },
metadata: {
type: "object",
minProperties: 1,
},
},
required: ["type", "name", "id", "version", "metadata"],
additionalProperties: false,
};
Deno.exit(await root.execute(Deno.args));
--- services/README.md ---
[![License](https://img.shields.io/badge/Licence-Apache%202.0-blue.svg)](./LICENSE)
![services](https://github.com/nats-io/nats.js/actions/workflows/test.yml/badge.svg)
[![JSDoc](https://img.shields.io/badge/JSDoc-reference-blue)](https://nats-io.github.io/nats.js/services/index.html)
[![JSR](https://jsr.io/badges/@nats-io/services)](https://jsr.io/@nats-io/services)
[![JSR](https://jsr.io/badges/@nats-io/services/score)](https://jsr.io/@nats-io/services)
[![NPM Version](https://img.shields.io/npm/v/%40nats-io%2Fservices)](https://www.npmjs.com/package/@nats-io/services)
![NPM Downloads](https://img.shields.io/npm/dt/%40nats-io%2Fservices)
![NPM Downloads](https://img.shields.io/npm/dm/%40nats-io%2Fservices)
# Services
For a quick overview of the libraries and how to install them, see
[runtimes.md](../runtimes.md).
The Services module introduces a higher-level API for implementing services with
NATS. NATS has always been a strong technology on which to build services, as
they are easy to write, are location and DNS independent and can be scaled up or
down by simply adding or removing instances of the service.
The services module further streamlines NATS services development by providing
observability and standardization. The Service Framework allows your services to
be discovered, queried for status and schema information without additional
work.
To create services using the services module simply install this library and
create a `new Svc(nc)`.
## Creating a Service
```typescript
const svc = new Svc(nc);
const service = await svc.add({
name: "max",
version: "0.0.1",
description: "returns max number in a request",
});
// add an endpoint listening on "max"
const max = service.addEndpoint("max", (err, msg) => {
msg?.respond();
});
```
If you omit the handler, the service is actually an iterator for service
messages. To process messages incoming to the service:
```typescript
for await (const r of max) {
r.respond();
}
```
For those paying attention, this looks suspiciously like a regular subscription.
And it is. The only difference is that the service collects additional
_metadata_ that allows the service framework to provide some monitoring and
discovery for free.
To invoke the service, it is a simple NATS request:
```typescript
const response = await nc.request("max", JSON.stringify([1, 2, 3]));
```
## Discovery and Monitoring
When the service started, the framework automatically assigned it an unique
`ID`. The `name` and `ID` identify particular instance of the service. If you
start a second instance, that instance will also have the same `name` but will
sport a different `ID`.
To discover services that are running, create a monitoring client:
```typescript
const m = svc.client();
// you can ping, request info, and stats information.
// All the operations return iterators describing the services found.
for await (const s of await m.ping()) {
console.log(s.id);
}
await m.stats();
await m.info();
```
Additional filtering is possible, and they are valid for all the operations:
```typescript
// get the stats services that have the name "max"
await m.stats("max");
// or target a specific instance:
await m.stats("max", id);
```
For a more elaborate first example see:
[simple example here](examples/01_services.ts)
## Multiple Endpoints
More complex services will have more than one endpoint. For example a calculator
service may have endpoints for `sum`, `average`, `max`, etc. This type of
service is also possible with the service api.
You can create the service much like before. In this case, you don't need the
endpoint (yet!):
```typescript
const calc = await nc.services.add({
name: "calc",
version: "0.0.1",
description: "example calculator service",
});
```
One of the simplifications for service it that it helps build consistent subject
hierarchy for your services. To create a subject hierarchy, you add a _group_.
```typescript
const g = calc.addGroup("calc");
```
The group is a _prefix_ subject where you can add endpoints. The name can be
anything that is a valid subject prefix.
```typescript
const sums = g.addEndpoint("sum");
(async () => {
for await (const m of sums) {
// decode the message payload into an array of numbers
const numbers = m.json<number[]>();
// add them together
const s = numbers.reduce((sum, v) => {
return sum + v;
});
// return a number
m.respond(JSON.stringify(s));
}
})();
```
`addEndpoint()` takes a name, and an optional handler (it can also take a set of
options). The `name` must be a simple name. This means no dots, wildcards, etc.
`name` is then appended to the group where it is added, forming the subject
where the endpoint listens.
In the above case, the `sum` endpoint is listening for requests on `calc.sum`.
For those paying attention, you can specify a callback much like in the first
example, if you don't, the return value of the add endpoint is an iterator.
For a complete example see:
[multiple endpoints](examples/02_multiple_endpoints.ts)
--- transport-node/examples/bench.js ---
#!/usr/bin/env node
const parse = require("minimist");
const { Bench, Metric, connect, Nuid } = require("../lib/mod");
const process = require("node:process");
const defaults = {
s: "127.0.0.1:4222",
c: 100000,
p: 128,
subject: new Nuid().next(),
i: 1,
json: false,
csv: false,
csvheader: false,
pendingLimit: 32,
};
const argv = parse(
process.argv.slice(2),
{
alias: {
"s": ["server"],
"c": ["count"],
"d": ["debug"],
"p": ["payload"],
"i": ["iterations"],
},
default: defaults,
string: [
"subject",
],
boolean: [
"asyncRequests",
"callbacks",
"json",
"csv",
"csvheader",
],
},
);
if (argv.h || argv.help || (!argv.sub && !argv.pub && !argv.req && !argv.rep)) {
console.log(
"usage: bench.js [--json] [--callbacks] [--csv] [--csvheader] [--pub] [--sub] [--req (--asyncRequests)] [--count <#messages>=100000] [--payload <#bytes>=128] [--iterations <#loop>=1>] [--server server] [--subject <subj>]\n",
);
process.exit(0);
}
const server = argv.server;
const count = parseInt(argv.count);
const bytes = parseInt(argv.payload);
const iters = parseInt(argv.iterations);
const pendingLimit = parseInt(argv.pendingLimit) * 1024;
const metrics = [];
(async () => {
for (let i = 0; i < iters; i++) {
const nc = await connect(
{ servers: server, debug: argv.debug, noAsyncTraces: true },
);
nc.protocol.pendingLimit = pendingLimit;
const opts = {
msgs: count,
size: bytes,
asyncRequests: argv.asyncRequests,
callbacks: argv.callbacks,
pub: argv.pub,
sub: argv.sub,
req: argv.req,
rep: argv.rep,
subject: argv.subject,
};
const bench = new Bench(nc, opts);
const m = await bench.run();
metrics.push(...m);
await nc.close();
}
})().then(() => {
const reducer = (a, m) => {
if (a) {
a.name = m.name;
a.payload = m.payload;
a.bytes += m.bytes;
a.duration += m.duration;
a.msgs += m.msgs;
a.lang = m.lang;
a.version = m.version;
a.async = m.async;
a.max = Math.max(a.max === undefined ? 0 : a.max, m.duration);
a.min = Math.min(a.min === undefined ? m.duration : a.max, m.duration);
}
return a;
};
if (!argv.json && !argv.csv) {
const pubsub = metrics.filter((m) => m.name === "pubsub").reduce(
reducer,
new Metric("pubsub", 0),
);
const reqrep = metrics.filter((m) => m.name === "reqrep").reduce(
reducer,
new Metric("reqrep", 0),
);
const pub = metrics.filter((m) => m.name === "pub").reduce(
reducer,
new Metric("pub", 0),
);
const sub = metrics.filter((m) => m.name === "sub").reduce(
reducer,
new Metric("sub", 0),
);
const req = metrics.filter((m) => m.name === "req").reduce(
reducer,
new Metric("req", 0),
);
const rep = metrics.filter((m) => m.name === "rep").reduce(
reducer,
new Metric("rep", 0),
);
if (pubsub && pubsub.msgs) {
console.log(pubsub.toString());
}
if (reqrep && reqrep.msgs) {
console.log(reqrep.toString());
}
if (pub && pub.msgs) {
console.log(pub.toString());
}
if (sub && sub.msgs) {
console.log(sub.toString());
}
if (req && req.msgs) {
console.log(req.toString());
}
if (rep && rep.msgs) {
console.log(rep.toString());
}
} else if (argv.json) {
console.log(JSON.stringify(metrics, null, 2));
} else if (argv.csv) {
const lines = metrics.map((m) => {
return m.toCsv();
});
if (argv.csvheader) {
lines.unshift(Metric.header());
}
console.log(lines.join(""));
}
});
--- transport-node/examples/nats-events.js ---
#!/usr/bin/env node
const process = require("node:process");
const parse = require("minimist");
const { connect } = require("../index");
const argv = parse(
process.argv.slice(2),
{
alias: {
"s": ["server"],
},
default: {
s: "127.0.0.1:4222",
},
},
);
const opts = { servers: argv.s, maxReconnectAttempts: -1 };
(async () => {
let nc;
try {
nc = await connect(opts);
} catch (err) {
console.log(`error connecting to nats: ${err.message}`);
return;
}
console.info(`connected ${nc.getServer()}`);
let counter = 0;
(async () => {
for await (const s of nc.status()) {
counter++;
console.info(`${counter} ${s.type}: ${JSON.stringify(s.data)}`);
}
})().then();
await nc.closed()
.then((err) => {
if (err) {
console.error(`closed with an error: ${err.message}`);
}
});
})();
--- transport-node/examples/nats-pub.js ---
#!/usr/bin/env node
const parse = require("minimist");
const { connect, headers, credsAuthenticator, delay } = require(
"../index",
);
const fs = require("node:fs");
const process = require("node:process");
const argv = parse(
process.argv.slice(2),
{
alias: {
"s": ["server"],
"c": ["count"],
"i": ["interval"],
"f": ["creds"],
},
default: {
s: "127.0.0.1:4222",
c: 1,
i: 0,
},
boolean: true,
string: ["server", "count", "interval", "headers", "creds"],
},
);
const opts = { servers: argv.s };
const subject = String(argv._[0]);
const payload = argv._[1] || "";
const count = (argv.c === -1 ? Number.MAX_SAFE_INTEGER : argv.c) || 1;
const interval = argv.i || 0;
if (argv.debug) {
opts.debug = true;
}
if (argv.creds) {
const data = fs.readFileSync(argv.creds);
opts.authenticator = credsAuthenticator(data);
}
if (argv.h || argv.help || !subject) {
console.log(
"Usage: nats-pub [-s server] [--creds=/path/file.creds] [-c <count>=1] [-i <interval>=0] [--headers='k=v;k2=v2'] subject [msg]",
);
console.log("to publish forever, specify -c=-1 or --count=-1");
process.exit(1);
}
(async () => {
let nc;
try {
nc = await connect(opts);
} catch (err) {
console.log(`error connecting to nats: ${err.message}`);
return;
}
console.info(`connected ${nc.getServer()}`);
nc.closed()
.then((err) => {
if (err) {
console.error(`closed with an error: ${err.message}`);
}
});
const pubopts = {};
if (argv.headers) {
const hdrs = headers();
argv.headers.split(";").map((l) => {
const [k, v] = l.split("=");
hdrs.append(k, v);
});
pubopts.headers = hdrs;
}
for (let i = 1; i <= count; i++) {
nc.publish(subject, payload, pubopts);
console.log(`[${i}] ${subject}: ${payload}`);
if (interval) {
await delay(interval);
}
}
await nc.flush();
await nc.close();
})();
--- transport-node/examples/nats-rep.js ---
#!/usr/bin/env node
const parse = require("minimist");
const { connect, headers, credsAuthenticator } = require(
"../index",
);
const fs = require("node:fs");
const process = require("node:process");
const argv = parse(
process.argv.slice(2),
{
alias: {
"s": ["server"],
"q": ["queue"],
"e": ["echo"],
"f": ["creds"],
},
default: {
s: "127.0.0.1:4222",
q: "",
},
boolean: ["echo", "headers", "debug"],
string: ["server", "queue", "creds"],
},
);
const opts = { servers: argv.s };
const subject = argv._[0] ? String(argv._[0]) : "";
const payload = argv._[1] || "";
if (argv.debug) {
opts.debug = true;
}
if (argv.creds) {
const data = fs.readFileSync(argv.creds);
opts.authenticator = credsAuthenticator(data);
}
if (argv.h || argv.help || !subject || (argv._[1] && argv.q)) {
console.log(
"Usage: nats-rep [-s server] [--creds=/path/file.creds] [-q queue] [--headers='k=v;k2=v2'] [-e echo_payload] subject [payload]",
);
process.exit(1);
}
(async () => {
let nc;
try {
nc = await connect(opts);
} catch (err) {
console.log(`error connecting to nats: ${err.message}`);
return;
}
console.info(`connected ${nc.getServer()}`);
nc.closed()
.then((err) => {
if (err) {
console.error(`closed with an error: ${err.message}`);
}
});
const sub = nc.subscribe(subject, { queue: argv.q });
console.info(`${argv.q !== "" ? "queue " : ""}listening to ${subject}`);
for await (const m of sub) {
const hdrs = argv.headers ? (argv.e ? m.headers : headers()) : undefined;
if (hdrs) {
hdrs.set("sequence", sub.getProcessed().toString());
hdrs.set("time", Date.now().toString());
}
if (m.respond(argv.e ? m.data : payload, { headers: hdrs })) {
console.log(
`[${sub.getProcessed()}]: ${m.subject} ${m.reply}: ${m.data}`,
);
} else {
console.log(
`[${sub.getProcessed()}]: ${m.subject} ignored - no reply subject`,
);
}
}
})();
--- transport-node/examples/nats-req.js ---
#!/usr/bin/env node
const parse = require("minimist");
const { connect, headers, credsAuthenticator, delay } = require(
"../index",
);
const fs = require("node:fs");
const process = require("node:process");
const argv = parse(
process.argv.slice(2),
{
alias: {
"s": ["server"],
"c": ["count"],
"i": ["interval"],
"t": ["timeout"],
"f": ["creds"],
},
default: {
s: "127.0.0.1:4222",
c: 1,
i: 0,
t: 1000,
},
boolean: true,
string: ["server", "count", "interval", "headers", "creds"],
},
);
const opts = { servers: argv.s };
const subject = String(argv._[0]);
const payload = String(argv._[1]) || "";
const count = (argv.c == -1 ? Number.MAX_SAFE_INTEGER : argv.c) || 1;
const interval = argv.i;
if (argv.debug) {
opts.debug = true;
}
if (argv.creds) {
const data = fs.readFileSync(argv.creds);
opts.authenticator = credsAuthenticator(data);
}
if (argv.h || argv.help || !subject) {
console.log(
"Usage: nats-pub [-s server] [--creds=/path/file.creds] [-c <count>=1] [-t <timeout>=1000] [-i <interval>=0] [--headers='k=v;k2=v2' subject [msg]",
);
console.log("to request forever, specify -c=-1 or --count=-1");
process.exit(1);
}
(async () => {
let nc;
try {
nc = await connect(opts);
} catch (err) {
console.log(`error connecting to nats: ${err.message}`);
return;
}
console.info(`connected ${nc.getServer()}`);
nc.closed()
.then((err) => {
if (err) {
console.error(`closed with an error: ${err.message}`);
}
});
const hdrs = argv.headers ? headers() : undefined;
if (hdrs) {
argv.headers.split(";").map((l) => {
const [k, v] = l.split("=");
hdrs.append(k, v);
});
}
for (let i = 1; i <= count; i++) {
await nc.request(
subject,
payload,
{ timeout: argv.t, headers: hdrs },
)
.then((m) => {
console.log(`[${i}]: ${m.string()}`);
if (argv.headers && m.headers) {
const h = [];
for (const [key, value] of m.headers) {
h.push(`${key}=${value}`);
}
console.log(`\t${h.join(";")}`);
}
})
.catch((err) => {
console.log(`[${i}]: request failed: ${err.message}`);
});
if (interval) {
await delay(interval);
}
}
await nc.flush();
await nc.close();
})();
--- transport-node/examples/nats-sub.js ---
#!/usr/bin/env node
const parse = require("minimist");
const { connect, credsAuthenticator } = require("../index");
const fs = require("node:fs");
const process = require("node:process");
const argv = parse(
process.argv.slice(2),
{
alias: {
"s": ["server"],
"q": ["queue"],
"f": ["creds"],
},
default: {
s: "127.0.0.1:4222",
q: "",
},
boolean: ["headers", "debug"],
string: ["server", "queue", "creds"],
},
);
const opts = { servers: argv.s };
const subject = argv._[0] ? String(argv._[0]) : ">";
if (argv.debug) {
opts.debug = true;
}
if (argv.creds) {
const data = fs.readFileSync(argv.creds);
opts.authenticator = credsAuthenticator(data);
}
if (argv.h || argv.help || !subject) {
console.log(
"Usage: nats-sub [-s server] [--creds=/path/file.creds] [-q queue] [--headers] subject",
);
process.exit(1);
}
(async () => {
let nc;
try {
nc = await connect(opts);
} catch (err) {
console.log(`error connecting to nats: ${err.message}`);
return;
}
console.info(`connected ${nc.getServer()}`);
nc.closed()
.then((err) => {
if (err) {
console.error(`closed with an error: ${err.message}`);
}
});
const sub = nc.subscribe(subject, { queue: argv.q });
console.info(`${argv.q !== "" ? "queue " : ""}listening to ${subject}`);
for await (const m of sub) {
console.log(`[${sub.getProcessed()}]: ${m.subject}: ${m.string()}`);
if (argv.headers && m.headers) {
const h = [];
for (const [key, value] of m.headers) {
h.push(`${key}=${value}`);
}
console.log(`\t${h.join(";")}`);
}
}
})();
--- transport-node/examples/util.js ---
/*
* Copyright 2024 Synadia Communications, Inc
* 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.
*/
exports.Performance = class Performance {
timers;
measures;
constructor() {
this.timers = new Map();
this.measures = new Map();
}
mark(key) {
this.timers.set(key, Date.now());
}
measure(key, startKey, endKey) {
const s = this.timers.get(startKey);
if (s === undefined) {
throw new Error(`${startKey} is not defined`);
}
const e = this.timers.get(endKey);
if (e === undefined) {
throw new Error(`${endKey} is not defined`);
}
this.measures.set(key, e - s);
}
getEntries() {
const values = [];
this.measures.forEach((v, k) => {
values.push({ name: k, duration: v });
});
return values;
}
};
--- transport-node/tests/basics_test.js ---
/*
* Copyright 2018-2024 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.
*/
const { describe, it } = require("node:test");
const assert = require("node:assert").strict;
const {
connect,
errors,
createInbox,
} = require(
"../lib/mod",
);
const {
VERSION,
} = require("../lib/node_transport");
const { Lock } = require("./helpers/lock");
const { NatsServer } = require("./helpers/launcher");
const { jetstreamServerConf } = require("./helpers/jsutil.js");
const u = "demo.nats.io:4222";
describe(
"basics",
{ timeout: 20_000, concurrency: true, forceExit: true },
() => {
it("basics - reported version", () => {
const pkg = require("../package.json");
assert.equal(VERSION, pkg.version);
});
it("basics - connect default", async () => {
const ns = await NatsServer.start({ port: 4222 });
const nc = await connect();
await nc.flush();
await nc.close();
await ns.stop();
});
it("basics - tls connect", async () => {
const nc = await connect({ servers: ["demo.nats.io"] });
assert.equal(nc.protocol.transport?.isEncrypted(), true);
await nc.flush();
await nc.close();
});
it("basics - connect host", async () => {
const nc = await connect({ servers: "demo.nats.io" });
await nc.flush();
await nc.close();
});
it("basics - connect hostport", async () => {
const nc = await connect({ servers: "demo.nats.io:4222" });
await nc.flush();
await nc.close();
});
it("basics - connect servers", async () => {
const nc = await connect({ servers: ["demo.nats.io"] });
await nc.flush();
await nc.close();
});
it("basics - fail connect", async () => {
await assert.rejects(
() => {
return connect({ servers: "127.0.0.1:32001" });
},
errors.ConnectionError,
"connection refused",
);
});
it("basics - pubsub", async () => {
const subj = createInbox();
const nc = await connect({ servers: u });
const sub = nc.subscribe(subj);
const iter = (async () => {
for await (const _ of sub) {
break;
}
})();
nc.publish(subj);
await iter;
assert.equal(sub.getProcessed(), 1);
await nc.close();
});
it("basics - request", async () => {
const nc = await connect({ servers: u });
const s = createInbox();
const sub = nc.subscribe(s);
const _ = (async () => {
for await (const m of sub) {
m.respond("foo");
}
})();
const msg = await nc.request(s);
await nc.close();
assert.equal(msg.string(), "foo");
});
it("basics - socket error", async () => {
const ns = await NatsServer.start(jetstreamServerConf());
const nc = await connect({ port: ns.port, reconnect: false });
const closed = nc.closed();
nc.protocol.transport.socket.emit(
"error",
new Error("something bad happened"),
);
nc.protocol.transport.socket.emit("close");
const err = await closed;
assert.equal(err?.message, "something bad happened");
ns.stop();
});
it("basics - server gone", async () => {
const ns = await NatsServer.start(jetstreamServerConf());
const nc = await connect({
port: ns.port,
maxReconnectAttempts: 3,
reconnectTimeWait: 100,
});
const closed = nc.closed();
await ns.stop();
const err = await closed;
assert.ok(err instanceof errors.ConnectionError);
assert.equal(err.message, "connection refused");
});
it("basics - server error", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port, reconnect: false });
setTimeout(() => {
nc.protocol.sendCommand("X\r\n");
});
const err = await nc.closed();
assert(err instanceof errors.ProtocolError);
await ns.stop();
});
it("basics - disconnect reconnects", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port });
const lock = new Lock(1);
const status = nc.status();
(async () => {
for await (const s of status) {
switch (s.type) {
case "reconnect":
lock.unlock();
break;
default:
}
}
})().then();
nc.protocol.transport.disconnect();
await lock;
await nc.close();
await ns.stop();
});
},
it("basics - ws urls fail", async () => {
await assert.rejects(
() => {
return connect({ servers: ["ws://localhost:4222"] });
},
Error,
"'servers' deno client doesn't support websockets, use the 'wsconnect' function instead",
);
await assert.rejects(
() => {
return connect({ servers: "ws://localhost:4222" });
},
Error,
"'servers' deno client doesn't support websockets, use the 'wsconnect' function instead",
);
await assert.rejects(
() => {
return connect({ servers: ["wss://localhost:4222"] });
},
Error,
"'servers' deno client doesn't support websockets, use the 'wsconnect' function instead",
);
await assert.rejects(
() => {
return connect({ servers: "wss://localhost:4222" });
},
Error,
"'servers' deno client doesn't support websockets, use the 'wsconnect' function instead",
);
}),
);
--- transport-node/tests/jetstream_test.js ---
/*
* Copyright 2018-2024 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.
*/
const { describe, it } = require("node:test");
const assert = require("node:assert").strict;
const {
connect,
} = require(
"../index",
);
const { jetstream, jetstreamManager } = require(
"@nats-io/jetstream",
);
const { Kvm } = require("@nats-io/kv");
const { Objm } = require("@nats-io/obj");
describe(
"jetstream",
{ timeout: 20_000, concurrency: true, forceExit: true },
() => {
it("jetstream is a function", async () => {
assert.equal(typeof jetstream, "function");
const nc = await connect({ servers: "demo.nats.io" });
const js = jetstream(nc);
assert.ok(js);
await nc.close();
});
it("jetstreamManager is a function", async () => {
assert.equal(typeof jetstreamManager, "function");
const nc = await connect({ servers: "demo.nats.io" });
const jsm = await jetstreamManager(nc);
await jsm.getAccountInfo();
await nc.close();
});
it("kvm is a function", async () => {
assert.equal(typeof Kvm, "function");
const nc = await connect({ servers: "demo.nats.io" });
const kvm = new Kvm(nc);
let c = 0;
for await (const _ of kvm.list()) {
c++;
}
assert.ok(c >= 0);
await nc.close();
});
it("objm is a function", async () => {
assert.equal(typeof Objm, "function");
const nc = await connect({ servers: "demo.nats.io" });
const objm = new Objm(nc);
let c = 0;
for await (const _ of objm.list()) {
c++;
}
assert.ok(c >= 0);
await nc.close();
});
},
);
--- transport-node/tests/noiptls_test.js ---
/*
* Copyright 2020-2024 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.
*/
const { describe, it } = require("node:test");
const {
connect,
} = require(
"../index",
);
const process = require("node:process");
const { resolve, join } = require("node:path");
const { Lock } = require("./helpers/lock");
const { NatsServer } = require("./helpers/launcher");
const dir = process.cwd();
const tlsConfig = {
trace: true,
tls: {
ca_file: resolve(join(dir, "/tests/certs/ca.pem")),
cert_file: resolve(join(dir, "/tests/certs/server_noip.pem")),
key_file: resolve(join(dir, "/tests/certs/key_noip.pem")),
},
};
describe("tls", { timeout: 20_000, concurrency: true, forceExit: true }, () => {
it("reconnect via tls by ip", async () => {
if (process.env.CI) {
console.log("skipped test");
return;
}
const servers = await NatsServer.startCluster(3, tlsConfig);
const nc = await connect(
{
port: servers[0].port,
reconnectTimeWait: 250,
tls: {
caFile: resolve(join(dir, "/tests/certs/ca.pem")),
},
},
);
await nc.flush();
const lock = Lock();
const iter = nc.status();
(async () => {
for await (const e of iter) {
if (e.type === "reconnect") {
lock.unlock();
}
}
})().then();
await servers[0].stop();
await lock;
await nc.close();
await NatsServer.stopAll(servers);
});
});
--- transport-node/tests/reconnect_test.js ---
/*
* Copyright 2018-2024 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.
*/
const { describe, it } = require("node:test");
const assert = require("node:assert").strict;
const {
connect,
wsconnect,
} = require("../index");
const { NatsServer } = require("./helpers/launcher");
const {
createInbox,
deferred,
delay,
} = require("@nats-io/nats-core/internal");
const { Lock } = require("./helpers/lock");
describe("websocket reconnect", { timeout: 20_000, forceExit: true }, () => {
it("reconnect websocket - disconnect reconnects", async () => {
let srv = await NatsServer.start({
logfile: "/tmp/nats-server.log",
websocket: {
port: -1,
no_tls: true,
},
});
const d = deferred();
const nc = await wsconnect({
debug: true,
servers: [`ws://127.0.0.1:${srv.websocket}`],
});
const port = srv.websocket;
(async () => {
for await (const e of nc.status()) {
if (e.type === "reconnect") {
d.resolve();
}
}
})();
await delay(1000);
await srv.stop();
srv = await NatsServer.start({
websocket: {
port,
no_tls: true,
},
});
await d;
await nc.close();
await srv.stop();
});
});
describe(
"reconnect",
{ timeout: 20_000, concurrency: true, forceExit: true },
() => {
it("reconnect - should receive when some servers are invalid", async () => {
const lock = Lock(1);
const servers = ["127.0.0.1:7", "demo.nats.io:4222"];
const nc = await connect({ servers: servers, noRandomize: true });
const subj = createInbox();
await nc.subscribe(subj, {
callback: () => {
lock.unlock();
},
});
nc.publish(subj);
await lock;
await nc.close();
const a = nc.protocol.servers.getServers();
assert.equal(a.length, 1);
assert.ok(a[0].didConnect);
});
it("reconnect - events", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
waitOnFirstConnect: true,
reconnectTimeWait: 100,
maxReconnectAttempts: 10,
});
let disconnects = 0;
let reconnecting = 0;
const status = nc.status();
(async () => {
for await (const e of status) {
switch (e.type) {
case "disconnect":
disconnects++;
break;
case "reconnecting":
reconnecting++;
break;
default:
t.log(e);
}
}
})().then();
await srv.stop();
try {
await nc.closed();
} catch (err) {
assert.equal(err.message, "connection refused");
}
assert.equal(disconnects, 1, "disconnects");
assert.equal(reconnecting, 10, "reconnecting");
});
it("reconnect - reconnect not emitted if suppressed", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
reconnect: false,
});
let disconnects = 0;
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "disconnect":
disconnects++;
break;
case "reconnecting":
t.fail("shouldn't have emitted reconnecting");
break;
}
}
})().then();
await srv.stop();
await nc.closed();
});
it("reconnect - reconnecting after proper delay", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
reconnectTimeWait: 500,
maxReconnectAttempts: 1,
});
const serverLastConnect =
nc.protocol.servers.getCurrentServer().lastConnect;
const dt = deferred();
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "reconnecting":
dt.resolve(Date.now() - serverLastConnect);
break;
}
}
})();
await srv.stop();
const elapsed = await dt;
assert.ok(elapsed >= 500 && elapsed <= 700, `elapsed was ${elapsed}`);
await nc.closed();
});
it("reconnect - indefinite reconnects", async () => {
let srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
reconnectTimeWait: 100,
maxReconnectAttempts: -1,
});
let disconnects = 0;
let reconnects = 0;
let reconnect = false;
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "disconnect":
disconnects++;
break;
case "reconnect":
reconnect = true;
nc.close();
break;
case "reconnecting":
reconnects++;
break;
}
}
})().then();
await srv.stop();
const lock = Lock(1);
setTimeout(async () => {
srv = await srv.restart();
lock.unlock();
}, 1000);
await nc.closed();
await srv.stop();
await lock;
await srv.stop();
assert.ok(reconnects > 5);
assert.ok(reconnect);
assert.equal(disconnects, 1);
});
it("reconnect - jitter", async () => {
const srv = await NatsServer.start();
let called = false;
const h = () => {
called = true;
return 15;
};
const dc = await connect({
port: srv.port,
reconnect: false,
});
const hasDefaultFn =
typeof dc.options.reconnectDelayHandler === "function";
const nc = await connect({
port: srv.port,
maxReconnectAttempts: 1,
reconnectDelayHandler: h,
});
await srv.stop();
await nc.closed();
await dc.closed();
assert.ok(called);
assert.ok(hasDefaultFn);
});
it("reconnect - internal disconnect forces reconnect", async () => {
const srv = await NatsServer.start();
const nc = await connect({
port: srv.port,
reconnect: true,
reconnectTimeWait: 200,
});
let stale = false;
let disconnect = false;
const lock = Lock();
(async () => {
for await (const e of nc.status()) {
switch (e.type) {
case "staleConnection":
stale = true;
break;
case "disconnect":
disconnect = true;
break;
case "reconnect":
lock.unlock();
break;
}
}
})().then();
nc.protocol.disconnect();
await lock;
assert.ok(disconnect, "disconnect");
assert.ok(stale, "stale");
await nc.close();
await srv.stop();
});
},
);
--- transport-node/tests/tls_test.js ---
/*
* Copyright 2020-2024 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.
*/
const { describe, it } = require("node:test");
const assert = require("node:assert").strict;
const {
connect,
} = require(
"../index",
);
const process = require("node:process");
const { resolve, join } = require("node:path");
const { readFileSync } = require("node:fs");
const { Lock } = require("./helpers/lock");
const { NatsServer } = require("./helpers/launcher");
const { buildAuthenticator, extend, Connect } = require(
"@nats-io/nats-core/internal",
);
const dir = process.cwd();
const tlsConfig = {
host: "0.0.0.0",
tls: {
cert_file: resolve(join(dir, "./tests/certs/server.pem")),
key_file: resolve(join(dir, "./tests/certs/key.pem")),
ca_file: resolve(join(dir, "./tests/certs/ca.pem")),
},
};
describe("tls", { timeout: 20_000, concurrency: true, forceExit: true }, () => {
it("fail if server doesn't support TLS", async () => {
const ns = await NatsServer.start({ host: "0.0.0.0" });
const lock = Lock();
await connect({ servers: `localhost:${ns.port}`, tls: {} })
.then(() => {
assert.fail("shouldn't have connected");
})
.catch((err) => {
assert.equal(err.message, "server does not support 'tls'");
lock.unlock();
});
await lock;
await ns.stop();
});
it("connects to tls without option", async () => {
const nc = await connect({ servers: "demo.nats.io:4443" });
await nc.flush();
await nc.close();
});
it("custom ca fails without proper ca", async () => {
const ns = await NatsServer.start(tlsConfig);
const lock = Lock();
await connect({ servers: `localhost:${ns.port}`, maxReconnectAttempts: 4 })
.then(() => {
assert.fail("shouldn't have connected without client ca");
})
.catch(() => {
// this throws a totally un-useful connection reset.
lock.unlock();
});
await lock;
await ns.stop();
});
it("connects with proper ca", async () => {
const ns = await NatsServer.start(tlsConfig);
const nc = await connect({
servers: `localhost:${ns.port}`,
tls: {
caFile: tlsConfig.tls.ca_file,
},
});
await nc.flush();
assert.ok(nc.protocol.transport.socket.authorized);
await nc.close();
await ns.stop();
});
it("connects with rejectUnauthorized is honored", async () => {
const ns = await NatsServer.start(tlsConfig);
const nc = await connect({
servers: `localhost:${ns.port}`,
tls: {
rejectUnauthorized: false,
},
});
await nc.flush();
assert.ok(!nc.protocol.transport.socket.authorized);
await nc.close();
await ns.stop();
});
it("client auth", async () => {
const ns = await NatsServer.start(tlsConfig);
const certs = {
keyFile: resolve(join(dir, "./tests/certs/client-key.pem")),
certFile: resolve(join(dir, "./tests/certs/client-cert.pem")),
caFile: resolve(join(dir, "./tests/certs/ca.pem")),
};
const nc = await connect({
port: ns.port,
tls: certs,
});
await nc.flush();
await nc.close();
await ns.stop();
});
it("client auth direct", async () => {
const ns = await NatsServer.start(tlsConfig);
const certs = {
key: readFileSync(resolve(join(dir, "./tests/certs/client-key.pem"))),
cert: readFileSync(resolve(join(dir, "./tests/certs/client-cert.pem"))),
ca: readFileSync(resolve(join(dir, "./tests/certs/ca.pem"))),
};
const nc = await connect({
port: ns.port,
tls: certs,
});
await nc.flush();
await nc.close();
await ns.stop();
});
it("bad file paths", async () => {
const ns = await NatsServer.start(tlsConfig);
const certs = {
keyFile: "./tests/certs/client-key.pem",
certFile: "./x/certs/client-cert.pem",
caFile: "./tests/certs/ca.pem",
};
try {
await connect({
port: ns.port,
tls: certs,
});
assert.fail("should have not connected");
} catch (err) {
assert.ok(
err.message.indexOf("/x/certs/client-cert.pem doesn't exist") > -1,
);
}
await ns.stop();
});
it("shouldn't leak tls config", () => {
const tlsOptions = {
keyFile: resolve(join(dir, "./tests/certs/client-key.pem")),
certFile: resolve(join(dir, "./tests/certs/client-cert.pem")),
caFile: resolve(join(dir, "./tests/certs/ca.pem")),
};
let opts = { tls: tlsOptions, cert: "another" };
const auth = buildAuthenticator(opts);
opts = extend(opts, auth);
const c = new Connect({ version: "1.2.3", lang: "test" }, opts);
const cc = JSON.parse(JSON.stringify(c));
assert.equal(cc.tls_required, true);
assert.equal(cc.cert, undefined);
assert.equal(cc.keyFile, undefined);
assert.equal(cc.certFile, undefined);
assert.equal(cc.caFile, undefined);
assert.equal(cc.tls, undefined);
});
async function tlsInvalidCertMacro(conf, _tlsCode, re) {
const ns = await NatsServer.start(tlsConfig);
await assert.rejects(() => {
return connect({ servers: `localhost:${ns.port}`, tls: conf });
}, (err) => {
assert.ok(re.exec(err.message));
return true;
});
await ns.stop();
}
it(
"invalid cert",
() => {
return tlsInvalidCertMacro(
{
keyFile: resolve(join(dir, "./tests/certs/client-key.pem")),
certFile: resolve(join(dir, "./tests/certs/ca.pem")),
caFile: resolve(join(dir, "./tests/certs/server.pem")),
},
"ERR_OSSL_X509_KEY_VALUES_MISMATCH",
/key values mismatch/i,
);
},
);
it(
"invalid pem no start",
() => {
return tlsInvalidCertMacro(
{
keyFile: resolve(join(dir, "./tests/certs/client-cert.pem")),
certFile: resolve(join(dir, "./tests/certs/client-key.pem")),
caFile: resolve(join(dir, "./tests/certs/ca.pem")),
},
"ERR_OSSL_PEM_NO_START_LINE",
/no start line/i,
);
},
);
async function tlsInvalidArgPathMacro(conf, arg) {
const ns = await NatsServer.start(tlsConfig);
try {
await connect({ servers: `localhost:${ns.port}`, tls: conf });
assert.fail("shouldn't have connected");
} catch (err) {
const v = conf[arg];
assert.equal(err.message, `${v} doesn't exist`);
}
await ns.stop();
}
it("invalid key file", () => {
return tlsInvalidArgPathMacro({
keyFile: resolve(join(dir, "./tests/certs/client.ky")),
}, "keyFile");
});
it("invalid cert file", () => {
return tlsInvalidArgPathMacro({
certFile: resolve(join(dir, "./tests/certs/client.cert")),
}, "certFile");
});
it("invalid ca file", () => {
return tlsInvalidArgPathMacro({
caFile: resolve(join(dir, "./tests/certs/ca.cert")),
}, "caFile");
});
it("available connects with or without", async () => {
const conf = Object.assign({}, { allow_non_tls: true }, tlsConfig);
const ns = await NatsServer.start(conf);
// test will fail to connect because the certificate is
// not trusted, but the upgrade process was attempted.
try {
await connect({
servers: `localhost:${ns.port}`,
});
assert.fail("shouldn't have connected");
} catch (err) {
assert.ok(
err.message.indexOf("unable to verify the first certificate") > -1,
);
}
// will upgrade to tls as tls is required
const a = connect({
servers: `localhost:${ns.port}`,
tls: {
caFile: resolve(join(dir, "./tests/certs/ca.pem")),
},
});
// will NOT upgrade to tls
const b = connect({
servers: `localhost:${ns.port}`,
tls: null,
});
const conns = await Promise.all([a, b]);
await conns[0].flush();
await conns[1].flush();
assert.equal(conns[0].protocol.transport.isEncrypted(), true);
assert.equal(conns[1].protocol.transport.isEncrypted(), false);
await ns.stop();
});
it("tls first", async () => {
const ns = await NatsServer.start({
host: "0.0.0.0",
tls: {
handshake_first: true,
cert_file: resolve(join(dir, "./tests/certs/server.pem")),
key_file: resolve(join(dir, "./tests/certs/key.pem")),
ca_file: resolve(join(dir, "./tests/certs/ca.pem")),
},
});
const nc = await connect({
port: ns.port,
tls: {
handshakeFirst: true,
ca: readFileSync(resolve(join(dir, "./tests/certs/ca.pem"))),
},
});
await nc.flush();
await nc.close();
await ns.stop();
});
it("tls first reject unauthorized", async () => {
const ns = await NatsServer.start({
host: "0.0.0.0",
tls: {
handshake_first: true,
cert_file: resolve(join(dir, "./tests/certs/server.pem")),
key_file: resolve(join(dir, "./tests/certs/key.pem")),
ca_file: resolve(join(dir, "./tests/certs/ca.pem")),
},
});
const nc = await connect({
port: ns.port,
tls: {
handshakeFirst: true,
rejectUnauthorized: false,
},
});
await nc.flush();
await nc.close();
await ns.stop();
});
});
--- transport-node/README.md ---
# NATS Node Transport - A [NATS](http://nats.io) client for [Node.Js](https://nodejs.org/en/)
A Node.js transport for the [NATS messaging system](https://nats.io).
[![License](https://img.shields.io/badge/Licence-Apache%202.0-blue.svg)](./LICENSE)
![transport-node](https://github.com/nats-io/nats.js/actions/workflows/transport-node-test.yml/badge.svg)
[![JSDoc](https://img.shields.io/badge/JSDoc-reference-blue)](https://nats-io.github.io/nats.js)
[![NPM Version](https://img.shields.io/npm/v/%40nats-io%2Ftransport-node)](https://www.npmjs.com/package/@nats-io/transport-node)
![NPM Downloads](https://img.shields.io/npm/dt/%40nats-io%2Ftransport-node)
![NPM Downloads](https://img.shields.io/npm/dm/%40nats-io%2Ftransport-node)
This module implements a Node.js native TCP transport for NATS. This library
re-exports [NATS core](../core/README.md) library which implements all basic
NATS client functionality. This library is compatible with
[Bun](https://bun.sh/).
# Installation
```bash
npm install @nats-io/transport-node
# or
bun install @nats-io/transport-node
```
You can then import the `connect` function to connect using the node transport
like this:
```typescript
import { connect } from "@nats-io/transport-node";
```
To use [NATS JetStream](../jetstream/README.md), [NATS KV](../kv/README.md),
[NATS Object Store](../obj/README.md), or the
[NATS Services](../services/README.md) functionality you'll need to install the
desired modules as described in each of the modules README files.
This module simply exports a
[`connect()` function](../core/README.md#connecting-to-a-nats-server) that
returns a `NatsConnection` supported by a Nodejs TCP socket. This library
re-exports all the public APIs for the [core](../core/README.md) module. Please
visit the core module for examples on how to use a connection or refer to the
[JSDoc documentation](https://nats-io.github.io/nats.deno).
## Supported Node Versions
Our support policy for Nodejs versions follows
[Nodejs release support](https://github.com/nodejs/Release). We will support and
build node-nats on even-numbered Nodejs versions that are current or in LTS.
--- README.md ---
# NATS.js - The JavaScript clients for [NATS](http://nats.io)
[![License](https://img.shields.io/badge/Licence-Apache%202.0-blue.svg)](./LICENSE)
[![JSDoc](https://img.shields.io/badge/JSDoc-reference-blue)](https://nats-io.github.io/nats.js/)
![example workflow](https://github.com/nats-io/nats.js/actions/workflows/test.yml/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/nats-io/nats.js/badge.svg?branch=main&kill_cache=1)](https://coveralls.io/github/nats-io/nats.js?branch=main)
> [!IMPORTANT]
>
> This project reorganizes the NATS Base Client library (originally part of
> nats.deno), into multiple modules, and on-boards the supported transports
> (Deno, Node/Bun, and WebSocket).
>
> This repository now supersedes:
>
> - [nats.deno](https://github.com/nats-io/nats.deno)
> - [nats.ws](https://github.com/nats-io/nats.ws)
> - Note that the old nats.js has been renamed to
> [nats.node](https://github.com/nats-io/nats.node) so that the repository
> _nats.js_ could be used for this project.
>
> Changes are well documented and should be easy to locate and implement, and
> are all described in [migration.md](migration.md).
Welcome to the new NATS.js repository! Beginning with the v3 release of the
JavaScript clients, the NATS.js repository reorganizes the NATS JavaScript
client libraries into a formal mono-repo.
This repository hosts native runtime support ("transports") for:
- Deno
- Node/Bun
- Browsers (W3C websocket)
A big change with the v3 clients is that the "nats-base-client" which implements
all the runtime agnostic functionality of the clients, is now split into several
modules. This split simplifies the initial user experience as well as the
development and evolution of the JavaScript clients and new functionality.
The new modules are:
- [Core](core/README.md) which implements basic NATS core functionality
- [JetStream](jetstream/README.md) which implements JetStream functionality
- [Kv](kv/README.md) which implements NATS KV functionality (uses JetStream)
- [Obj](obj/README.md) which implements NATS Object Store functionality (uses
JetStream)
- [Services](services/README.md) which implements a framework for building NATS
services
If you are getting started with NATS for the first time, you'll be able to pick
one of our technologies and more easily incorporate it into your apps. Perhaps
you heard about the NATS KV and would like to incorporate it into your app. The
KV module will shortcut a lot of concepts for you. You will of course need a
transport which will allow you to `connect` to a NATS server, but once you know
how to create a connection you will be focusing on a smaller subset of the APIs
rather than be confronted with all the functionality you available to a NATS
client. From there, we are certain that you will broaden your use of NATS into
other areas, but your initial effort should be more straight forward.
Another reason for the change is that, the new modules have the potential to
make your client a bit smaller, and if versions change on a submodule that you
don't use, you won't be confronted with an upgrade choice. These modules also
allows us to version more strictly, and thus telegraph to you the effort or
scope of changes in the update and prevent surprises when upgrading.
By decoupling of the NATS client functionality from a transport, we enable NATS
developers to create new modules that can run in all runtimes so long as they
follow a pattern where a `NatsConnection` (or some other standard interface) is
used as the basis of the module. For example, the JetStream module exposes the
`jetstream()` and `jetstreamManager()` functions that return the JetStream API
to interact with JetStream for creating resources or consuming streams. The
actual connection type is not important, and the library will work regardless of
the runtime provided so long as the runtime has the minimum support required by
the library.
# Getting Started
For a quick overview of the libraries and how to install them, see
[runtimes.md](runtimes.md).
If you are migrating from the legacy nats.deno or nats.js or nats.ws clients
don't despair. Changes are well documented and should be easy to locate and
implement, and are all [described in migration.md](migration.md).
If you want to get started with NATS the best starting point is the transport
that matches the runtime you want to use:
- [Deno Transport](transport-deno/README.md) which implements a TCP transport
for [Deno](https://deno.land)
- [Node Transport](transport-node/README.md) which implements a TCP transport
for [Node](https://nodejs.org) and [Bun](https://bun.sh).
- For browser [W3C Websocket] runtimes, the websocket client is now part of the
[Core](core/README.md), as some runtimes such as Deno and Node v22 support it
natively.
The module for the transport will tell you how to install it, and how to use it.
If you want to write a library that uses NATS under the cover, your starting
point is likely [Core](core/README.md). If data oriented, it may be
[JetStream](jetstream/README.md).
## Documentation
Each of the modules has an introductory page that shows the main API usage for
the module. You can access it's
[JSDoc here](https://nats-io.github.io/nats.js/).
## Contributing
If you are interested in contributing to NATS, read about our...
- [Contributing guide](./CONTRIBUTING.md)
- [Report issues or propose Pull Requests](https://github.com/nats-io)
This file has been truncated, but you can view the full file.
Selected Files Directory Structure:
└── ./
├── README.md
├── core
│ ├── README.md
│ └── examples
│ └── snippets
│ ├── autounsub.ts
│ ├── basics.ts
│ ├── connect.ts
│ ├── headers.ts
│ ├── js-client.ts
│ ├── js.ts
│ ├── json.ts
│ ├── kv.ts
│ ├── no_responders.ts
│ ├── queuegroups.ts
│ ├── service.ts
│ ├── service_client.ts
│ ├── stream.ts
│ ├── sub_timeout.ts
│ ├── unsub.ts
│ └── wildcard_subscriptions.ts
├── jetstream
│ ├── README.md
│ └── examples
│ ├── 01_consumers.js
│ ├── 02_next.js
│ ├── 03_batch.js
│ ├── 04_consume.js
│ ├── 05_consume.js
│ ├── 06_heartbeats.js
│ ├── 07_consume_jobs.js
│ ├── 08_consume_process.js
│ ├── 09_replay.js
│ ├── js_readme_publish_examples.js
│ ├── jsm_readme_jsm_example.js
│ └── util.js
├── kv
│ └── README.md
├── obj
│ └── README.md
├── services
│ ├── README.md
│ └── examples
│ ├── 01_services.ts
│ ├── 02_multiple_endpoints.ts
│ ├── 03_bigdata-client.ts
│ ├── 03_bigdata.ts
│ └── 03_util.ts
└── transport-node
├── README.md
└── examples
├── bench.js
├── nats-events.js
├── nats-pub.js
├── nats-rep.js
├── nats-req.js
├── nats-sub.js
└── util.js
--- core/examples/snippets/autounsub.ts ---
/*
* Copyright 2020 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
import type { Subscription } from "@nats-io/transport-deno";
// create a connection
const nc = await connect({ servers: "demo.nats.io:4222" });
// create a simple subscriber that listens for only one message
// and then auto unsubscribes, ending the async iterator
const sub = nc.subscribe("hello", { max: 3 });
const h1 = handler(sub);
const msub = nc.subscribe("hello");
const h2 = handler(msub);
for (let i = 1; i < 6; i++) {
nc.publish("hello", `hello-${i}`);
}
// ensure all the messages have been delivered to the server
// meaning that the subscription also processed them.
await nc.flush();
// unsub manually from the second subscription
msub.unsubscribe();
// await the handlers come back
await Promise.all([h1, h2]);
await nc.close();
async function handler(s: Subscription) {
const id = s.getID();
const max = s.getMax();
console.log(
`sub [${id}] listening to ${s.getSubject()} ${
max ? "and will unsubscribe after " + max + " msgs" : ""
}`,
);
for await (const m of s) {
console.log(`sub [${id}] #${s.getProcessed()}: ${m.string()}`);
}
console.log(`sub [${id}] is done.`);
}
--- core/examples/snippets/basics.ts ---
/*
* Copyright 2020 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "demo.nats.io:4222" });
// create a simple subscriber and iterate over messages
// matching the subscription
const sub = nc.subscribe("hello");
(async () => {
for await (const m of sub) {
console.log(`[${sub.getProcessed()}]: ${m.string()}`);
}
console.log("subscription closed");
})();
nc.publish("hello", "world");
nc.publish("hello", "again");
// we want to ensure that messages that are in flight
// get processed, so we are going to drain the
// connection. Drain is the same as close, but makes
// sure that all messages in flight get seen
// by the iterator. After calling drain,
// the connection closes.
await nc.drain();
--- core/examples/snippets/connect.ts ---
/*
* Copyright 2020-2021 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 the connect function from a transport
import { connect } from "@nats-io/transport-deno";
const servers = [
{},
{ servers: ["demo.nats.io:4442", "demo.nats.io:4222"] },
{ servers: "demo.nats.io:4443" },
{ port: 4222 },
{ servers: "localhost" },
];
servers.forEach(async (v) => {
try {
const nc = await connect(v);
console.log(`connected to ${nc.getServer()}`);
// this promise indicates the client closed
const done = nc.closed();
// do something with the connection
// close the connection
await nc.close();
// check if the close was OK
const err = await done;
if (err) {
console.log(`error closing:`, err);
}
} catch (_err) {
console.log(`error connecting to ${JSON.stringify(v)}`);
}
});
--- core/examples/snippets/headers.ts ---
/*
* Copyright 2021-2024 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,
createInbox,
Empty,
headers,
nuid,
} from "@nats-io/transport-deno";
const nc = await connect(
{
servers: `demo.nats.io`,
},
);
// this function generates a random inbox subject
const subj = createInbox();
// subscribe to it
const sub = nc.subscribe(subj);
(async () => {
for await (const m of sub) {
if (m.headers) {
for (const [key, value] of m.headers) {
console.log(`${key}=${value}`);
}
console.log("ID", m.headers.get("ID"));
console.log("Id", m.headers.get("Id"));
console.log("id", m.headers.get("id"));
}
}
})().then();
// header names can be any printable ASCII character with the exception of `:`.
// header values can be any ASCII character except `\r` or `\n`.
// see https://www.ietf.org/rfc/rfc822.txt
const h = headers();
h.append("id", nuid.next());
h.append("unix_time", Date.now().toString());
nc.publish(subj, Empty, { headers: h });
await nc.flush();
await nc.close();
--- core/examples/snippets/js-client.ts ---
<Error reading file: Error: ENOENT: no such file or directory, open '/Users/hk/Dev/local-others/nats.js/core/examples/snippets/js-client.ts'>
--- core/examples/snippets/js.ts ---
import { connect, Empty } from "@nats-io/transport-deno";
// to create a connection to a nats-server:
const nc = await connect({ servers: "localhost:4222" });
import { AckPolicy, jetstream, jetstreamManager } from "@nats-io/jetstream";
const jsm = await jetstreamManager(nc);
for await (const si of jsm.streams.list()) {
console.log(si);
}
// add a stream - jetstream can capture nats core messages
const stream = "mystream";
const subj = `mystream.*`;
await jsm.streams.add({ name: stream, subjects: [subj] });
for (let i = 0; i < 100; i++) {
nc.publish(`${subj}.a`, Empty);
}
// find a stream that stores a specific subject:
const name = await jsm.streams.find("mystream.A");
// retrieve info about the stream by its name
const si = await jsm.streams.info(name);
// update a stream configuration
si.config.subjects?.push("a.b");
await jsm.streams.update(name, si.config);
// get a particular stored message in the stream by sequence
// this is not associated with a consumer
const sm = await jsm.streams.getMessage(stream, { seq: 1 });
console.log("sm?.seq", sm?.seq);
// delete the 5th message in the stream, securely erasing it
await jsm.streams.deleteMessage(stream, 5);
// purge all messages in the stream, the stream itself remains.
await jsm.streams.purge(stream);
// purge all messages with a specific subject (filter can be a wildcard)
await jsm.streams.purge(stream, { filter: "a.b" });
// purge messages with a specific subject keeping some messages
await jsm.streams.purge(stream, { filter: "a.c", keep: 5 });
// purge all messages with upto (not including seq)
await jsm.streams.purge(stream, { seq: 90 });
// purge all messages with upto sequence that have a matching subject
await jsm.streams.purge(stream, { filter: "a.d", seq: 100 });
// list all consumers for a stream:
const consumers = await jsm.consumers.list(stream).next();
consumers.forEach((ci) => {
console.log(ci);
});
// add a new durable consumer
await jsm.consumers.add(stream, {
durable_name: "me",
ack_policy: AckPolicy.Explicit,
});
// retrieve a consumer's status and configuration
const ci = await jsm.consumers.info(stream, "me");
console.log(ci);
// delete a particular consumer
await jsm.consumers.delete(stream, "me");
// ```
// ```typescript
// // create the str
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment