diff --git a/.github/workflows/consistency_checks.yml b/.github/workflows/consistency_checks.yml index e484117a..9d339d16 100644 --- a/.github/workflows/consistency_checks.yml +++ b/.github/workflows/consistency_checks.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest-4-cores strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] module: [ core, jetstream, diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 2c6e7872..35fcd0a3 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest-4-cores strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] node-version: [22.x] permissions: contents: read diff --git a/.github/workflows/deno_checks.yml b/.github/workflows/deno_checks.yml index 78a12325..2a909a87 100644 --- a/.github/workflows/deno_checks.yml +++ b/.github/workflows/deno_checks.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest-4-cores strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] module: [core, jetstream, kv, obj, services, transport-deno] steps: diff --git a/.github/workflows/jetstream.yml b/.github/workflows/jetstream.yml index 06cf49e2..22c5f25c 100644 --- a/.github/workflows/jetstream.yml +++ b/.github/workflows/jetstream.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest-4-cores strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] steps: - name: Git Checkout Core diff --git a/.github/workflows/kv.yml b/.github/workflows/kv.yml index eca9e0de..d1be3a5f 100644 --- a/.github/workflows/kv.yml +++ b/.github/workflows/kv.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest-4-cores strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] steps: - name: Git Checkout Core diff --git a/.github/workflows/node_checks.yml b/.github/workflows/node_checks.yml index 281afa07..57303f8a 100644 --- a/.github/workflows/node_checks.yml +++ b/.github/workflows/node_checks.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest-4-cores strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] node-version: [22.x] steps: diff --git a/.github/workflows/obj.yml b/.github/workflows/obj.yml index d4fafb2e..abe7d0d6 100644 --- a/.github/workflows/obj.yml +++ b/.github/workflows/obj.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest-4-cores strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] steps: - name: Git Checkout Core diff --git a/.github/workflows/services.yml b/.github/workflows/services.yml index e8420264..cdc4f334 100644 --- a/.github/workflows/services.yml +++ b/.github/workflows/services.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest-4-cores strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] steps: - name: Git Checkout Core diff --git a/.github/workflows/transport-node-test.yml b/.github/workflows/transport-node-test.yml index 242286e7..3c04e251 100644 --- a/.github/workflows/transport-node-test.yml +++ b/.github/workflows/transport-node-test.yml @@ -18,7 +18,7 @@ jobs: test: strategy: matrix: - deno-version: [1.45.x] + deno-version: [2.0.x] node-version: [22.x] name: test node transport with local dependencies diff --git a/TODO.md b/TODO.md index 6c6f0f4c..d7a9b673 100644 --- a/TODO.md +++ b/TODO.md @@ -9,3 +9,5 @@ - headers_only is needed in Consumer - add a test for next/fetch/consume where message size smaller than availablle + +- doc diff --git a/bin/check-dep-versions.ts b/bin/check-dep-versions.ts index 45a2b927..dc0458c1 100644 --- a/bin/check-dep-versions.ts +++ b/bin/check-dep-versions.ts @@ -26,6 +26,7 @@ type PackageJSON = { name: string; version: string; dependencies: Record; + devDependencies: Record; }; type DenoJSON = { name: string; @@ -207,6 +208,15 @@ class NodeModule extends BaseModule { return null; } + hasDev(module: string): SemVer | null { + if (this.data.devDependencies) { + return NodeModule.parseVersion( + this.data.devDependencies[module], + ); + } + return null; + } + static parseVersion(v: string): SemVer | null { if (v) { return v.startsWith("^") || v.startsWith("~") @@ -220,12 +230,26 @@ class NodeModule extends BaseModule { if (this.data.dependencies) { const have = this.has(module); if (have && version.compare(have) !== 0) { - this.data.dependencies[module] = `~${version.string()}`; + let prefix = this.data.dependencies[module].charAt(0); + if (prefix !== "^" && prefix !== "~") { + prefix = ""; + } + this.data.dependencies[module] = `${prefix}${version.string()}`; this.changed = true; - return true; } } - return false; + if (this.data.devDependencies) { + const have = this.hasDev(module); + if (have && version.compare(have) !== 0) { + let prefix = this.data.devDependencies[module].charAt(0); + if (prefix !== "^" && prefix !== "~") { + prefix = ""; + } + this.data.devDependencies[module] = `${prefix}${version.string()}`; + this.changed = true; + } + } + return this.changed; } store(dir: string): Promise { @@ -279,15 +303,15 @@ for (const dir of dirs) { } const nmm = await NodeModule.load(d); if (nmm) { - if (nmm.has(moduleName)) { + if (nmm.has(moduleName) || nmm.hasDev(moduleName)) { nmm.update(moduleName, v); await nmm.store(d); } - const onuid = nmm.has("@nats-io/nuid"); + const onuid = nmm.has("@nats-io/nuid") || nmm.hasDev("@nats-io/nuid"); if (onuid) { nuid = nuid.max(onuid); } - const onkeys = nmm.has("@nats-io/nkeys"); + const onkeys = nmm.has("@nats-io/nkeys") || nmm.hasDev("@nats-io/nkeys"); if (onkeys) { nkeys = nkeys.max(onkeys); } diff --git a/core/README.md b/core/README.md index 7061e021..d299763d 100644 --- a/core/README.md +++ b/core/README.md @@ -107,7 +107,7 @@ is working. ```typescript // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-4"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; const servers = [ {}, @@ -179,7 +179,7 @@ the server. ```typescript // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-4"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; // to create a connection to a nats-server: const nc = await connect({ servers: "demo.nats.io:4222" }); @@ -241,8 +241,8 @@ All subscriptions are independent. If two different subscriptions match a subject, both will get to process the message: ```typescript -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-4"; -import type { Subscription } from "jsr:@nats-io/nats-transport-deno@3.0.0-4"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import type { Subscription } from "jsr:@nats-io/transport-deno@3.0.0-7"; const nc = await connect({ servers: "demo.nats.io:4222" }); // subscriptions can have wildcard subjects @@ -418,11 +418,11 @@ independent unit. Note that non-queue subscriptions are also independent of subscriptions in a queue group. ```typescript -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; import type { NatsConnection, Subscription, -} from "jsr:@nats-io/nats-transport-deno@3.0.0-4"; +} from "jsr:@nats-io/transport-deno@3.0.0-7"; async function createService( name: string, @@ -541,29 +541,33 @@ If you send a request for which there's no interest, the request will be immediately rejected: ```typescript -import { connect, ErrorCode } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import type { NatsError } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; - -const nc = await connect( - { - servers: `demo.nats.io`, - }, -); +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { + NoRespondersError, + RequestError, + TimeoutError, +} from "jsr:@nats-io/transport-deno@3.0.0-7"; + +const nc = await connect({ + servers: `demo.nats.io`, +}); try { const m = await nc.request("hello.world"); console.log(m.data); } catch (err) { - const nerr = err as NatsError; - switch (nerr.code) { - case ErrorCode.NoResponders: - console.log("no one is listening to 'hello.world'"); - break; - case ErrorCode.Timeout: + if (err instanceof RequestError) { + if (err.cause instanceof TimeoutError) { console.log("someone is listening but didn't respond"); - break; - default: - console.log("request failed", err); + } 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}`); } } @@ -591,7 +595,7 @@ Setting the `user`/`pass` or `token` options, simply initializes an ```typescript // if the connection requires authentication, provide `user` and `pass` or // `token` options in the NatsConnectionOptions -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-5"; const nc1 = await connect({ servers: "127.0.0.1:4222", @@ -680,8 +684,8 @@ You can specify several options when creating a subscription: - `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: NatsError|null, msg: Msg) => void;` that should be used for handling - the message. Subscriptions with callbacks are NOT iterators. + `(err: Error|null, msg: Msg) => void;` that should be used for handling the + message. Subscriptions with callbacks are NOT iterators. #### Auto Unsubscribe @@ -701,7 +705,7 @@ const sub = nc.subscribe("hello", { timeout: 1000 }); // handle the messages } })().catch((err) => { - if (err.code === ErrorCode.Timeout) { + if (err instanceof TimeoutError) { console.log(`sub timed out!`); } else { console.log(`sub iterator got an error!`); diff --git a/core/deno.json b/core/deno.json index a7270a7d..ddbc0ad0 100644 --- a/core/deno.json +++ b/core/deno.json @@ -1,6 +1,6 @@ { "name": "@nats-io/nats-core", - "version": "3.0.0-30", + "version": "3.0.0-34", "exports": { ".": "./src/mod.ts", "./internal": "./src/internal_mod.ts" diff --git a/core/examples/snippets/autounsub.ts b/core/examples/snippets/autounsub.ts index 15b1dc41..c65b506a 100644 --- a/core/examples/snippets/autounsub.ts +++ b/core/examples/snippets/autounsub.ts @@ -14,8 +14,8 @@ */ // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import type { Subscription } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import type { Subscription } from "jsr:@nats-io/transport-deno@3.0.0-7"; // create a connection const nc = await connect({ servers: "demo.nats.io:4222" }); diff --git a/core/examples/snippets/basics.ts b/core/examples/snippets/basics.ts index b46c0c58..e7a3214e 100644 --- a/core/examples/snippets/basics.ts +++ b/core/examples/snippets/basics.ts @@ -14,7 +14,7 @@ */ // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; // to create a connection to a nats-server: const nc = await connect({ servers: "demo.nats.io:4222" }); diff --git a/core/examples/snippets/connect.ts b/core/examples/snippets/connect.ts index d1e95378..938d5c58 100644 --- a/core/examples/snippets/connect.ts +++ b/core/examples/snippets/connect.ts @@ -14,7 +14,7 @@ */ // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; const servers = [ {}, diff --git a/core/examples/snippets/headers.ts b/core/examples/snippets/headers.ts index ed3322bc..1a1b6d2e 100644 --- a/core/examples/snippets/headers.ts +++ b/core/examples/snippets/headers.ts @@ -19,7 +19,7 @@ import { Empty, headers, nuid, -} from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +} from "jsr:@nats-io/transport-deno@3.0.0-7"; const nc = await connect( { diff --git a/core/examples/snippets/json.ts b/core/examples/snippets/json.ts index a05984be..4bd7e484 100644 --- a/core/examples/snippets/json.ts +++ b/core/examples/snippets/json.ts @@ -14,7 +14,7 @@ */ // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; // to create a connection to a nats-server: const nc = await connect({ servers: "demo.nats.io:4222" }); @@ -30,7 +30,7 @@ const sub = nc.subscribe("people"); for await (const m of sub) { // typescript will see this as a Person const p = m.json(); - console.log(`[${sub.getProcessed()}]: ${p.name}`); + console.log(p); } })(); diff --git a/core/examples/snippets/no_responders.ts b/core/examples/snippets/no_responders.ts index cbca427d..b2a65092 100644 --- a/core/examples/snippets/no_responders.ts +++ b/core/examples/snippets/no_responders.ts @@ -1,5 +1,20 @@ -import { connect, ErrorCode } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import type { NatsError } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +/* + * 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 } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { errors } from "jsr:@nats-io/transport-deno@3.0.0-7"; const nc = await connect( { @@ -11,16 +26,18 @@ try { const m = await nc.request("hello.world"); console.log(m.data); } catch (err) { - const nerr = err as NatsError; - switch (nerr.code) { - case ErrorCode.NoResponders: - console.log("no one is listening to 'hello.world'"); - break; - case ErrorCode.Timeout: + if (err instanceof Error) { + if (err.cause instanceof errors.TimeoutError) { console.log("someone is listening but didn't respond"); - break; - default: - console.log("request failed", err); + } 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}`); } } diff --git a/core/examples/snippets/queuegroups.ts b/core/examples/snippets/queuegroups.ts index a7848970..98877d42 100644 --- a/core/examples/snippets/queuegroups.ts +++ b/core/examples/snippets/queuegroups.ts @@ -12,11 +12,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; import type { NatsConnection, Subscription, -} from "jsr:@nats-io/nats-transport-deno@3.0.0-4"; +} from "jsr:@nats-io/transport-deno@3.0.0-7"; async function createService( name: string, diff --git a/core/examples/snippets/service.ts b/core/examples/snippets/service.ts index 07593023..1d5cee35 100644 --- a/core/examples/snippets/service.ts +++ b/core/examples/snippets/service.ts @@ -14,8 +14,8 @@ */ // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import type { Subscription } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import type { Subscription } from "jsr:@nats-io/transport-deno@3.0.0-7"; // create a connection const nc = await connect({ servers: "demo.nats.io" }); diff --git a/core/examples/snippets/service_client.ts b/core/examples/snippets/service_client.ts index ad5edfea..a2089b54 100644 --- a/core/examples/snippets/service_client.ts +++ b/core/examples/snippets/service_client.ts @@ -14,7 +14,7 @@ */ // import the connect function from a transport -import { connect, Empty } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect, Empty } from "jsr:@nats-io/transport-deno@3.0.0-7"; // create a connection const nc = await connect({ servers: "demo.nats.io:4222" }); diff --git a/core/examples/snippets/stream.ts b/core/examples/snippets/stream.ts index 24025748..970e7adf 100644 --- a/core/examples/snippets/stream.ts +++ b/core/examples/snippets/stream.ts @@ -14,7 +14,7 @@ */ // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; // to create a connection to a nats-server: const nc = await connect({ servers: "demo.nats.io" }); diff --git a/core/examples/snippets/sub_timeout.ts b/core/examples/snippets/sub_timeout.ts index 1e525df6..1214099c 100644 --- a/core/examples/snippets/sub_timeout.ts +++ b/core/examples/snippets/sub_timeout.ts @@ -14,7 +14,7 @@ */ // import the connect function from a transport -import { connect, ErrorCode } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect, errors } from "jsr:@nats-io/transport-deno@3.0.0-7"; // to create a connection to a nats-server: const nc = await connect({ servers: "demo.nats.io:4222" }); @@ -27,7 +27,7 @@ const sub = nc.subscribe("hello", { timeout: 1000 }); // handle the messages } })().catch((err) => { - if (err.code === ErrorCode.Timeout) { + if (err instanceof errors.TimeoutError) { console.log(`sub timed out!`); } else { console.log(`sub iterator got an error!`); diff --git a/core/examples/snippets/unsub.ts b/core/examples/snippets/unsub.ts index 60f3d7bc..a232cf87 100644 --- a/core/examples/snippets/unsub.ts +++ b/core/examples/snippets/unsub.ts @@ -14,7 +14,7 @@ */ // import the connect function from a transport -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; // to create a connection to a nats-server: const nc = await connect({ servers: "demo.nats.io:4222" }); @@ -38,9 +38,9 @@ const manual = nc.subscribe("hello"); const done = (async () => { console.log("waiting for a message on `hello` with a payload of `stop`"); for await (const m of manual) { - const d = sc.decode(m.data); - console.log("manual", manual.getProcessed(), d); - if (d === "stop") { + const payload = m.string(); + console.log("manual", manual.getProcessed(), payload); + if (payload === "stop") { manual.unsubscribe(); } } diff --git a/core/examples/snippets/wildcard_subscriptions.ts b/core/examples/snippets/wildcard_subscriptions.ts index df60c439..47629d0b 100644 --- a/core/examples/snippets/wildcard_subscriptions.ts +++ b/core/examples/snippets/wildcard_subscriptions.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import type { Subscription } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import type { Subscription } from "jsr:@nats-io/transport-deno@3.0.0-7"; const nc = await connect({ servers: "demo.nats.io:4222" }); diff --git a/core/import_map.json b/core/import_map.json index 217cf47b..d9752cfa 100644 --- a/core/import_map.json +++ b/core/import_map.json @@ -3,6 +3,6 @@ "test_helpers": "../test_helpers/mod.ts", "@nats-io/nats-core": "./src/mod.ts", "@nats-io/nats-core/internal": "./src/internal_mod.ts", - "@std/io": "jsr:@std/io@0.224.0" + "@std/io": "jsr:@std/io@0.225.0" } } diff --git a/core/package.json b/core/package.json index 28eccbb1..c986b250 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@nats-io/nats-core", - "version": "3.0.0-30", + "version": "3.0.0-34", "files": [ "lib/", "LICENSE", diff --git a/core/src/authenticator.ts b/core/src/authenticator.ts index 9df7ee41..955d755c 100644 --- a/core/src/authenticator.ts +++ b/core/src/authenticator.ts @@ -23,7 +23,6 @@ import type { TokenAuth, UserPass, } from "./core.ts"; -import { ErrorCode, NatsError } from "./core.ts"; export function multiAuthenticator(authenticators: Authenticator[]) { return (nonce?: string): Auth => { @@ -134,16 +133,13 @@ export function credsAuthenticator( // get the JWT let m = CREDS.exec(s); if (!m) { - throw NatsError.errorForCode(ErrorCode.BadCreds); + throw new Error("unable to parse credentials"); } const jwt = m[1].trim(); // get the nkey m = CREDS.exec(s); if (!m) { - throw NatsError.errorForCode(ErrorCode.BadCreds); - } - if (!m) { - throw NatsError.errorForCode(ErrorCode.BadCreds); + throw new Error("unable to parse credentials"); } const seed = TE.encode(m[1].trim()); diff --git a/core/src/bench.ts b/core/src/bench.ts index de15d34c..41bab4a8 100644 --- a/core/src/bench.ts +++ b/core/src/bench.ts @@ -17,7 +17,6 @@ import { Empty } from "./types.ts"; import { nuid } from "./nuid.ts"; import { deferred, Perf } from "./util.ts"; import type { NatsConnectionImpl } from "./nats.ts"; -import { ErrorCode, NatsError } from "./core.ts"; import type { NatsConnection } from "./core.ts"; export class Metric { @@ -124,7 +123,7 @@ export class Bench { this.payload = this.size ? new Uint8Array(this.size) : Empty; if (!this.pub && !this.sub && !this.req && !this.rep) { - throw new Error("no bench option selected"); + throw new Error("no options selected"); } } @@ -132,11 +131,7 @@ export class Bench { this.nc.closed() .then((err) => { if (err) { - throw new NatsError( - `bench closed with an error: ${err.message}`, - ErrorCode.Unknown, - err, - ); + throw err; } }); diff --git a/core/src/core.ts b/core/src/core.ts index 4878511b..6c61592e 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -14,6 +14,7 @@ */ import { nuid } from "./nuid.ts"; +import { InvalidArgumentError } from "./errors.ts"; /** * Events reported by the {@link NatsConnection#status} iterator. @@ -43,157 +44,7 @@ export enum DebugEvents { ClientInitiatedReconnect = "client initiated reconnect", } -export enum ErrorCode { - // emitted by the client - ApiError = "BAD API", - BadAuthentication = "BAD_AUTHENTICATION", - BadCreds = "BAD_CREDS", - BadHeader = "BAD_HEADER", - BadJson = "BAD_JSON", - BadPayload = "BAD_PAYLOAD", - BadSubject = "BAD_SUBJECT", - Cancelled = "CANCELLED", - ConnectionClosed = "CONNECTION_CLOSED", - ConnectionDraining = "CONNECTION_DRAINING", - ConnectionRefused = "CONNECTION_REFUSED", - ConnectionTimeout = "CONNECTION_TIMEOUT", - Disconnect = "DISCONNECT", - InvalidOption = "INVALID_OPTION", - InvalidPayload = "INVALID_PAYLOAD", - MaxPayloadExceeded = "MAX_PAYLOAD_EXCEEDED", - NoResponders = "503", - NotFunction = "NOT_FUNC", - RequestError = "REQUEST_ERROR", - ServerOptionNotAvailable = "SERVER_OPT_NA", - SubClosed = "SUB_CLOSED", - SubDraining = "SUB_DRAINING", - Timeout = "TIMEOUT", - Tls = "TLS", - Unknown = "UNKNOWN_ERROR", - WssRequired = "WSS_REQUIRED", - - // jetstream - JetStreamInvalidAck = "JESTREAM_INVALID_ACK", - JetStream404NoMessages = "404", - JetStream408RequestTimeout = "408", - //@deprecated: use JetStream409 - JetStream409MaxAckPendingExceeded = "409", - JetStream409 = "409", - JetStreamNotEnabled = "503", - JetStreamIdleHeartBeat = "IDLE_HEARTBEAT", - - // emitted by the server - AuthorizationViolation = "AUTHORIZATION_VIOLATION", - AuthenticationExpired = "AUTHENTICATION_EXPIRED", - ProtocolError = "NATS_PROTOCOL_ERR", - PermissionsViolation = "PERMISSIONS_VIOLATION", - AuthenticationTimeout = "AUTHENTICATION_TIMEOUT", - AccountExpired = "ACCOUNT_EXPIRED", -} - -export function isNatsError(err: NatsError | Error): err is NatsError { - return typeof (err as NatsError).code === "string"; -} - -export interface ApiError { - /** - * HTTP like error code in the 300 to 500 range - */ - code: number; - /** - * A human friendly description of the error - */ - description: string; - /** - * The NATS error code unique to each kind of error - */ - err_code?: number; -} - -export class Messages { - messages: Map; - - constructor() { - this.messages = new Map(); - this.messages.set( - ErrorCode.InvalidPayload, - "Invalid payload type - payloads can be 'binary', 'string', or 'json'", - ); - this.messages.set(ErrorCode.BadJson, "Bad JSON"); - this.messages.set( - ErrorCode.WssRequired, - "TLS is required, therefore a secure websocket connection is also required", - ); - } - - static getMessage(s: string): string { - return messages.getMessage(s); - } - - getMessage(s: string): string { - return this.messages.get(s) || s; - } -} - -// safari doesn't support static class members -const messages: Messages = new Messages(); - -export class NatsError extends Error { - // TODO: on major version this should change to a number/enum - code: string; - permissionContext?: { operation: string; subject: string; queue?: string }; - chainedError?: Error; - // these are for supporting jetstream - api_error?: ApiError; - - /** - * @param {String} message - * @param {String} code - * @param {Error} [chainedError] - * - * @api private - */ - constructor(message: string, code: string, chainedError?: Error) { - super(message); - this.name = "NatsError"; - this.message = message; - this.code = code; - this.chainedError = chainedError; - } - - static errorForCode(code: string, chainedError?: Error): NatsError { - const m = Messages.getMessage(code); - return new NatsError(m, code, chainedError); - } - - isAuthError(): boolean { - return this.code === ErrorCode.AuthenticationExpired || - this.code === ErrorCode.AuthorizationViolation || - this.code === ErrorCode.AccountExpired; - } - - isAuthTimeout(): boolean { - return this.code === ErrorCode.AuthenticationTimeout; - } - - isPermissionError(): boolean { - return this.code === ErrorCode.PermissionsViolation; - } - - isProtocolError(): boolean { - return this.code === ErrorCode.ProtocolError; - } - - isJetStreamError(): boolean { - return this.api_error !== undefined; - } - - jsError(): ApiError | null { - return this.api_error ? this.api_error : null; - } -} - -export type MsgCallback = (err: NatsError | null, msg: T) => void; +export type MsgCallback = (err: Error | null, msg: T) => void; /** * Subscription Options @@ -232,6 +83,7 @@ export interface DnsResolveFn { export interface Status { type: Events | DebugEvents; data: string | ServersChanged | number; + error?: Error; permissionContext?: { operation: string; subject: string }; } @@ -806,12 +658,15 @@ export interface Publisher { export function createInbox(prefix = ""): string { prefix = prefix || "_INBOX"; if (typeof prefix !== "string") { - throw (new Error("prefix must be a string")); + throw (new TypeError("prefix must be a string")); } prefix.split(".") .forEach((v) => { if (v === "*" || v === ">") { - throw new Error(`inbox prefixes cannot have wildcards '${prefix}'`); + throw InvalidArgumentError.format( + "prefix", + `cannot have wildcards ('${prefix}')`, + ); } }); return `${prefix}.${nuid.next()}`; @@ -824,7 +679,7 @@ export interface Request { resolver(err: Error | null, msg: Msg): void; - cancel(err?: NatsError): void; + cancel(err?: Error): void; } /** diff --git a/core/src/errors.ts b/core/src/errors.ts new file mode 100644 index 00000000..eed0c1dd --- /dev/null +++ b/core/src/errors.ts @@ -0,0 +1,300 @@ +/* + * 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. + */ + +/** + * Represents an error that is thrown when an invalid subject is encountered. + * This class extends the built-in Error object. + * + * @class + * @extends Error + */ +export class InvalidSubjectError extends Error { + constructor(subject: string, options?: ErrorOptions) { + super(`illegal subject: '${subject}'`, options); + this.name = "InvalidSubjectError"; + } +} + +export class InvalidArgumentError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "InvalidArgumentError"; + } + + static format( + property: string | string[], + message: string, + options?: ErrorOptions, + ): InvalidArgumentError { + if (Array.isArray(message) && message.length > 1) { + message = message[0]; + } + if (Array.isArray(property)) { + property = property.map((n) => `'${n}'`); + property = property.join(","); + } else { + property = `'${property}'`; + } + return new InvalidArgumentError(`${property} ${message}`, options); + } +} + +/** + * InvalidOperationError is a custom error class that extends the standard Error object. + * It represents an error that occurs when an invalid operation is attempted on one of + * objects returned by the API. For example, trying to iterate on an object that was + * configured with a callback. + * + * @class InvalidOperationError + * @extends {Error} + * + * @param {string} message - The error message that explains the reason for the error. + * @param {ErrorOptions} [options] - Optional parameter to provide additional error options. + */ +export class InvalidOperationError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "InvalidOperationError"; + } +} + +/** + * Represents an error indicating that user authentication has expired. + * This error is typically thrown when a user attempts to access a connection + * but their authentication credentials have expired. + */ +export class UserAuthenticationExpiredError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "UserAuthenticationExpiredError"; + } + + static parse(s: string): UserAuthenticationExpiredError | null { + const ss = s.toLowerCase(); + if (ss.indexOf("user authentication expired") !== -1) { + return new UserAuthenticationExpiredError(s); + } + return null; + } +} + +/** + * Represents an error related to authorization issues. + * Note that these could represent an authorization violation, + * or that the account authentication configuration has expired, + * or an authentication timeout. + */ +export class AuthorizationError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "AuthorizationError"; + } + + static parse(s: string): AuthorizationError | null { + const messages = [ + "authorization violation", + "account authentication expired", + "authentication timeout", + ]; + + const ss = s.toLowerCase(); + + for (let i = 0; i < messages.length; i++) { + if (ss.indexOf(messages[i]) !== -1) { + return new AuthorizationError(s); + } + } + + return null; + } +} + +/** + * Class representing an error thrown when an operation is attempted on a closed connection. + * + * This error is intended to signal that a connection-related operation could not be completed + * because the connection is no longer open or has been terminated. + * + * @class + * @extends Error + */ +export class ClosedConnectionError extends Error { + constructor() { + super("closed connection"); + this.name = "ClosedConnectionError"; + } +} + +/** + * The `ConnectionDrainingError` class represents a specific type of error + * that occurs when a connection is being drained. + * + * This error is typically used in scenarios where connections need to be + * gracefully closed or when they are transitioning to an inactive state. + * + * The error message is set to "connection draining" and the error name is + * overridden to "DrainingConnectionError". + */ +export class DrainingConnectionError extends Error { + constructor() { + super("connection draining"); + this.name = "DrainingConnectionError"; + } +} + +/** + * Represents an error that occurs when a network connection fails. + * Extends the built-in Error class to provide additional context for connection-related issues. + * + * @param {string} message - A human-readable description of the error. + * @param {ErrorOptions} [options] - Optional settings for customizing the error behavior. + */ +export class ConnectionError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "ConnectionError"; + } +} + +/** + * Represents an error encountered during protocol operations. + * This class extends the built-in `Error` class, providing a specific + * error type called `ProtocolError`. + * + * @param {string} message - A descriptive message describing the error. + * @param {ErrorOptions} [options] - Optional parameters to include additional details. + */ +export class ProtocolError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "ProtocolError"; + } +} + +/** + * Class representing an error that occurs during an request operation + * (such as TimeoutError, or NoRespondersError, or some other error). + * + * @extends Error + */ +export class RequestError extends Error { + constructor(message = "", options?: ErrorOptions) { + super(message, options); + this.name = "RequestError"; + } + + isNoResponders(): boolean { + return this.cause instanceof NoRespondersError; + } +} + +/** + * TimeoutError is a custom error class that extends the built-in Error class. + * It is used to represent an error that occurs when an operation exceeds a + * predefined time limit. + * + * @class + * @extends {Error} + */ +export class TimeoutError extends Error { + constructor(options?: ErrorOptions) { + super("timeout", options); + this.name = "TimeoutError"; + } +} + +/** + * NoRespondersError is an error thrown when no responders (no service is + * subscribing to the subject) are found for a given subject. This error + * is typically found as the cause for a RequestError + * + * @extends Error + * + * @param {string} subject - The subject for which no responders were found. + * @param {ErrorOptions} [options] - Optional error options. + */ +export class NoRespondersError extends Error { + subject: string; + constructor(subject: string, options?: ErrorOptions) { + super(`no responders: '${subject}'`, options); + this.subject = subject; + this.name = "NoResponders"; + } +} + +/** + * Class representing a Permission Violation Error. + * It provides information about the operation (either "publish" or "subscription") + * and the subject used for the operation and the optional queue (if a subscription). + * + * This error is terminal for a subscription. + */ +export class PermissionViolationError extends Error { + operation: "publish" | "subscription"; + subject: string; + queue?: string; + + constructor( + message: string, + operation: "publish" | "subscription", + subject: string, + queue?: string, + options?: ErrorOptions, + ) { + super(message, options); + this.name = "PermissionViolationError"; + this.operation = operation; + this.subject = subject; + this.queue = queue; + } + + static parse(s: string): PermissionViolationError | null { + const t = s ? s.toLowerCase() : ""; + if (t.indexOf("permissions violation") === -1) { + return null; + } + let operation: "publish" | "subscription" = "publish"; + let subject = ""; + let queue: string | undefined = undefined; + const m = s.match(/(Publish|Subscription) to "(\S+)"/); + if (m) { + operation = m[1].toLowerCase() as "publish" | "subscription"; + subject = m[2]; + if (operation === "subscription") { + const qm = s.match(/using queue "(\S+)"/); + if (qm) { + queue = qm[1]; + } + } + } + return new PermissionViolationError(s, operation, subject, queue); + } +} + +export const errors = { + AuthorizationError, + ClosedConnectionError, + ConnectionError, + DrainingConnectionError, + InvalidArgumentError, + InvalidOperationError, + InvalidSubjectError, + NoRespondersError, + PermissionViolationError, + ProtocolError, + RequestError, + TimeoutError, + UserAuthenticationExpiredError, +}; diff --git a/core/src/headers.ts b/core/src/headers.ts index 07fddbc3..5e7f73b5 100644 --- a/core/src/headers.ts +++ b/core/src/headers.ts @@ -17,7 +17,8 @@ import { TD, TE } from "./encoders.ts"; import type { MsgHdrs } from "./core.ts"; -import { ErrorCode, Match, NatsError } from "./core.ts"; +import { Match } from "./core.ts"; +import { InvalidArgumentError } from "./errors.ts"; // https://www.ietf.org/rfc/rfc822.txt // 3.1.2. STRUCTURE OF HEADER FIELDS @@ -47,9 +48,9 @@ export function canonicalMIMEHeaderKey(k: string): string { for (let i = 0; i < k.length; i++) { let c = k.charCodeAt(i); if (c === colon || c < start || c > end) { - throw new NatsError( - `'${k[i]}' is not a valid character for a header key`, - ErrorCode.BadHeader, + throw InvalidArgumentError.format( + "header", + `'${k[i]}' is not a valid character in a header name`, ); } if (upper && a <= c && c <= z) { @@ -65,7 +66,7 @@ export function canonicalMIMEHeaderKey(k: string): string { export function headers(code = 0, description = ""): MsgHdrs { if ((code === 0 && description !== "") || (code > 0 && description === "")) { - throw new Error("setting status requires both code and description"); + throw InvalidArgumentError.format("description", "is required"); } return new MsgHdrsImpl(code, description); } @@ -170,9 +171,9 @@ export class MsgHdrsImpl implements MsgHdrs { static validHeaderValue(k: string): string { const inv = /[\r\n]/; if (inv.test(k)) { - throw new NatsError( - "invalid header value - \\r and \\n are not allowed.", - ErrorCode.BadHeader, + throw InvalidArgumentError.format( + "header", + "values cannot contain \\r or \\n", ); } return k.trim(); diff --git a/core/src/internal_mod.ts b/core/src/internal_mod.ts index 8d801642..fd6f48ee 100644 --- a/core/src/internal_mod.ts +++ b/core/src/internal_mod.ts @@ -81,7 +81,6 @@ export { Empty } from "./types.ts"; export { extractProtocolMessage, protoLen } from "./transport.ts"; export type { - ApiError, Auth, Authenticator, CallbackFn, @@ -119,11 +118,8 @@ export type { export { createInbox, DebugEvents, - ErrorCode, Events, - isNatsError, Match, - NatsError, RequestStrategy, syncIterator, } from "./core.ts"; @@ -142,3 +138,20 @@ export { Base64Codec, Base64UrlCodec, Base64UrlPaddedCodec } from "./base64.ts"; export { SHA256 } from "./sha256.ts"; export { wsconnect, wsUrlParseFn } from "./ws_transport.ts"; + +export { + AuthorizationError, + ClosedConnectionError, + ConnectionError, + DrainingConnectionError, + errors, + InvalidArgumentError, + InvalidOperationError, + InvalidSubjectError, + NoRespondersError, + PermissionViolationError, + ProtocolError, + RequestError, + TimeoutError, + UserAuthenticationExpiredError, +} from "./errors.ts"; diff --git a/core/src/mod.ts b/core/src/mod.ts index 70986628..db81c3aa 100644 --- a/core/src/mod.ts +++ b/core/src/mod.ts @@ -14,40 +14,51 @@ */ export { + AuthorizationError, backoff, Bench, buildAuthenticator, canonicalMIMEHeaderKey, + ClosedConnectionError, + ConnectionError, createInbox, credsAuthenticator, deadline, DebugEvents, deferred, delay, + DrainingConnectionError, Empty, - ErrorCode, + errors, Events, headers, + InvalidArgumentError, + InvalidOperationError, + InvalidSubjectError, jwtAuthenticator, Match, Metric, millis, MsgHdrsImpl, nanos, - NatsError, nkeyAuthenticator, nkeys, + NoRespondersError, Nuid, nuid, + PermissionViolationError, + ProtocolError, + RequestError, RequestStrategy, syncIterator, + TimeoutError, tokenAuthenticator, + UserAuthenticationExpiredError, usernamePasswordAuthenticator, wsconnect, } from "./internal_mod.ts"; export type { - ApiError, Auth, Authenticator, Backoff, diff --git a/core/src/msg.ts b/core/src/msg.ts index 9d5aef65..8689e075 100644 --- a/core/src/msg.ts +++ b/core/src/msg.ts @@ -23,17 +23,6 @@ import type { RequestInfo, ReviverFn, } from "./core.ts"; -import { ErrorCode, NatsError } from "./core.ts"; - -export function isRequestError(msg: Msg): NatsError | null { - // NATS core only considers errors 503s on messages that have no payload - // everything else simply forwarded as part of the message and is considered - // application level information - if (msg && msg.data.length === 0 && msg.headers?.code === 503) { - return NatsError.errorForCode(ErrorCode.NoResponders); - } - return null; -} export class MsgImpl implements Msg { _headers?: MsgHdrs; diff --git a/core/src/muxsubscription.ts b/core/src/muxsubscription.ts index f49bef73..7469f899 100644 --- a/core/src/muxsubscription.ts +++ b/core/src/muxsubscription.ts @@ -12,9 +12,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { isRequestError } from "./msg.ts"; import type { Msg, MsgCallback, Request } from "./core.ts"; -import { createInbox, ErrorCode, NatsError } from "./core.ts"; +import { createInbox } from "./core.ts"; +import { NoRespondersError, RequestError } from "./errors.ts"; + +import type { PermissionViolationError } from "./errors.ts"; export class MuxSubscription { baseInbox!: string; @@ -60,37 +62,39 @@ export class MuxSubscription { return Array.from(this.reqs.values()); } - handleError(isMuxPermissionError: boolean, err?: NatsError): boolean { - if (err && err.permissionContext) { - if (isMuxPermissionError) { - // one or more requests queued but mux cannot process them - this.all().forEach((r) => { - r.resolver(err, {} as Msg); - }); + handleError( + isMuxPermissionError: boolean, + err: PermissionViolationError, + ): boolean { + if (isMuxPermissionError) { + // one or more requests queued but mux cannot process them + this.all().forEach((r) => { + r.resolver(err, {} as Msg); + }); + return true; + } + if (err.operation === "publish") { + const req = this.all().find((s) => { + return s.requestSubject === err.subject; + }); + if (req) { + req.resolver(err, {} as Msg); return true; } - const ctx = err.permissionContext; - if (ctx.operation === "publish") { - const req = this.all().find((s) => { - return s.requestSubject === ctx.subject; - }); - if (req) { - req.resolver(err, {} as Msg); - return true; - } - } } return false; } dispatcher(): MsgCallback { - return (err: NatsError | null, m: Msg) => { + return (err: Error | null, m: Msg) => { const token = this.getToken(m); if (token) { const r = this.get(token); if (r) { - if (err === null && m.headers) { - err = isRequestError(m); + if (err === null) { + err = (m?.data?.length === 0 && m.headers?.code === 503) + ? new NoRespondersError(r.requestSubject) + : null; } r.resolver(err, m); } @@ -99,7 +103,7 @@ export class MuxSubscription { } close() { - const err = NatsError.errorForCode(ErrorCode.Timeout); + const err = new RequestError("connection closed"); this.reqs.forEach((req) => { req.resolver(err, {} as Msg); }); diff --git a/core/src/nats.ts b/core/src/nats.ts index 23b52333..defa7df1 100644 --- a/core/src/nats.ts +++ b/core/src/nats.ts @@ -16,24 +16,19 @@ import { deferred } from "./util.ts"; import { ProtocolHandler, SubscriptionImpl } from "./protocol.ts"; import { Empty } from "./encoders.ts"; -import { NatsError } from "./types.ts"; import type { Features, SemVer } from "./semver.ts"; import { parseSemVer } from "./semver.ts"; import { parseOptions } from "./options.ts"; import { QueuedIteratorImpl } from "./queued_iterator.ts"; -import { RequestMany, RequestOne } from "./request.ts"; - import type { RequestManyOptionsInternal } from "./request.ts"; - -import { isRequestError } from "./msg.ts"; -import { createInbox, ErrorCode, RequestStrategy } from "./core.ts"; -import type { Dispatcher } from "./core.ts"; +import { RequestMany, RequestOne } from "./request.ts"; import type { ConnectionOptions, Context, + Dispatcher, Msg, NatsConnection, Payload, @@ -47,6 +42,8 @@ import type { Subscription, SubscriptionOptions, } from "./core.ts"; +import { createInbox, RequestStrategy } from "./core.ts"; +import { errors, InvalidArgumentError, TimeoutError } from "./errors.ts"; export class NatsConnectionImpl implements NatsConnection { options: ConnectionOptions; @@ -91,17 +88,17 @@ export class NatsConnectionImpl implements NatsConnection { _check(subject: string, sub: boolean, pub: boolean) { if (this.isClosed()) { - throw NatsError.errorForCode(ErrorCode.ConnectionClosed); + throw new errors.ClosedConnectionError(); } if (sub && this.isDraining()) { - throw NatsError.errorForCode(ErrorCode.ConnectionDraining); + throw new errors.DrainingConnectionError(); } if (pub && this.protocol.noMorePublishing) { - throw NatsError.errorForCode(ErrorCode.ConnectionDraining); + throw new errors.DrainingConnectionError(); } subject = subject || ""; if (subject.length === 0) { - throw NatsError.errorForCode(ErrorCode.BadSubject); + throw new errors.InvalidSubjectError(subject); } } @@ -111,6 +108,9 @@ export class NatsConnectionImpl implements NatsConnection { options?: PublishOptions, ): void { this._check(subject, false, true); + if (options?.reply) { + this._check(options.reply, false, true); + } this.protocol.publish(subject, data, options); } @@ -185,7 +185,9 @@ export class NatsConnectionImpl implements NatsConnection { opts.strategy = opts.strategy || RequestStrategy.Timer; opts.maxWait = opts.maxWait || 1000; if (opts.maxWait < 1) { - return Promise.reject(new NatsError("timeout", ErrorCode.InvalidOption)); + return Promise.reject( + InvalidArgumentError.format("timeout", "must be greater than 0"), + ); } // the iterator for user results @@ -218,9 +220,9 @@ export class NatsConnectionImpl implements NatsConnection { // we only expect runtime errors or a no responders if ( msg?.data?.length === 0 && - msg?.headers?.status === ErrorCode.NoResponders + msg?.headers?.status === "503" ) { - err = NatsError.errorForCode(ErrorCode.NoResponders); + err = new errors.NoRespondersError(subject); } // augment any error with the current stack to provide context // for the error on the suer code @@ -292,7 +294,7 @@ export class NatsConnectionImpl implements NatsConnection { try { this.publish(subject, data, { reply: sub.getSubject() }); } catch (err) { - cancel(err as NatsError); + cancel(err as Error); } let timer = setTimeout(() => { @@ -328,7 +330,7 @@ export class NatsConnectionImpl implements NatsConnection { }, ); } catch (err) { - r.cancel(err as NatsError); + r.cancel(err as Error); } } @@ -348,13 +350,15 @@ export class NatsConnectionImpl implements NatsConnection { const asyncTraces = !(this.protocol.options.noAsyncTraces || false); opts.timeout = opts.timeout || 1000; if (opts.timeout < 1) { - return Promise.reject(new NatsError("timeout", ErrorCode.InvalidOption)); + return Promise.reject( + InvalidArgumentError.format("timeout", `must be greater than 0`), + ); } if (!opts.noMux && opts.reply) { return Promise.reject( - new NatsError( - "reply can only be used with noMux", - ErrorCode.InvalidOption, + InvalidArgumentError.format( + ["reply", "noMux"], + "are mutually exclusive", ), ); } @@ -364,31 +368,32 @@ export class NatsConnectionImpl implements NatsConnection { ? opts.reply : createInbox(this.options.inboxPrefix); const d = deferred(); - const errCtx = asyncTraces ? new Error() : null; + const errCtx = asyncTraces ? new errors.RequestError("") : null; const sub = this.subscribe( inbox, { max: 1, timeout: opts.timeout, callback: (err, msg) => { + // check for no responders status + if (msg && msg.data?.length === 0 && msg.headers?.code === 503) { + err = new errors.NoRespondersError(subject); + } if (err) { - // timeouts from `timeout()` will have the proper stack - if (errCtx && err.code !== ErrorCode.Timeout) { - err.stack += `\n\n${errCtx.stack}`; - } - sub.unsubscribe(); - d.reject(err); - } else { - err = isRequestError(msg); - if (err) { - // if we failed here, help the developer by showing what failed + // we have a proper stack on timeout + if (!(err instanceof TimeoutError)) { if (errCtx) { - err.stack += `\n\n${errCtx.stack}`; + errCtx.message = err.message; + errCtx.cause = err; + err = errCtx; + } else { + err = new errors.RequestError(err.message, { cause: err }); } - d.reject(err); - } else { - d.resolve(msg); } + d.reject(err); + sub.unsubscribe(); + } else { + d.resolve(msg); } }, }, @@ -418,7 +423,7 @@ export class NatsConnectionImpl implements NatsConnection { }, ); } catch (err) { - r.cancel(err as NatsError); + r.cancel(err as Error); } const p = Promise.race([r.timer, r.deferred]); @@ -435,23 +440,17 @@ export class NatsConnectionImpl implements NatsConnection { */ flush(): Promise { if (this.isClosed()) { - return Promise.reject( - NatsError.errorForCode(ErrorCode.ConnectionClosed), - ); + return Promise.reject(new errors.ClosedConnectionError()); } return this.protocol.flush(); } drain(): Promise { if (this.isClosed()) { - return Promise.reject( - NatsError.errorForCode(ErrorCode.ConnectionClosed), - ); + return Promise.reject(new errors.ClosedConnectionError()); } if (this.isDraining()) { - return Promise.reject( - NatsError.errorForCode(ErrorCode.ConnectionDraining), - ); + return Promise.reject(new errors.DrainingConnectionError()); } this.draining = true; return this.protocol.drain(); @@ -509,8 +508,11 @@ export class NatsConnectionImpl implements NatsConnection { } async rtt(): Promise { - if (!this.protocol._closed && !this.protocol.connected) { - throw NatsError.errorForCode(ErrorCode.Disconnect); + if (this.isClosed()) { + throw new errors.ClosedConnectionError(); + } + if (!this.protocol.connected) { + throw new errors.RequestError("connection disconnected"); } const start = Date.now(); await this.flush(); @@ -523,14 +525,10 @@ export class NatsConnectionImpl implements NatsConnection { reconnect(): Promise { if (this.isClosed()) { - return Promise.reject( - NatsError.errorForCode(ErrorCode.ConnectionClosed), - ); + return Promise.reject(new errors.ClosedConnectionError()); } if (this.isDraining()) { - return Promise.reject( - NatsError.errorForCode(ErrorCode.ConnectionDraining), - ); + return Promise.reject(new errors.DrainingConnectionError()); } return this.protocol.reconnect(); } diff --git a/core/src/options.ts b/core/src/options.ts index 62388f03..a10eb009 100644 --- a/core/src/options.ts +++ b/core/src/options.ts @@ -16,13 +16,14 @@ import { extend } from "./util.ts"; import { defaultPort, getResolveFn } from "./transport.ts"; import type { Authenticator, ConnectionOptions, ServerInfo } from "./core.ts"; -import { createInbox, DEFAULT_HOST, ErrorCode, NatsError } from "./core.ts"; +import { createInbox, DEFAULT_HOST } from "./core.ts"; import { multiAuthenticator, noAuthFn, tokenAuthenticator, usernamePasswordAuthenticator, } from "./authenticator.ts"; +import { errors, InvalidArgumentError } from "./errors.ts"; export const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10; export const DEFAULT_JITTER = 100; @@ -83,9 +84,9 @@ export function parseOptions(opts?: ConnectionOptions): ConnectionOptions { } if (opts.servers.length > 0 && opts.port) { - throw new NatsError( - "port and servers options are mutually exclusive", - ErrorCode.InvalidOption, + throw InvalidArgumentError.format( + ["servers", "port"], + "are mutually exclusive", ); } @@ -101,10 +102,7 @@ export function parseOptions(opts?: ConnectionOptions): ConnectionOptions { ["reconnectDelayHandler", "authenticator"].forEach((n) => { if (options[n] && typeof options[n] !== "function") { - throw new NatsError( - `${n} option should be a function`, - ErrorCode.NotFunction, - ); + throw TypeError(`'${n}' must be a function`); } }); @@ -122,11 +120,7 @@ export function parseOptions(opts?: ConnectionOptions): ConnectionOptions { } if (options.inboxPrefix) { - try { - createInbox(options.inboxPrefix); - } catch (err) { - throw new NatsError((err as Error).message, ErrorCode.ApiError); - } + createInbox(options.inboxPrefix); } // if not set - we set it @@ -137,9 +131,9 @@ export function parseOptions(opts?: ConnectionOptions): ConnectionOptions { if (options.resolve) { if (typeof getResolveFn() !== "function") { - throw new NatsError( - `'resolve' is not supported on this client`, - ErrorCode.InvalidOption, + throw InvalidArgumentError.format( + "resolve", + "is not supported in the current runtime", ); } } @@ -151,16 +145,16 @@ export function checkOptions(info: ServerInfo, options: ConnectionOptions) { const { proto, tls_required: tlsRequired, tls_available: tlsAvailable } = info; if ((proto === undefined || proto < 1) && options.noEcho) { - throw new NatsError("noEcho", ErrorCode.ServerOptionNotAvailable); + throw new errors.ConnectionError(`server does not support 'noEcho'`); } const tls = tlsRequired || tlsAvailable || false; if (options.tls && !tls) { - throw new NatsError("tls", ErrorCode.ServerOptionNotAvailable); + throw new errors.ConnectionError(`server does not support 'tls'`); } } -export function checkUnsupportedOption(prop: string, v?: string) { +export function checkUnsupportedOption(prop: string, v?: unknown) { if (v) { - throw new NatsError(prop, ErrorCode.InvalidOption); + throw InvalidArgumentError.format(prop, "is not supported"); } } diff --git a/core/src/protocol.ts b/core/src/protocol.ts index 8d96ffff..61f18c97 100644 --- a/core/src/protocol.ts +++ b/core/src/protocol.ts @@ -13,24 +13,22 @@ * limitations under the License. */ import { decode, Empty, encode, TE } from "./encoders.ts"; -import { CR_LF, CRLF, getResolveFn, newTransport } from "./transport.ts"; import type { Transport } from "./transport.ts"; -import { deferred, delay, extend, timeout } from "./util.ts"; +import { CR_LF, CRLF, getResolveFn, newTransport } from "./transport.ts"; import type { Deferred, Timeout } from "./util.ts"; +import { deferred, delay, extend, timeout } from "./util.ts"; import { DataBuffer } from "./databuffer.ts"; -import { Servers } from "./servers.ts"; import type { ServerImpl } from "./servers.ts"; +import { Servers } from "./servers.ts"; import { QueuedIteratorImpl } from "./queued_iterator.ts"; import type { MsgHdrsImpl } from "./headers.ts"; import { MuxSubscription } from "./muxsubscription.ts"; -import { Heartbeat } from "./heartbeats.ts"; import type { PH } from "./heartbeats.ts"; +import { Heartbeat } from "./heartbeats.ts"; import type { MsgArg, ParserEvent } from "./parser.ts"; import { Kind, Parser } from "./parser.ts"; import { MsgImpl } from "./msg.ts"; import { Features, parseSemVer } from "./semver.ts"; -import { DebugEvents, ErrorCode, Events, NatsError } from "./core.ts"; - import type { ConnectionOptions, Dispatcher, @@ -45,12 +43,20 @@ import type { Subscription, SubscriptionOptions, } from "./core.ts"; +import { DebugEvents, Events } from "./core.ts"; import { DEFAULT_MAX_PING_OUT, DEFAULT_PING_INTERVAL, DEFAULT_RECONNECT_TIME_WAIT, } from "./options.ts"; +import { errors, InvalidArgumentError } from "./errors.ts"; + +import type { + AuthorizationError, + PermissionViolationError, + UserAuthenticationExpiredError, +} from "./errors.ts"; const FLUSH_THRESHOLD = 1024 * 32; @@ -154,7 +160,7 @@ export class SubscriptionImpl extends QueuedIteratorImpl } } - callback(err: NatsError | null, msg: Msg) { + callback(err: Error | null, msg: Msg) { this.cancelTimeout(); err ? this.stop(err) : this.push(msg); } @@ -195,10 +201,12 @@ export class SubscriptionImpl extends QueuedIteratorImpl drain(): Promise { if (this.protocol.isClosed()) { - return Promise.reject(NatsError.errorForCode(ErrorCode.ConnectionClosed)); + return Promise.reject(new errors.ClosedConnectionError()); } if (this.isClosed()) { - return Promise.reject(NatsError.errorForCode(ErrorCode.SubClosed)); + return Promise.reject( + new errors.InvalidOperationError("subscription is already closed"), + ); } if (!this.drained) { this.draining = true; @@ -289,28 +297,26 @@ export class Subscriptions { } } - handleError(err?: NatsError): boolean { - if (err && err.permissionContext) { - const ctx = err.permissionContext; - const subs = this.all(); - let sub; - if (ctx.operation === "subscription") { - sub = subs.find((s) => { - return s.subject === ctx.subject && s.queue === ctx.queue; - }); - } else if (ctx.operation === "publish") { - // we have a no mux subscription - sub = subs.find((s) => { - return s.requestSubject === ctx.subject; - }); - } - if (sub) { - sub.callback(err, {} as Msg); - sub.close(); - this.subs.delete(sub.sid); - return sub !== this.mux; - } + handleError(err: PermissionViolationError): boolean { + const subs = this.all(); + let sub; + if (err.operation === "subscription") { + sub = subs.find((s) => { + return s.subject === err.subject && s.queue === err.queue; + }); + } else if (err.operation === "publish") { + // we have a no mux subscription + sub = subs.find((s) => { + return s.requestSubject === err.subject; + }); } + if (sub) { + sub.callback(err, {} as Msg); + sub.close(); + this.subs.delete(sub.sid); + return sub !== this.mux; + } + return false; } @@ -345,7 +351,7 @@ export class ProtocolHandler implements Dispatcher { outBytes: number; inBytes: number; pendingLimit: number; - lastError?: NatsError; + lastError?: Error; abortReconnect: boolean; whyClosed: string; @@ -402,7 +408,7 @@ export class ProtocolHandler implements Dispatcher { this.pongs = []; // reject the pongs - the disconnect from here shouldn't have a trace // because that confuses API consumers - const err = NatsError.errorForCode(ErrorCode.Disconnect); + const err = new errors.RequestError("connection disconnected"); err.stack = ""; pongs.forEach((p) => { p.reject(err); @@ -492,7 +498,7 @@ export class ProtocolHandler implements Dispatcher { // two of these, and the default for the client will be to // close, rather than attempt again - possibly they have an // authenticator that dynamically updates - if (this.lastError?.code === ErrorCode.AuthenticationExpired) { + if (this.lastError instanceof errors.UserAuthenticationExpiredError) { this.lastError = undefined; } }) @@ -607,7 +613,7 @@ export class ProtocolHandler implements Dispatcher { } else if (this.lastError) { throw this.lastError; } else { - throw NatsError.errorForCode(ErrorCode.ConnectionRefused); + throw new errors.ConnectionError("connection refused"); } } const now = Date.now(); @@ -646,40 +652,20 @@ export class ProtocolHandler implements Dispatcher { return h; } - static toError(s: string): NatsError { - const t = s ? s.toLowerCase() : ""; - if (t.indexOf("permissions violation") !== -1) { - const err = new NatsError(s, ErrorCode.PermissionsViolation); - const m = s.match(/(Publish|Subscription) to "(\S+)"/); - if (m) { - const operation = m[1].toLowerCase(); - const subject = m[2]; - let queue = undefined; - - if (operation === "subscription") { - const qm = s.match(/using queue "(\S+)"/); - if (qm) { - queue = qm[1]; - } - } - err.permissionContext = { - operation, - subject, - queue, - }; - } + static toError(s: string): Error { + let err: Error | null = errors.PermissionViolationError.parse(s); + if (err) { + return err; + } + err = errors.UserAuthenticationExpiredError.parse(s); + if (err) { + return err; + } + err = errors.AuthorizationError.parse(s); + if (err) { return err; - } else if (t.indexOf("authorization violation") !== -1) { - return new NatsError(s, ErrorCode.AuthorizationViolation); - } else if (t.indexOf("user authentication expired") !== -1) { - return new NatsError(s, ErrorCode.AuthenticationExpired); - } else if (t.indexOf("account authentication expired") != -1) { - return new NatsError(s, ErrorCode.AccountExpired); - } else if (t.indexOf("authentication timeout") !== -1) { - return new NatsError(s, ErrorCode.AuthenticationTimeout); - } else { - return new NatsError(s, ErrorCode.ProtocolError); } + return new errors.ProtocolError(s); } processMsg(msg: MsgArg, data: Uint8Array) { @@ -705,44 +691,47 @@ export class ProtocolHandler implements Dispatcher { } processError(m: Uint8Array) { - const s = decode(m); + let s = decode(m); + if (s.startsWith("'") && s.endsWith("'")) { + s = s.slice(1, s.length - 1); + } const err = ProtocolHandler.toError(s); - const status: Status = { type: Events.Error, data: err.code }; - if (err.isPermissionError()) { - let isMuxPermissionError = false; - if (err.permissionContext) { - status.permissionContext = err.permissionContext; + + switch (err.constructor) { + case errors.PermissionViolationError: { + const pe = err as PermissionViolationError; const mux = this.subscriptions.getMux(); - isMuxPermissionError = mux?.subject === err.permissionContext.subject; - } - this.subscriptions.handleError(err); - this.muxSubscriptions.handleError(isMuxPermissionError, err); - if (isMuxPermissionError) { - // remove the permission - enable it to be recreated - this.subscriptions.setMux(null); + const isMuxPermission = mux ? pe.subject === mux.subject : false; + this.subscriptions.handleError(pe); + this.muxSubscriptions.handleError(isMuxPermission, pe); + if (isMuxPermission) { + // remove the permission - enable it to be recreated + this.subscriptions.setMux(null); + } } } - this.dispatchStatus(status); + + this.dispatchStatus({ type: Events.Error, error: err, data: err.message }); this.handleError(err); } - handleError(err: NatsError) { - if (err.isAuthError()) { + handleError(err: Error) { + if ( + err instanceof errors.UserAuthenticationExpiredError || + err instanceof errors.AuthorizationError + ) { this.handleAuthError(err); - } else if (err.isProtocolError()) { - this.lastError = err; - } else if (err.isAuthTimeout()) { - this.lastError = err; } - // fallthrough here - if (!err.isPermissionError()) { + + if (!(err instanceof errors.PermissionViolationError)) { this.lastError = err; } } - handleAuthError(err: NatsError) { + handleAuthError(err: UserAuthenticationExpiredError | AuthorizationError) { if ( - (this.lastError && err.code === this.lastError.code) && + (this.lastError instanceof errors.UserAuthenticationExpiredError || + this.lastError instanceof errors.AuthorizationError) && this.options.ignoreAuthErrorAbort === false ) { this.abortReconnect = true; @@ -862,14 +851,16 @@ export class ProtocolHandler implements Dispatcher { subject: string, payload: Payload = Empty, options?: PublishOptions, - ) { + ): void { let data; if (payload instanceof Uint8Array) { data = payload; } else if (typeof payload === "string") { data = TE.encode(payload); } else { - throw NatsError.errorForCode(ErrorCode.BadPayload); + throw new TypeError( + "payload types can be strings or Uint8Array", + ); } let len = data.length; @@ -880,7 +871,10 @@ export class ProtocolHandler implements Dispatcher { let hlen = 0; if (options.headers) { if (this.info && !this.info.headers) { - throw new NatsError("headers", ErrorCode.ServerOptionNotAvailable); + InvalidArgumentError.format( + "headers", + "are not available on this server", + ); } const hdrs = options.headers as MsgHdrsImpl; headers = hdrs.encode(); @@ -889,7 +883,7 @@ export class ProtocolHandler implements Dispatcher { } if (this.info && len > this.info.max_payload) { - throw NatsError.errorForCode(ErrorCode.MaxPayloadExceeded); + throw InvalidArgumentError.format("payload", "max_payload size exceeded"); } this.outBytes += len; this.outMsgs++; @@ -940,14 +934,14 @@ export class ProtocolHandler implements Dispatcher { return s; } - unsubscribe(s: SubscriptionImpl, max?: number) { + unsubscribe(s: SubscriptionImpl, max?: number): void { this.unsub(s, max); if (s.max === undefined || s.received >= s.max) { this.subscriptions.cancel(s); } } - unsub(s: SubscriptionImpl, max?: number) { + unsub(s: SubscriptionImpl, max?: number): void { if (!s || this.isClosed()) { return; } @@ -959,7 +953,7 @@ export class ProtocolHandler implements Dispatcher { s.max = max; } - resub(s: SubscriptionImpl, subject: string) { + resub(s: SubscriptionImpl, subject: string): void { if (!s || this.isClosed()) { return; } @@ -981,7 +975,7 @@ export class ProtocolHandler implements Dispatcher { return p; } - sendSubscriptions() { + sendSubscriptions(): void { const cmds: string[] = []; this.subscriptions.all().forEach((s) => { const sub = s as SubscriptionImpl; @@ -1024,21 +1018,21 @@ export class ProtocolHandler implements Dispatcher { return this._closed; } - drain(): Promise { + async drain(): Promise { const subs = this.subscriptions.all(); const promises: Promise[] = []; subs.forEach((sub: Subscription) => { promises.push(sub.drain()); }); - return Promise.all(promises) - .then(async () => { - this.noMorePublishing = true; - await this.flush(); - return this.close(); - }) - .catch(() => { - // cannot happen - }); + try { + await Promise.allSettled(promises); + } catch { + // nothing we can do here + } finally { + this.noMorePublishing = true; + await this.flush(); + } + return this.close(); } private flushPending() { diff --git a/core/src/queued_iterator.ts b/core/src/queued_iterator.ts index e5d224a2..f7c45b5c 100644 --- a/core/src/queued_iterator.ts +++ b/core/src/queued_iterator.ts @@ -15,8 +15,8 @@ import type { Deferred } from "./util.ts"; import { deferred } from "./util.ts"; import type { QueuedIterator } from "./core.ts"; -import { ErrorCode, NatsError } from "./core.ts"; import type { CallbackFn, Dispatcher } from "./core.ts"; +import { InvalidOperationError } from "./errors.ts"; export class QueuedIteratorImpl implements QueuedIterator, Dispatcher { inflight: number; @@ -83,10 +83,12 @@ export class QueuedIteratorImpl implements QueuedIterator, Dispatcher { async *iterate(): AsyncIterableIterator { if (this.noIterator) { - throw new NatsError("unsupported iterator", ErrorCode.ApiError); + throw new InvalidOperationError( + "iterator cannot be used when a callback is registered", + ); } if (this.yielding) { - throw new NatsError("already yielding", ErrorCode.ApiError); + throw new InvalidOperationError("iterator is already yielding"); } this.yielding = true; try { diff --git a/core/src/request.ts b/core/src/request.ts index 97579224..2b1c4e24 100644 --- a/core/src/request.ts +++ b/core/src/request.ts @@ -22,12 +22,13 @@ import type { RequestManyOptions, RequestOptions, } from "./core.ts"; -import { ErrorCode, NatsError, RequestStrategy } from "./core.ts"; +import { RequestStrategy } from "./core.ts"; +import { errors, RequestError, TimeoutError } from "./errors.ts"; export class BaseRequest { token: string; received: number; - ctx?: Error; + ctx?: RequestError; requestSubject: string; mux: MuxSubscription; @@ -41,7 +42,7 @@ export class BaseRequest { this.received = 0; this.token = nuid.next(); if (asyncTraces) { - this.ctx = new Error(); + this.ctx = new RequestError(); } } } @@ -70,7 +71,7 @@ export class RequestMany extends BaseRequest implements Request { super(mux, requestSubject); this.opts = opts; if (typeof this.opts.callback !== "function") { - throw new Error("callback is required"); + throw new TypeError("callback must be a function"); } this.callback = this.opts.callback; @@ -87,7 +88,7 @@ export class RequestMany extends BaseRequest implements Request { }, opts.maxWait); } - cancel(err?: NatsError): void { + cancel(err?: Error): void { if (err) { this.callback(err, null); } @@ -101,7 +102,7 @@ export class RequestMany extends BaseRequest implements Request { if (this.ctx) { err.stack += `\n\n${this.ctx.stack}`; } - this.cancel(err as NatsError); + this.cancel(err as Error); } else { this.callback(null, msg); if (this.opts.strategy === RequestStrategy.Count) { @@ -149,8 +150,15 @@ export class RequestOne extends BaseRequest implements Request { this.timer.cancel(); } if (err) { - if (this.ctx) { - err.stack += `\n\n${this.ctx.stack}`; + // we have proper stack on timeout + if (!(err instanceof TimeoutError)) { + if (this.ctx) { + this.ctx.message = err.message; + this.ctx.cause = err; + err = this.ctx; + } else { + err = new errors.RequestError(err.message, { cause: err }); + } } this.deferred.reject(err); } else { @@ -159,13 +167,13 @@ export class RequestOne extends BaseRequest implements Request { this.cancel(); } - cancel(err?: NatsError): void { + cancel(err?: Error): void { if (this.timer) { this.timer.cancel(); } this.mux.cancel(this); this.deferred.reject( - err ? err : NatsError.errorForCode(ErrorCode.Cancelled), + err ? err : new RequestError("cancelled"), ); } } diff --git a/core/src/types.ts b/core/src/types.ts index 632ab30c..e1925a52 100644 --- a/core/src/types.ts +++ b/core/src/types.ts @@ -12,7 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { ApiError, Dispatcher, MsgHdrs, QueuedIterator } from "./core.ts"; -export { NatsError } from "./core.ts"; +export type { Dispatcher, MsgHdrs, QueuedIterator } from "./core.ts"; export { Empty } from "./encoders.ts"; diff --git a/core/src/util.ts b/core/src/util.ts index 37f66153..f2ea0786 100644 --- a/core/src/util.ts +++ b/core/src/util.ts @@ -15,7 +15,7 @@ // deno-lint-ignore-file no-explicit-any import { TD } from "./encoders.ts"; import type { Nanos } from "./core.ts"; -import { ErrorCode, NatsError } from "./core.ts"; +import { TimeoutError } from "./errors.ts"; export type ValueResult = { isError: false; @@ -67,7 +67,7 @@ export interface Timeout extends Promise { export function timeout(ms: number, asyncTraces = true): Timeout { // by generating the stack here to help identify what timed out - const err = asyncTraces ? NatsError.errorForCode(ErrorCode.Timeout) : null; + const err = asyncTraces ? new TimeoutError() : null; let methods; let timer: number; const p = new Promise((_resolve, reject) => { @@ -80,7 +80,7 @@ export function timeout(ms: number, asyncTraces = true): Timeout { // @ts-ignore: node is not a number timer = setTimeout(() => { if (err === null) { - reject(NatsError.errorForCode(ErrorCode.Timeout)); + reject(new TimeoutError()); } else { reject(err); } @@ -111,14 +111,19 @@ export function delay(ms = 0): Delay { return Object.assign(p, methods) as Delay; } -export function deadline(p: Promise, millis = 1000): Promise { - const err = new Error(`deadline exceeded`); +export async function deadline(p: Promise, millis = 1000): Promise { const d = deferred(); const timer = setTimeout( - () => d.reject(err), + () => { + d.reject(new TimeoutError()); + }, millis, ); - return Promise.race([p, d]).finally(() => clearTimeout(timer)); + try { + return await Promise.race([p, d]); + } finally { + clearTimeout(timer); + } } export interface Deferred extends Promise { diff --git a/core/src/version.ts b/core/src/version.ts index 14911460..837c22a5 100644 --- a/core/src/version.ts +++ b/core/src/version.ts @@ -1,2 +1,2 @@ // This file is generated - do not edit -export const version = "3.0.0-30"; +export const version = "3.0.0-34"; diff --git a/core/src/ws_transport.ts b/core/src/ws_transport.ts index ce1647c8..12aad856 100644 --- a/core/src/ws_transport.ts +++ b/core/src/ws_transport.ts @@ -19,7 +19,6 @@ import type { Server, ServerInfo, } from "./core.ts"; -import { ErrorCode, NatsError } from "./core.ts"; import type { Deferred } from "./util.ts"; import { deferred, delay, render } from "./util.ts"; import type { Transport, TransportFactory } from "./transport.ts"; @@ -29,6 +28,7 @@ import { DataBuffer } from "./databuffer.ts"; import { INFO } from "./protocol.ts"; import { NatsConnectionImpl } from "./nats.ts"; import { version } from "./version.ts"; +import { errors, InvalidArgumentError } from "./errors.ts"; const VERSION = version; const LANG = "nats.ws"; @@ -155,11 +155,7 @@ export class WsTransport implements Transport { return; } const evt = e as ErrorEvent; - const err = new NatsError( - evt.message, - ErrorCode.Unknown, - new Error(evt.error), - ); + const err = new errors.ConnectionError(evt.message); if (!connected) { ok.reject(err); } else { @@ -336,7 +332,10 @@ export function wsconnect( urlParseFn: wsUrlParseFn, factory: (): Transport => { if (opts.tls) { - throw new NatsError("tls", ErrorCode.InvalidOption); + throw InvalidArgumentError.format( + "tls", + "is not configurable on w3c websocket connections", + ); } return new WsTransport(); }, diff --git a/core/tests/auth_test.ts b/core/tests/auth_test.ts index 89c1dd1d..80a10769 100644 --- a/core/tests/auth_test.ts +++ b/core/tests/auth_test.ts @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { _setup, assertErrorCode, cleanup, NatsServer } from "test_helpers"; +import { _setup, cleanup, NatsServer } from "test_helpers"; import { assert, assertArrayIncludes, @@ -32,7 +32,6 @@ import type { MsgImpl, NatsConnection, NatsConnectionImpl, - NatsError, NKeyAuth, Status, UserPass, @@ -43,7 +42,6 @@ import { DEFAULT_MAX_RECONNECT_ATTEMPTS, deferred, Empty, - ErrorCode, Events, jwtAuthenticator, nkeyAuthenticator, @@ -51,6 +49,7 @@ import { tokenAuthenticator, usernamePasswordAuthenticator, } from "../src/internal_mod.ts"; +import { errors } from "../src/errors.ts"; const conf = { authorization: { @@ -67,29 +66,33 @@ const conf = { Deno.test("auth - none", async () => { const ns = await NatsServer.start(conf); - try { - const nc = await connect( - { port: ns.port }, - ); - await nc.close(); - fail("shouldnt have been able to connect"); - } catch (ex) { - assertErrorCode(ex as NatsError, ErrorCode.AuthorizationViolation); - } + + await assertRejects( + async () => { + const nc = await connect( + { port: ns.port }, + ); + await nc.close(); + fail("shouldnt have been able to connect"); + }, + errors.AuthorizationError, + ); + await ns.stop(); }); Deno.test("auth - bad", async () => { const ns = await NatsServer.start(conf); - try { - const nc = await connect( - { port: ns.port, user: "me", pass: "hello" }, - ); - await nc.close(); - fail("shouldnt have been able to connect"); - } catch (ex) { - assertErrorCode(ex as NatsError, ErrorCode.AuthorizationViolation); - } + await assertRejects( + async () => { + const nc = await connect( + { port: ns.port, user: "me", pass: "hello" }, + ); + await nc.close(); + fail("shouldnt have been able to connect"); + }, + errors.AuthorizationError, + ); await ns.stop(); }); @@ -159,11 +162,12 @@ Deno.test("auth - sub no permissions keeps connection", async () => { }); const v = await Promise.all([errStatus, cbErr, sub.closed]); - assertEquals(v[0].data, ErrorCode.PermissionsViolation); + assertEquals(v[0].data, `Permissions Violation for Subscription to "bar"`); assertEquals( v[1]?.message, - "'Permissions Violation for Subscription to \"bar\"'", + `Permissions Violation for Subscription to "bar"`, ); + assertEquals(nc.isClosed(), false); await cleanup(ns, nc); @@ -200,10 +204,13 @@ Deno.test("auth - sub iterator no permissions keeps connection", async () => { await nc.flush(); const v = await Promise.all([errStatus, iterErr, sub.closed]); - assertEquals(v[0].data, ErrorCode.PermissionsViolation); + assertEquals( + v[0].data, + `Permissions Violation for Subscription to "bar"`, + ); assertEquals( v[1]?.message, - "'Permissions Violation for Subscription to \"bar\"'", + `Permissions Violation for Subscription to "bar"`, ); assertEquals(sub.isClosed(), true); assertEquals(nc.isClosed(), false); @@ -232,7 +239,7 @@ Deno.test("auth - pub permissions keep connection", async () => { nc.publish("bar"); const v = await errStatus; - assertEquals(v.data, ErrorCode.PermissionsViolation); + assertEquals(v.data, `Permissions Violation for Publish to "bar"`); assertEquals(nc.isClosed(), false); await cleanup(ns, nc); @@ -256,15 +263,16 @@ Deno.test("auth - req permissions keep connection", async () => { } })().then(); - const err = await assertRejects( + await assertRejects( async () => { await nc.request("bar"); }, - ) as NatsError; - assertEquals(err.code, ErrorCode.PermissionsViolation); + errors.RequestError, + `Permissions Violation for Publish to "bar"`, + ); const v = await errStatus; - assertEquals(v.data, ErrorCode.PermissionsViolation); + assertEquals(v.data, `Permissions Violation for Publish to "bar"`); assertEquals(nc.isClosed(), false); await cleanup(ns, nc); @@ -436,18 +444,20 @@ Deno.test("auth - custom error", async () => { }); Deno.test("basics - bad auth", async () => { - try { - await connect( - { - servers: "connect.ngs.global", - waitOnFirstConnect: true, - user: "me", - pass: "you", - }, - ); - } catch (err) { - assertErrorCode(err as NatsError, ErrorCode.AuthorizationViolation); - } + await assertRejects( + () => { + return connect( + { + servers: "connect.ngs.global", + reconnect: false, + user: "me", + pass: "you", + }, + ); + }, + errors.AuthorizationError, + "Authorization Violation", + ); }); Deno.test("auth - nkey authentication", async () => { @@ -549,21 +559,19 @@ Deno.test("auth - expiration is notified", async () => { const U = nkeys.createUser(); const ujwt = await encodeUser("U", U, A, { bearer_token: true }, { - exp: Math.round(Date.now() / 1000) + 3, + exp: Math.round(Date.now() / 1000) + 5, }); const nc = await connect({ port: ns.port, - maxReconnectAttempts: -1, + reconnect: false, authenticator: jwtAuthenticator(ujwt), }); let authErrors = 0; (async () => { for await (const s of nc.status()) { - if ( - s.type === Events.Error && s.data === ErrorCode.AuthenticationExpired - ) { + if (s.error instanceof errors.UserAuthenticationExpiredError) { authErrors++; } } @@ -571,7 +579,8 @@ Deno.test("auth - expiration is notified", async () => { const err = await nc.closed(); assert(authErrors >= 1); - assertErrorCode(err!, ErrorCode.AuthenticationExpired); + assertExists(err); + assert(err instanceof errors.UserAuthenticationExpiredError, err?.message); await cleanup(ns); }); @@ -628,7 +637,7 @@ Deno.test("auth - expiration is notified and recovered", async () => { } break; case Events.Error: - if (s.data === ErrorCode.AuthenticationExpired) { + if (s.error instanceof errors.UserAuthenticationExpiredError) { authErrors++; } break; @@ -646,7 +655,7 @@ Deno.test("auth - expiration is notified and recovered", async () => { }); Deno.test("auth - bad auth is notified", async () => { - let ns = await NatsServer.start(conf); + const ns = await NatsServer.start(conf); let count = 0; @@ -664,19 +673,18 @@ Deno.test("auth - bad auth is notified", async () => { (async () => { for await (const s of nc.status()) { if ( - s.type === Events.Error && s.data === ErrorCode.AuthorizationViolation + s.type === Events.Error && s.error instanceof errors.AuthorizationError ) { badAuths++; } } })().then(); - await ns.stop(); - ns = await ns.restart(); + await nc.reconnect(); const err = await nc.closed(); assert(badAuths > 1); - assertErrorCode(err!, ErrorCode.AuthorizationViolation); + assert(err instanceof errors.AuthorizationError); await ns.stop(); }); @@ -721,32 +729,19 @@ Deno.test("auth - perm request error", async () => { const status = deferred(); (async () => { for await (const s of nc.status()) { - if ( - s.permissionContext?.operation === "publish" && - s.permissionContext?.subject === "q" - ) { - status.resolve(s); + if (s.error instanceof errors.PermissionViolationError) { + if (s.error.operation === "publish" && s.error.subject === "q") { + status.resolve(s); + } } } })().then(); - const response = deferred(); - nc.request("q") - .catch((err) => { - response.resolve(err); - }); - - const [r, s] = await Promise.all([response, status]); - assertErrorCode(r, ErrorCode.PermissionsViolation); - const ne = r as NatsError; - assertEquals(ne.permissionContext?.operation, "publish"); - assertEquals(ne.permissionContext?.subject, "q"); - - assertEquals(s.type, Events.Error); - assertEquals(s.data, ErrorCode.PermissionsViolation); - assertEquals(s.permissionContext?.operation, "publish"); - assertEquals(s.permissionContext?.subject, "q"); + assertRejects(() => { + return nc.request("q"); + }, errors.RequestError); + await status; await cleanup(ns, nc, sc); }); @@ -790,31 +785,21 @@ Deno.test("auth - perm request error no mux", async () => { const status = deferred(); (async () => { for await (const s of nc.status()) { - if ( - s.permissionContext?.operation === "publish" && - s.permissionContext?.subject === "q" - ) { - status.resolve(s); + if (s.error instanceof errors.PermissionViolationError) { + if (s.error.operation === "publish" && s.error.subject === "q") { + status.resolve(s); + } } } })().then(); - const response = deferred(); - nc.request("q", Empty, { noMux: true, timeout: 1000 }) - .catch((err) => { - response.resolve(err); - }); - - const [r, s] = await Promise.all([response, status]); - assertErrorCode(r, ErrorCode.PermissionsViolation); - const ne = r as NatsError; - assertEquals(ne.permissionContext?.operation, "publish"); - assertEquals(ne.permissionContext?.subject, "q"); - - assertEquals(s.type, Events.Error); - assertEquals(s.data, ErrorCode.PermissionsViolation); - assertEquals(s.permissionContext?.operation, "publish"); - assertEquals(s.permissionContext?.subject, "q"); + await assertRejects( + () => { + return nc.request("q", Empty, { noMux: true, timeout: 1000 }); + }, + errors.RequestError, + "q", + ); await cleanup(ns, nc, sc); }); @@ -859,11 +844,10 @@ Deno.test("auth - perm request error deliver to sub", async () => { const status = deferred(); (async () => { for await (const s of nc.status()) { - if ( - s.permissionContext?.operation === "publish" && - s.permissionContext?.subject === "q" - ) { - status.resolve(s); + if (s.error instanceof errors.PermissionViolationError) { + if (s.error.subject === "q" && s.error.operation === "publish") { + status.resolve(); + } } } })().then(); @@ -874,22 +858,17 @@ Deno.test("auth - perm request error deliver to sub", async () => { }, }); - const response = deferred(); - nc.request("q", Empty, { noMux: true, reply: inbox, timeout: 1000 }) - .catch((err) => { - response.resolve(err); - }); - - const [r, s] = await Promise.all([response, status]); - assertErrorCode(r, ErrorCode.PermissionsViolation); - const ne = r as NatsError; - assertEquals(ne.permissionContext?.operation, "publish"); - assertEquals(ne.permissionContext?.subject, "q"); - - assertEquals(s.type, Events.Error); - assertEquals(s.data, ErrorCode.PermissionsViolation); - assertEquals(s.permissionContext?.operation, "publish"); - assertEquals(s.permissionContext?.subject, "q"); + 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); @@ -932,13 +911,14 @@ Deno.test("auth - mux sub ok", async () => { }); await sc.flush(); - const response = deferred(); - nc.request("q") - .catch((err) => { - response.resolve(err); - }); - const ne = await response as NatsError; - assertEquals(ne.permissionContext?.operation, "subscription"); + await assertRejects( + () => { + return nc.request("q"); + }, + errors.RequestError, + "Permissions Violation for Subscription", + ); + //@ts-ignore: test assertEquals(nc.protocol.subscriptions.getMux(), null); @@ -1001,34 +981,24 @@ Deno.test("auth - perm sub iterator error", async () => { const status = deferred(); (async () => { for await (const s of nc.status()) { - if ( - s.permissionContext?.operation === "subscription" && - s.permissionContext?.subject === "q" - ) { - status.resolve(s); + if (s.error instanceof errors.PermissionViolationError) { + if (s.error.subject === "q" && s.error.operation === "publish") { + status.resolve(s); + } } } })().then(); const sub = nc.subscribe("q"); - const iterReject = deferred(); - (async () => { - for await (const _m of sub) { - // ignored - } - })().catch((err) => { - iterReject.resolve(err as NatsError); - }); - - const [s, i] = await Promise.all([status, iterReject]); - assertEquals(s.type, Events.Error); - assertEquals(s.data, ErrorCode.PermissionsViolation); - assertEquals(s.permissionContext?.operation, "subscription"); - assertEquals(s.permissionContext?.subject, "q"); - - assertEquals(i.code, ErrorCode.PermissionsViolation); - assertEquals(i.permissionContext?.operation, "subscription"); - assertEquals(i.permissionContext?.subject, "q"); + await assertRejects( + async () => { + for await (const _m of sub) { + // ignored + } + }, + errors.PermissionViolationError, + `Permissions Violation for Subscription to "q"`, + ); await cleanup(ns, nc); }); @@ -1051,7 +1021,7 @@ Deno.test("auth - perm error is not in lastError", async () => { const nci = nc as NatsConnectionImpl; assertEquals(nci.protocol.lastError, undefined); - const d = deferred(); + const d = deferred(); nc.subscribe("q", { callback: (err) => { d.resolve(err); @@ -1060,7 +1030,7 @@ Deno.test("auth - perm error is not in lastError", async () => { const err = await d; assert(err !== null); - assertEquals(err?.isPermissionError(), true); + assert(err instanceof errors.PermissionViolationError); assert(nci.protocol.lastError === undefined); await cleanup(ns, nc); @@ -1090,7 +1060,7 @@ Deno.test("auth - ignore auth error abort", async () => { let count = 0; (async () => { for await (const s of nc.status()) { - if (s.type === "error" && s.data === "AUTHORIZATION_VIOLATION") { + if (s.error instanceof errors.AuthorizationError) { count++; } } @@ -1247,7 +1217,6 @@ Deno.test("auth - request context", async () => { }); const a = await connect({ user: "a", pass: "a", port: ns.port }); - console.log(await (a as NatsConnectionImpl).context()); await a.request("q.hello"); await cleanup(ns, nc, a); @@ -1276,7 +1245,7 @@ Deno.test("auth - sub queue permission", async () => { }, }); - const qBad = deferred(); + const qBad = deferred(); nc.subscribe("q", { queue: "bad", callback: (err, _msg) => { @@ -1292,7 +1261,7 @@ Deno.test("auth - sub queue permission", async () => { await qA; - assertEquals(err.code, ErrorCode.PermissionsViolation); + assert(err instanceof errors.PermissionViolationError); assertStringIncludes(err.message, 'using queue "bad"'); await cleanup(ns, nc); }); @@ -1319,7 +1288,6 @@ Deno.test("auth - account expired", async () => { const ujwt = await encodeUser("U", U, A, { bearer_token: true }); const { ns, nc } = await _setup(connect, conf, { - debug: true, reconnect: false, authenticator: jwtAuthenticator(ujwt), }); @@ -1327,7 +1295,10 @@ Deno.test("auth - account expired", async () => { const d = deferred(); (async () => { for await (const s of nc.status()) { - if (s.type === Events.Error && s.data === ErrorCode.AccountExpired) { + if ( + s.error instanceof errors.AuthorizationError && + s.data === "Account Authentication Expired" + ) { d.resolve(); break; } @@ -1336,7 +1307,8 @@ Deno.test("auth - account expired", async () => { const w = await nc.closed(); assertExists(w); - assertEquals((w as NatsError).code, ErrorCode.AccountExpired); + assert(w instanceof errors.AuthorizationError); + assertEquals(w.message, "Account Authentication Expired"); await cleanup(ns, nc); }); diff --git a/core/tests/authenticator_test.ts b/core/tests/authenticator_test.ts index 93895080..41e03ab4 100644 --- a/core/tests/authenticator_test.ts +++ b/core/tests/authenticator_test.ts @@ -35,7 +35,7 @@ import type { NatsConnectionImpl, } from "../src/internal_mod.ts"; -import { assertEquals } from "jsr:@std/assert"; +import { assertEquals, assertThrows } from "jsr:@std/assert"; import { encodeAccount, encodeOperator, @@ -253,3 +253,13 @@ Deno.test("authenticator - creds fn", async () => { await testAuthenticatorFn(authenticator, conf); }); + +Deno.test("authenticator - bad creds", () => { + assertThrows( + () => { + credsAuthenticator(new TextEncoder().encode("hello"))(); + }, + Error, + "unable to parse credentials", + ); +}); diff --git a/core/tests/autounsub_test.ts b/core/tests/autounsub_test.ts index 353fe1b9..3cde9438 100644 --- a/core/tests/autounsub_test.ts +++ b/core/tests/autounsub_test.ts @@ -12,12 +12,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assert, assertEquals } from "jsr:@std/assert"; +import { assertEquals, assertRejects } from "jsr:@std/assert"; -import { createInbox, Empty, ErrorCode } from "../src/internal_mod.ts"; +import { createInbox, Empty, errors } from "../src/internal_mod.ts"; import type { NatsConnectionImpl, Subscription } from "../src/internal_mod.ts"; import { _setup, cleanup, Lock } from "test_helpers"; import { connect } from "./connect.ts"; +import { TimeoutError } from "../src/errors.ts"; Deno.test("autounsub - max option", async () => { const { ns, nc } = await _setup(connect); @@ -198,16 +199,47 @@ Deno.test("autounsub - check cancelled request leaks", async () => { assertEquals(nci.protocol.subscriptions.size(), 1); assertEquals(nci.protocol.muxSubscriptions.size(), 1); + await assertRejects( + () => { + return rp; + }, + errors.RequestError, + subj, + ); // the rejection should be timeout - const lock = Lock(); - rp.catch((rej) => { - assert( - rej?.code === ErrorCode.NoResponders || rej?.code === ErrorCode.Timeout, - ); - lock.unlock(); + + // 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(connect); + 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 + }, }); - await lock; + 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); diff --git a/core/tests/basics_test.ts b/core/tests/basics_test.ts index 38c16f78..aac4f38c 100644 --- a/core/tests/basics_test.ts +++ b/core/tests/basics_test.ts @@ -14,23 +14,20 @@ */ import { assert, - assertArrayIncludes, assertEquals, assertExists, + assertInstanceOf, assertRejects, assertThrows, fail, } from "jsr:@std/assert"; -import { assertThrowsAsyncErrorCode } from "../../test_helpers/asserts.ts"; - import { collect, createInbox, deferred, delay, Empty, - ErrorCode, Feature, headers, isIP, @@ -42,20 +39,14 @@ import type { Msg, MsgHdrs, NatsConnectionImpl, - NatsError, Payload, Publisher, PublishOptions, SubscriptionImpl, } from "../src/internal_mod.ts"; -import { - _setup, - assertErrorCode, - cleanup, - Lock, - NatsServer, -} from "test_helpers"; +import { _setup, cleanup, Lock, NatsServer } 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(); @@ -86,13 +77,13 @@ Deno.test("basics - connect servers", async () => { }); Deno.test("basics - fail connect", async () => { - await connect({ servers: `127.0.0.1:32001` }) - .then(() => { - fail(); - }) - .catch((err) => { - assertErrorCode(err, ErrorCode.ConnectionRefused); - }); + await assertRejects( + () => { + return connect({ servers: `127.0.0.1:32001` }); + }, + errors.ConnectionError, + "connection refused", + ); }); Deno.test("basics - publish", async () => { @@ -104,14 +95,14 @@ Deno.test("basics - publish", async () => { Deno.test("basics - no publish without subject", async () => { const { ns, nc } = await _setup(connect); - try { - nc.publish(""); - fail("should not be able to publish without a subject"); - } catch (err) { - assertEquals((err as NatsError).code, ErrorCode.BadSubject); - } finally { - await cleanup(ns, nc); - } + assertThrows( + () => { + nc.publish(""); + }, + errors.InvalidSubjectError, + "illegal subject: ''", + ); + await cleanup(ns, nc); }); Deno.test("basics - pubsub", async () => { @@ -370,10 +361,26 @@ Deno.test("basics - request", async () => { Deno.test("basics - request no responders", async () => { const { ns, nc } = await _setup(connect); - const s = createInbox(); - await assertThrowsAsyncErrorCode(async () => { - await nc.request(s, Empty, { timeout: 100 }); - }, ErrorCode.NoResponders); + 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(connect); + await assertRejects( + () => { + return nc.request("q", Empty, { timeout: 100, noMux: true }); + }, + errors.RequestError, + "no responders: 'q'", + ); await cleanup(ns, nc); }); @@ -381,9 +388,21 @@ Deno.test("basics - request timeout", async () => { const { ns, nc } = await _setup(connect); const s = createInbox(); nc.subscribe(s, { callback: () => {} }); - await assertThrowsAsyncErrorCode(async () => { - await nc.request(s, Empty, { timeout: 100 }); - }, ErrorCode.Timeout); + 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(connect); + const s = createInbox(); + nc.subscribe(s, { callback: () => {} }); + await assertRejects(() => { + return nc.request(s, Empty, { timeout: 100, noMux: true }); + }, errors.TimeoutError); + await cleanup(ns, nc); }); @@ -391,21 +410,20 @@ Deno.test("basics - request cancel rejects", async () => { const { ns, nc } = await _setup(connect); const nci = nc as NatsConnectionImpl; const s = createInbox(); - const lock = Lock(); - nc.request(s, Empty, { timeout: 1000 }) - .then(() => { - fail(); - }) - .catch((err) => { - assertEquals(err.code, ErrorCode.Cancelled); - lock.unlock(); - }); + const check = assertRejects( + () => { + return nc.request(s, Empty, { timeout: 1000 }); + }, + errors.RequestError, + "cancelled", + ); nci.protocol.muxSubscriptions.reqs.forEach((v) => { v.cancel(); }); - await lock; + + await check; await cleanup(ns, nc); }); @@ -428,7 +446,7 @@ Deno.test("basics - old style requests", async () => { await cleanup(ns, nc); }); -Deno.test("basics - request with custom subject", async () => { +Deno.test("basics - reply can only be used with noMux", async () => { const { ns, nc } = await _setup(connect); nc.subscribe("q", { callback: (_err, msg) => { @@ -436,18 +454,14 @@ Deno.test("basics - request with custom subject", async () => { }, }); - try { - await nc.request( - "q", - Empty, - { reply: "bar", timeout: 1000 }, - ); + await assertRejects( + () => { + return nc.request("q", Empty, { reply: "bar", timeout: 1000 }); + }, + errors.InvalidArgumentError, + "'reply','noMux' are mutually exclusive", + ); - fail("should have failed"); - } catch (err) { - const nerr = err as NatsError; - assertEquals(ErrorCode.InvalidOption, nerr.code); - } await cleanup(ns, nc); }); @@ -496,13 +510,12 @@ Deno.test("basics - request with headers and custom subject", async () => { Deno.test("basics - request requires a subject", async () => { const { ns, nc } = await _setup(connect); await assertRejects( - async () => { - //@ts-ignore: subject missing on purpose - await nc.request(); + () => { + //@ts-ignore: testing + return nc.request(); }, - Error, - "BAD_SUBJECT", - undefined, + errors.InvalidSubjectError, + "illegal subject: ''", ); await cleanup(ns, nc); }); @@ -511,28 +524,24 @@ Deno.test("basics - closed returns error", async () => { const { ns, nc } = await _setup(connect, {}, { reconnect: false }); setTimeout(() => { (nc as NatsConnectionImpl).protocol.sendCommand("Y\r\n"); - }, 1000); - await nc.closed() - .then((v) => { - assertEquals((v as NatsError).code, ErrorCode.ProtocolError); - }); - + }, 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(connect); - const lock = Lock(1); const sub = nc.subscribe(createInbox(), { max: 1, timeout: 250 }); - (async () => { - for await (const _m of sub) { - // ignored - } - })().catch((err) => { - assertErrorCode(err, ErrorCode.Timeout); - lock.unlock(); - }); - await lock; + await assertRejects( + async () => { + for await (const _m of sub) { + // ignored + } + }, + errors.TimeoutError, + "timeout", + ); await cleanup(ns, nc); }); @@ -592,20 +601,15 @@ Deno.test("basics - no mux requests create normal subs", async () => { Deno.test("basics - no mux requests timeout", async () => { const { ns, nc } = await _setup(connect); - const lock = Lock(); const subj = createInbox(); nc.subscribe(subj, { callback: () => {} }); + await assertRejects( + () => { + return nc.request(subj, Empty, { timeout: 500, noMux: true }); + }, + errors.TimeoutError, + ); - await nc.request( - subj, - Empty, - { timeout: 1000, noMux: true }, - ) - .catch((err) => { - assertErrorCode(err, ErrorCode.Timeout); - lock.unlock(); - }); - await lock; await cleanup(ns, nc); }); @@ -633,11 +637,10 @@ Deno.test("basics - no mux request timeout doesn't leak subs", async () => { assertEquals(nci.protocol.subscriptions.size(), 1); await assertRejects( - async () => { - await nc.request("q", Empty, { noMux: true, timeout: 1000 }); + () => { + return nc.request("q", Empty, { noMux: true, timeout: 1000 }); }, - Error, - "TIMEOUT", + errors.TimeoutError, ); assertEquals(nci.protocol.subscriptions.size(), 1); @@ -649,14 +652,9 @@ Deno.test("basics - no mux request no responders doesn't leak subs", async () => const nci = nc as NatsConnectionImpl; assertEquals(nci.protocol.subscriptions.size(), 0); - - await assertRejects( - async () => { - await nc.request("q", Empty, { noMux: true, timeout: 1000 }); - }, - Error, - "503", - ); + await assertRejects(() => { + return nc.request("q", Empty, { noMux: true, timeout: 500 }); + }); assertEquals(nci.protocol.subscriptions.size(), 0); await cleanup(ns, nc); @@ -704,42 +702,73 @@ Deno.test("basics - no mux request no perms doesn't leak subs", async () => { await cleanup(ns, nc); }); -Deno.test("basics - no max_payload messages", async () => { +Deno.test("basics - max_payload errors", async () => { const { ns, nc } = await _setup(connect, { max_payload: 2048 }); const nci = nc as NatsConnectionImpl; assert(nci.protocol.info); const big = new Uint8Array(nci.protocol.info.max_payload + 1); - const subj = createInbox(); - try { - nc.publish(subj, big); - fail(); - } catch (err) { - assertErrorCode(err as NatsError, ErrorCode.MaxPayloadExceeded); - } + assertThrows( + () => { + nc.publish("foo", big); + }, + errors.InvalidArgumentError, + `payload size exceeded`, + ); - try { - await nc.request(subj, big).then(); - fail(); - } catch (err) { - assertErrorCode(err as NatsError, ErrorCode.MaxPayloadExceeded); - } + assertRejects( + () => { + return nc.request("foo", big); + }, + errors.InvalidArgumentError, + `payload size exceeded`, + ); - const sub = nc.subscribe(subj); - (async () => { - for await (const m of sub) { - m.respond(big); - fail(); - } - })().catch((err) => { - assertErrorCode(err, ErrorCode.MaxPayloadExceeded); + const d = deferred(); + setTimeout(() => { + nc.request("foo").catch((err) => { + d.reject(err); + }); }); - await nc.request(subj).then(() => { - fail(); - }).catch((err) => { - assertErrorCode(err, ErrorCode.Timeout); - }); + 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(connect); + nc.subscribe("q", { callback: () => {} }); + + const done = assertRejects( + () => { + return nc.request("q"); + }, + errors.RequestError, + "connection closed", + ); + + await nc.close(); + await done; await cleanup(ns, nc); }); @@ -960,39 +989,35 @@ Deno.test("basics - port and server are mutually exclusive", async () => { async () => { await connect({ servers: "localhost", port: 4222 }); }, - Error, - "port and servers options are mutually exclusive", + errors.InvalidArgumentError, + "'servers','port' are mutually exclusive", undefined, ); }); Deno.test("basics - rtt", async () => { const { ns, nc } = await _setup(connect, {}, { - maxReconnectAttempts: 5, - reconnectTimeWait: 250, + maxReconnectAttempts: 1, + reconnectTimeWait: 750, }); const rtt = await nc.rtt(); assert(rtt >= 0); await ns.stop(); - await delay(500); + await assertRejects( - async () => { - await nc.rtt(); + () => { + return nc.rtt(); }, - Error, - ErrorCode.Disconnect, + errors.RequestError, + "disconnected", ); await nc.closed(); - await assertRejects( - async () => { - await nc.rtt(); - }, - Error, - ErrorCode.ConnectionClosed, - ); + await assertRejects(() => { + return nc.rtt(); + }, errors.ClosedConnectionError); }); Deno.test("basics - request many count", async () => { @@ -1216,17 +1241,15 @@ Deno.test("basics - initial connect error", async () => { } })(); - try { - await connect({ port, reconnect: false }); - fail("shouldn't have connected"); - } catch (err) { - // in node we may get a disconnect which we generated - // in deno we get the connection reset - but if running in CI this may turn out to be - // a connection refused - assertArrayIncludes(["ECONNRESET", "CONNECTION_REFUSED"], [ - (err as NatsError).code, - ]); - } + const err = await assertRejects(() => { + return connect({ port, reconnect: false }); + }); + + assert( + err instanceof errors.ConnectionError || + err instanceof Deno.errors.ConnectionReset, + ); + listener.close(); await done; }); @@ -1244,16 +1267,16 @@ Deno.test("basics - inbox prefixes cannot have wildcards", async () => { async () => { await connect({ inboxPrefix: "_inbox.foo.>" }); }, - Error, - "inbox prefixes cannot have wildcards", + errors.InvalidArgumentError, + "'prefix' cannot have wildcards", ); assertThrows( () => { createInbox("_inbox.foo.*"); }, - Error, - "inbox prefixes cannot have wildcards", + errors.InvalidArgumentError, + "'prefix' cannot have wildcards", ); }); diff --git a/core/tests/bench_test.ts b/core/tests/bench_test.ts index ecaee89e..801d6868 100644 --- a/core/tests/bench_test.ts +++ b/core/tests/bench_test.ts @@ -77,7 +77,7 @@ Deno.test("bench - no opts toss", async () => { new Bench(nc, {}); }, Error, - "no bench option selected", + "no options selected", ); await nc.close(); diff --git a/core/tests/drain_test.ts b/core/tests/drain_test.ts index fe692945..14a96514 100644 --- a/core/tests/drain_test.ts +++ b/core/tests/drain_test.ts @@ -12,16 +12,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assert, assertEquals, fail } from "jsr:@std/assert"; -import { createInbox, ErrorCode } from "../src/internal_mod.ts"; -import type { Msg, NatsError } from "../src/internal_mod.ts"; import { - assertThrowsAsyncErrorCode, - assertThrowsErrorCode, - Lock, -} from "test_helpers"; + assert, + assertEquals, + assertRejects, + assertThrows, +} from "jsr:@std/assert"; +import { createInbox } from "../src/internal_mod.ts"; +import { Lock } from "test_helpers"; import { _setup, cleanup } 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(connect); @@ -118,77 +119,43 @@ Deno.test("drain - publish after drain fails", async () => { nc.subscribe(subj); await nc.drain(); - assertThrowsErrorCode( - () => { - nc.publish(subj); - }, - ErrorCode.ConnectionClosed, - ErrorCode.ConnectionDraining, - ); + 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(connect); - const nc2 = await connect({ port: ns.port }); - const lock = Lock(); - const subj = createInbox(); - // start a service for replies - await nc.subscribe(subj, { - callback: (_, msg: Msg) => { - if (msg.reply) { - msg.respond("ok"); - } - }, - }); - await nc.flush(); - - let first = true; - const done = Lock(); - await nc2.subscribe(subj, { - callback: async () => { - if (first) { - first = false; - nc2.drain() - .then(() => { - done.unlock(); - }); - try { - // should fail - await nc2.request(subj + "a"); - fail("shouldn't have been able to request"); - lock.unlock(); - } catch (err) { - assertEquals((err as NatsError).code, ErrorCode.ConnectionDraining); - lock.unlock(); - } - } - }, - }); - // publish a trigger for the drain and requests - nc2.publish(subj); - await nc2.flush(); - await lock; - await nc.close(); + const done = nc.drain(); + await assertRejects(() => { + return nc.request("foo"); + }, errors.DrainingConnectionError); await done; - await ns.stop(); + await cleanup(ns, nc); }); Deno.test("drain - reject drain on closed", async () => { const { ns, nc } = await _setup(connect); await nc.close(); - await assertThrowsAsyncErrorCode(async () => { - await nc.drain(); - }, ErrorCode.ConnectionClosed); + await assertRejects(() => { + return nc.drain(); + }, errors.ClosedConnectionError); await ns.stop(); }); Deno.test("drain - reject drain on draining", async () => { const { ns, nc } = await _setup(connect); const done = nc.drain(); - await assertThrowsAsyncErrorCode(() => { + await assertRejects(() => { return nc.drain(); - }, ErrorCode.ConnectionDraining); + }, errors.DrainingConnectionError); await done; await ns.stop(); }); @@ -196,9 +163,10 @@ Deno.test("drain - reject drain on draining", async () => { Deno.test("drain - reject subscribe on draining", async () => { const { ns, nc } = await _setup(connect); const done = nc.drain(); - assertThrowsErrorCode(() => { + assertThrows(() => { return nc.subscribe("foo"); - }, ErrorCode.ConnectionDraining); + }, errors.DrainingConnectionError); + await done; await ns.stop(); }); @@ -207,9 +175,13 @@ Deno.test("drain - reject subscription drain on closed sub callback", async () = const { ns, nc } = await _setup(connect); const sub = nc.subscribe("foo", { callback: () => {} }); sub.unsubscribe(); - await assertThrowsAsyncErrorCode(() => { - return sub.drain(); - }, ErrorCode.SubClosed); + await assertRejects( + () => { + return sub.drain(); + }, + errors.InvalidOperationError, + "subscription is already closed", + ); await nc.close(); await ns.stop(); }); @@ -217,13 +189,21 @@ Deno.test("drain - reject subscription drain on closed sub callback", async () = Deno.test("drain - reject subscription drain on closed sub iter", async () => { const { ns, nc } = await _setup(connect); const sub = nc.subscribe("foo"); + const d = (async () => { + for await (const _ of sub) { + // nothing + } + })().then(); + sub.unsubscribe(); - for await (const _m of sub) { - // nothing to do here - } - await assertThrowsAsyncErrorCode(() => { - return sub.drain(); - }, ErrorCode.SubClosed); + await d; + await assertRejects( + () => { + return sub.drain(); + }, + errors.InvalidOperationError, + "subscription is already closed", + ); await nc.close(); await ns.stop(); }); @@ -240,9 +220,9 @@ Deno.test("drain - reject subscription drain on closed", async () => { const { ns, nc } = await _setup(connect); const sub = nc.subscribe("foo"); await nc.close(); - await assertThrowsAsyncErrorCode(() => { + await assertRejects(() => { return sub.drain(); - }, ErrorCode.ConnectionClosed); + }, errors.ClosedConnectionError); await ns.stop(); }); diff --git a/core/tests/headers_test.ts b/core/tests/headers_test.ts index f104476d..562142d4 100644 --- a/core/tests/headers_test.ts +++ b/core/tests/headers_test.ts @@ -21,7 +21,6 @@ import { Match, MsgHdrsImpl, MsgImpl, - NatsError, Parser, } from "../src/internal_mod.ts"; import type { @@ -33,19 +32,28 @@ import { NatsServer } from "../../test_helpers/launcher.ts"; import { assert, assertEquals, assertThrows } from "jsr:@std/assert"; import { TestDispatcher } from "./parser_test.ts"; import { _setup, cleanup } 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"); - }, NatsError); + 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); - }, NatsError); + assertThrows( + () => { + h.set("a", v); + }, + errors.InvalidArgumentError, + "values cannot contain \\r or \\n", + ); }); }); @@ -325,7 +333,7 @@ Deno.test("headers - code/description", () => { headers(500); }, Error, - "setting status requires both code and description", + "'description' is required", ); assertThrows( @@ -333,7 +341,7 @@ Deno.test("headers - code/description", () => { headers(0, "some message"); }, Error, - "setting status requires both code and description", + "'description' is required", ); }); diff --git a/core/tests/iterators_test.ts b/core/tests/iterators_test.ts index da22bee9..ab2d996e 100644 --- a/core/tests/iterators_test.ts +++ b/core/tests/iterators_test.ts @@ -13,18 +13,18 @@ * limitations under the License. */ import { connect } from "./connect.ts"; -import { assert, assertEquals, assertRejects } from "jsr:@std/assert"; -import { assertErrorCode, Lock, NatsServer } from "test_helpers"; +import { assertEquals, assertRejects } from "jsr:@std/assert"; +import { Lock, NatsServer } from "test_helpers"; import { createInbox, delay, - ErrorCode, nuid, QueuedIteratorImpl, syncIterator, } from "../src/internal_mod.ts"; import type { NatsConnectionImpl } from "../src/internal_mod.ts"; import { _setup, cleanup } from "test_helpers"; +import { errors } from "../src/errors.ts"; Deno.test("iterators - unsubscribe breaks and closes", async () => { const { ns, nc } = await _setup(connect); @@ -132,27 +132,21 @@ Deno.test("iterators - connection close closes", async () => { Deno.test("iterators - cb subs fail iterator", async () => { const { ns, nc } = await _setup(connect); const subj = createInbox(); - const lock = Lock(2); - const sub = nc.subscribe(subj, { - callback: (err, msg) => { - assert(err === null); - assert(msg); - lock.unlock(); + 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", + ); - (async () => { - for await (const _m of sub) { - lock.unlock(); - } - })().catch((err) => { - assertErrorCode(err, ErrorCode.ApiError); - lock.unlock(); - }); nc.publish(subj); await nc.flush(); await cleanup(ns, nc); - await lock; }); Deno.test("iterators - cb message counts", async () => { @@ -259,7 +253,7 @@ Deno.test("iterators - sync iterator", async () => { } }, Error, - "unsupported iterator", + "iterator cannot be used when a callback is registered", ); await cleanup(ns, nc); diff --git a/core/tests/json_test.ts b/core/tests/json_test.ts index 86126948..61e792b5 100644 --- a/core/tests/json_test.ts +++ b/core/tests/json_test.ts @@ -15,7 +15,7 @@ import { connect } from "./connect.ts"; import { assertEquals } from "jsr:@std/assert"; import { createInbox } from "../src/internal_mod.ts"; -import type { Msg, NatsError } from "../src/internal_mod.ts"; +import type { Msg } from "../src/internal_mod.ts"; import { Lock } from "test_helpers"; import { _setup, cleanup } from "test_helpers"; @@ -25,7 +25,7 @@ function macro(input: unknown) { const lock = Lock(); const subj = createInbox(); nc.subscribe(subj, { - callback: (err: NatsError | null, msg: Msg) => { + callback: (err: Error | null, msg: Msg) => { assertEquals(null, err); // in JSON undefined is translated to null if (input === undefined) { diff --git a/core/tests/mrequest_test.ts b/core/tests/mrequest_test.ts index 01f24603..82d61323 100644 --- a/core/tests/mrequest_test.ts +++ b/core/tests/mrequest_test.ts @@ -20,11 +20,11 @@ import { deferred, delay, Empty, - Events, RequestStrategy, } from "../src/internal_mod.ts"; import { assert, assertEquals, assertRejects, fail } from "jsr:@std/assert"; +import { errors } from "../src/errors.ts"; async function requestManyCount(noMux = false): Promise { const { ns, nc } = await _setup(connect, {}); @@ -257,8 +257,8 @@ async function requestManyStopsOnError(noMux = false): Promise { // do nothing } }, - Error, - "503", + errors.NoRespondersError, + subj, ); await cleanup(ns, nc); } @@ -285,8 +285,10 @@ Deno.test("mreq - pub permission error", async () => { const d = deferred(); (async () => { for await (const s of nc.status()) { - if (s.type === Events.Error && s.permissionContext?.subject === "q") { - d.resolve(); + if (s.error instanceof errors.PermissionViolationError) { + if (s.error.subject === "q" && s.error.operation === "publish") { + d.resolve(); + } } } })().then(); @@ -330,11 +332,13 @@ Deno.test("mreq - sub permission error", async () => { const d = deferred(); (async () => { for await (const s of nc.status()) { - if ( - s.type === Events.Error && - s.permissionContext?.operation === "subscription" - ) { - d.resolve(); + if (s.error instanceof errors.PermissionViolationError) { + if ( + s.error.operation === "subscription" && + s.error.subject.startsWith("_INBOX.") + ) { + d.resolve(); + } } } })().then(); @@ -351,7 +355,7 @@ Deno.test("mreq - sub permission error", async () => { // nothing; } }, - Error, + errors.PermissionViolationError, "Permissions Violation for Subscription", ); await d; @@ -390,11 +394,13 @@ Deno.test("mreq - lost sub permission", async () => { const d = deferred(); (async () => { for await (const s of nc.status()) { - if ( - s.type === Events.Error && - s.permissionContext?.operation === "subscription" - ) { - d.resolve(); + if (s.error instanceof errors.PermissionViolationError) { + if ( + s.error.operation === "subscription" && + s.error.subject.startsWith("_INBOX.") + ) { + d.resolve(); + } } } })().then(); @@ -404,14 +410,14 @@ Deno.test("mreq - lost sub permission", async () => { const iter = await nc.requestMany("q", Empty, { strategy: RequestStrategy.Count, maxMessages: 3, - maxWait: 5000, + maxWait: 2000, noMux: true, }); for await (const _m of iter) { - // nothing + // nothing; } }, - Error, + errors.PermissionViolationError, "Permissions Violation for Subscription", ); await d; @@ -456,8 +462,8 @@ Deno.test("mreq - no responder doesn't leak subs", async () => { // nothing } }, - Error, - "503", + errors.NoRespondersError, + "no responders: 'q'", ); // the mux subscription diff --git a/core/tests/noresponders_test.ts b/core/tests/noresponders_test.ts deleted file mode 100644 index 4d37bfa0..00000000 --- a/core/tests/noresponders_test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 { createInbox, Empty, ErrorCode, headers } from "@nats-io/nats-core"; -import { assertErrorCode, Lock, NatsServer } from "test_helpers"; -import { assert, assertEquals, fail } from "jsr:@std/assert"; - -Deno.test("noresponders - option", async () => { - const srv = await NatsServer.start(); - const nc = await connect( - { - servers: `127.0.0.1:${srv.port}`, - }, - ); - - const lock = Lock(); - await nc.request(createInbox()) - .then(() => { - fail("should have not resolved"); - }) - .catch((err) => { - assertErrorCode(err, ErrorCode.NoResponders); - lock.unlock(); - }); - - await lock; - await nc.close(); - await srv.stop(); -}); - -Deno.test("noresponders - list", async () => { - const srv = await NatsServer.start(); - const nc = await connect( - { - servers: `nats://127.0.0.1:${srv.port}`, - }, - ); - - const subj = createInbox(); - const sub = nc.subscribe(subj); - (async () => { - for await (const m of sub) { - const h = headers(); - h.append("a", "b"); - m.respond(Empty, { headers: h }); - } - })().then(); - await nc.flush(); - - const msg = await nc.request(subj); - assert(msg.headers); - assertEquals(msg.headers.get("a"), "b"); - await nc.close(); - await srv.stop(); -}); diff --git a/core/tests/parser_test.ts b/core/tests/parser_test.ts index 08cb0a15..70b50746 100644 --- a/core/tests/parser_test.ts +++ b/core/tests/parser_test.ts @@ -69,7 +69,7 @@ export class TestDispatcher implements Dispatcher { this.pongs++; break; default: - throw new Error(`unknown parser evert ${JSON.stringify(a)}`); + throw new Error(`unknown parser event ${JSON.stringify(a)}`); } } } diff --git a/core/tests/protocol_test.ts b/core/tests/protocol_test.ts index c8f2ece0..896958e0 100644 --- a/core/tests/protocol_test.ts +++ b/core/tests/protocol_test.ts @@ -14,7 +14,6 @@ */ import { Empty, - ErrorCode, extractProtocolMessage, MuxSubscription, protoLen, @@ -23,10 +22,10 @@ import { Subscriptions, } from "../src/internal_mod.ts"; import type { Msg, ProtocolHandler } from "../src/internal_mod.ts"; -import { assertErrorCode } from "test_helpers"; -import { assertEquals, equal } from "jsr:@std/assert"; +import { assertEquals, assertRejects, equal } from "jsr:@std/assert"; +import { errors } from "../src/errors.ts"; -Deno.test("protocol - mux subscription unknown return null", async () => { +Deno.test("protocol - mux subscription cancel", async () => { const mux = new MuxSubscription(); mux.init(); @@ -37,13 +36,17 @@ Deno.test("protocol - mux subscription unknown return null", async () => { assertEquals(mux.get("alberto"), r); assertEquals(mux.getToken({ subject: "" } as Msg), null); - const p = Promise.race([r.deferred, r.timer]) - .catch((err) => { - assertErrorCode(err, ErrorCode.Cancelled); - }); + const check = assertRejects( + () => { + return Promise.race([r.deferred, r.timer]); + }, + errors.RequestError, + "cancelled", + ); r.cancel(); - await p; + + await check; assertEquals(mux.size(), 0); }); diff --git a/core/tests/queues_test.ts b/core/tests/queues_test.ts index 7f4cbb3d..0e86eaea 100644 --- a/core/tests/queues_test.ts +++ b/core/tests/queues_test.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -import { createInbox } from "../src/internal_mod.ts"; -import type { Subscription } from "../src/internal_mod.ts"; +import { createInbox } from "../src/core.ts"; +import type { Subscription } from "../src/core.ts"; import { assertEquals } from "jsr:@std/assert"; import { connect } from "./connect.ts"; import { _setup, cleanup } from "test_helpers"; diff --git a/core/tests/reconnect_test.ts b/core/tests/reconnect_test.ts index 3a96cdca..16fc3f8f 100644 --- a/core/tests/reconnect_test.ts +++ b/core/tests/reconnect_test.ts @@ -12,23 +12,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assert, assertEquals, fail } from "jsr:@std/assert"; +import { assert, assertEquals, assertInstanceOf, fail } from "jsr:@std/assert"; import { connect } from "./connect.ts"; -import { assertErrorCode, Lock, NatsServer } from "test_helpers"; +import { Lock, NatsServer } from "test_helpers"; import { createInbox, DataBuffer, DebugEvents, deferred, delay, - ErrorCode, Events, tokenAuthenticator, } from "../src/internal_mod.ts"; -import type { NatsConnectionImpl, NatsError } from "../src/internal_mod.ts"; +import type { NatsConnectionImpl } from "../src/nats.ts"; import { _setup, cleanup } from "test_helpers"; import { deadline } from "jsr:@std/async"; +import { ConnectionError } from "../src/errors.ts"; Deno.test("reconnect - should receive when some servers are invalid", async () => { const lock = Lock(1); @@ -77,11 +77,8 @@ Deno.test("reconnect - events", async () => { } })().then(); await srv.stop(); - try { - await nc.closed(); - } catch (err) { - assertErrorCode(err as NatsError, ErrorCode.ConnectionRefused); - } + const err = await nc.closed(); + assertInstanceOf(err, ConnectionError, "connection closed"); assertEquals(disconnects, 1); assertEquals(reconnecting, 10); }); @@ -276,27 +273,8 @@ Deno.test("reconnect - wait on first connect", async () => { // stop the server await srv.stop(); // no reconnect, will quit the client - const what = await nc.closed() as NatsError; - assertEquals(what.code, ErrorCode.ConnectionRefused); -}); - -Deno.test("reconnect - wait on first connect off", async () => { - const srv = await NatsServer.start({}); - const port = srv.port; - await delay(500); - await srv.stop(); - await delay(1000); - const pnc = connect({ - port: port, - }); - - try { - // should fail - await pnc; - } catch (err) { - const nerr = err as NatsError; - assertEquals(nerr.code, ErrorCode.ConnectionRefused); - } + const err = await nc.closed(); + assertInstanceOf(err, ConnectionError, "connection refused"); }); Deno.test("reconnect - close stops reconnects", async () => { @@ -467,7 +445,7 @@ Deno.test("reconnect - authentication timeout reconnects", async () => { }, }); - let counter = 4; + let counter = 3; const authenticator = tokenAuthenticator(() => { if (counter-- <= 0) { return "hello"; @@ -485,7 +463,7 @@ Deno.test("reconnect - authentication timeout reconnects", async () => { port: ns.port, token: "hello", waitOnFirstConnect: true, - timeout: 2000, + ignoreAuthErrorAbort: true, authenticator, }); diff --git a/core/tests/semver_test.ts b/core/tests/semver_test.ts index 6a26da40..b08d5518 100644 --- a/core/tests/semver_test.ts +++ b/core/tests/semver_test.ts @@ -13,12 +13,7 @@ * limitations under the License. */ -import { - compare, - Feature, - Features, - parseSemVer, -} from "../src/internal_mod.ts"; +import { compare, Feature, Features, parseSemVer } from "../src/semver.ts"; import { assert, assertEquals, diff --git a/core/tests/timeout_test.ts b/core/tests/timeout_test.ts index a6b19cfc..a8d149a6 100644 --- a/core/tests/timeout_test.ts +++ b/core/tests/timeout_test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 The NATS Authors + * 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 @@ -12,30 +12,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assertStringIncludes, fail } from "jsr:@std/assert"; +import { + assertInstanceOf, + assertRejects, + assertStringIncludes, +} from "jsr:@std/assert"; import { connect } from "./connect.ts"; -import { createInbox, Empty } from "../src/internal_mod.ts"; +import { createInbox, Empty, errors } from "../src/internal_mod.ts"; Deno.test("timeout - request noMux stack is useful", async () => { const nc = await connect({ servers: "demo.nats.io" }); const subj = createInbox(); - try { - await nc.request(subj, Empty, { noMux: true, timeout: 250 }); - fail("request should have failed!"); - } catch (err) { - assertStringIncludes((err as Error).stack || "", "timeout_test"); - } + 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 nc.close(); }); Deno.test("timeout - request stack is useful", async () => { const nc = await connect({ servers: "demo.nats.io" }); const subj = createInbox(); - try { - await nc.request(subj, Empty, { timeout: 250 }); - fail("request should have failed!"); - } catch (err) { - assertStringIncludes((err as Error).stack || "", "timeout_test"); - } + 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 nc.close(); }); diff --git a/core/tests/tls_test.ts b/core/tests/tls_test.ts index 202892df..b527649f 100644 --- a/core/tests/tls_test.ts +++ b/core/tests/tls_test.ts @@ -12,29 +12,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - assertEquals, - assertRejects, - assertStringIncludes, - fail, -} from "jsr:@std/assert"; +import { assertEquals, assertRejects } from "jsr:@std/assert"; import { connect } from "./connect.ts"; -import { ErrorCode } from "../src/internal_mod.ts"; +import { errors } from "../src/internal_mod.ts"; import type { NatsConnectionImpl } from "../src/internal_mod.ts"; -import { assertErrorCode, cleanup, Lock, NatsServer } from "test_helpers"; +import { cleanup, NatsServer } from "test_helpers"; Deno.test("tls - fail if server doesn't support TLS", async () => { const ns = await NatsServer.start(); - const lock = Lock(); - await connect({ port: ns.port, tls: {} }) - .then(() => { - fail("shouldn't have connected"); - }) - .catch((err) => { - assertErrorCode(err, ErrorCode.ServerOptionNotAvailable); - lock.unlock(); - }); - await lock; + await assertRejects( + () => { + return connect({ port: ns.port, tls: {}, reconnect: false }); + }, + errors.ConnectionError, + "server does not support 'tls'", + ); await ns.stop(); }); @@ -53,23 +45,13 @@ Deno.test("tls - custom ca fails without root", async () => { }; const ns = await NatsServer.start(config); - const lock = Lock(); - await connect({ servers: `localhost:${ns.port}` }) - .then(() => { - fail("shouldn't have connected without client ca"); - }) - .catch((err) => { - // this is a bogus error name - but at least we know we are rejected - assertEquals(err.name, "InvalidData"); - assertStringIncludes( - err.message, - "invalid peer certificate", - ); - assertStringIncludes(err.message, "UnknownIssuer"); - lock.unlock(); - }); - - await lock; + 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 }); }); diff --git a/core/tests/token_test.ts b/core/tests/token_test.ts index 633eb1df..001ac347 100644 --- a/core/tests/token_test.ts +++ b/core/tests/token_test.ts @@ -12,41 +12,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { fail } from "jsr:@std/assert"; -import { ErrorCode } from "../src/internal_mod.ts"; -import { assertErrorCode, NatsServer } from "test_helpers"; +import { assertRejects } from "jsr:@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); - try { - const nc = await connect( - { port: ns.port, reconnect: false }, - ); - nc.closed().then((err) => { - console.table(err); - }); - await nc.close(); - fail("should not have connected"); - } catch (err) { - assertErrorCode(err as Error, ErrorCode.AuthorizationViolation); - } + 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); - try { - const nc = await connect( - { port: ns.port, token: "bad" }, + await assertRejects(() => { + return connect( + { port: ns.port, token: "bad", reconnect: false }, ); - await nc.close(); - fail("should not have connected"); - } catch (err) { - assertErrorCode(err as Error, ErrorCode.AuthorizationViolation); - } + }, errors.AuthorizationError); await ns.stop(); }); diff --git a/core/tests/ws_test.ts b/core/tests/ws_test.ts index 1130a3af..36dc38fe 100644 --- a/core/tests/ws_test.ts +++ b/core/tests/ws_test.ts @@ -23,12 +23,12 @@ import { import { createInbox, DebugEvents, - ErrorCode, + errors, Events, wsconnect, wsUrlParseFn, } from "../src/internal_mod.ts"; -import type { NatsConnectionImpl, NatsError } from "../src/internal_mod.ts"; +import type { NatsConnectionImpl } from "../src/nats.ts"; import { assertBetween, cleanup, @@ -105,15 +105,13 @@ Deno.test( ); Deno.test("ws - tls options are not supported", async () => { - const err = await assertRejects( + await assertRejects( () => { return wsconnect({ servers: "wss://demo.nats.io:8443", tls: {} }); }, - Error, - "tls", + errors.InvalidArgumentError, + "'tls' is not configurable on w3c websocket connections", ); - - assertEquals((err as NatsError).code, ErrorCode.InvalidOption); }); Deno.test( diff --git a/jetstream/deno.json b/jetstream/deno.json index 098cfe63..52076eb5 100644 --- a/jetstream/deno.json +++ b/jetstream/deno.json @@ -1,6 +1,6 @@ { "name": "@nats-io/jetstream", - "version": "3.0.0-15", + "version": "3.0.0-21", "exports": { ".": "./src/mod.ts", "./internal": "./src/internal_mod.ts" @@ -33,6 +33,6 @@ "test": "deno test -A --parallel --reload --trace-leaks --quiet tests/ --import-map=import_map.json" }, "imports": { - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30" + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34" } } diff --git a/jetstream/examples/01_consumers.ts b/jetstream/examples/01_consumers.ts index 1dd292a8..cbbaa810 100644 --- a/jetstream/examples/01_consumers.ts +++ b/jetstream/examples/01_consumers.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import { jetstream } from "../src/mod.ts"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { jetstream } from "jsr:@nats-io/jetstream@3.0.0-18"; import { setupStreamAndConsumer } from "./util.ts"; // create a connection diff --git a/jetstream/examples/02_next.ts b/jetstream/examples/02_next.ts index 17c66f5a..94d645f6 100644 --- a/jetstream/examples/02_next.ts +++ b/jetstream/examples/02_next.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import { jetstream } from "../src/mod.ts"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { jetstream } from "jsr:@nats-io/jetstream@3.0.0-18"; import { setupStreamAndConsumer } from "./util.ts"; // create a connection diff --git a/jetstream/examples/03_batch.ts b/jetstream/examples/03_batch.ts index 8c1656b3..686f70a8 100644 --- a/jetstream/examples/03_batch.ts +++ b/jetstream/examples/03_batch.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import { jetstream } from "../src/mod.ts"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { jetstream } from "jsr:@nats-io/jetstream@3.0.0-18"; import { setupStreamAndConsumer } from "./util.ts"; // create a connection diff --git a/jetstream/examples/04_consume.ts b/jetstream/examples/04_consume.ts index 4f4a333a..601bf339 100644 --- a/jetstream/examples/04_consume.ts +++ b/jetstream/examples/04_consume.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import { jetstream } from "../src/mod.ts"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { jetstream } from "jsr:@nats-io/jetstream@3.0.0-18"; import { setupStreamAndConsumer } from "./util.ts"; // create a connection @@ -39,6 +39,6 @@ while (true) { m.ack(); } } catch (err) { - console.log(`consume failed: ${err.message}`); + console.log(`consume failed: ${(err as Error).message}`); } } diff --git a/jetstream/examples/05_consume.ts b/jetstream/examples/05_consume.ts index 8feccfd1..d1b0ca4b 100644 --- a/jetstream/examples/05_consume.ts +++ b/jetstream/examples/05_consume.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import { jetstream } from "../src/mod.ts"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { jetstream } from "jsr:@nats-io/jetstream@3.0.0-18"; import { setupStreamAndConsumer } from "./util.ts"; // create a connection diff --git a/jetstream/examples/06_heartbeats.ts b/jetstream/examples/06_heartbeats.ts index 488728f4..fc52d40e 100644 --- a/jetstream/examples/06_heartbeats.ts +++ b/jetstream/examples/06_heartbeats.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import { ConsumerEvents, jetstream } from "../src/mod.ts"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { ConsumerEvents, jetstream } from "jsr:@nats-io/jetstream@3.0.0-18"; import { setupStreamAndConsumer } from "./util.ts"; // create a connection diff --git a/jetstream/examples/07_consume_jobs.ts b/jetstream/examples/07_consume_jobs.ts index f6dc5e53..f158bb94 100644 --- a/jetstream/examples/07_consume_jobs.ts +++ b/jetstream/examples/07_consume_jobs.ts @@ -13,10 +13,10 @@ * limitations under the License. */ -import { connect, delay } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect, delay } from "jsr:@nats-io/transport-deno@3.0.0-7"; import { SimpleMutex } from "jsr:@nats-io/nats-core@3.0.0-17/internal"; -import { jetstream } from "../src/mod.ts"; -import type { JsMsg } from "../src/mod.ts"; +import { jetstream } from "jsr:@nats-io/jetstream@3.0.0-18"; +import type { JsMsg } from "jsr:@nats-io/jetstream@3.0.0-18"; import { setupStreamAndConsumer } from "./util.ts"; // create a connection diff --git a/jetstream/examples/08_consume_process.ts b/jetstream/examples/08_consume_process.ts index 44bcb53b..9af823b4 100644 --- a/jetstream/examples/08_consume_process.ts +++ b/jetstream/examples/08_consume_process.ts @@ -13,9 +13,9 @@ * limitations under the License. */ -import { connect } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; +import { connect } from "jsr:@nats-io/transport-deno@3.0.0-7"; import { setupStreamAndConsumer } from "./util.ts"; -import { jetstream } from "../src/mod.ts"; +import { jetstream } from "jsr:@nats-io/jetstream@3.0.0-18"; // create a connection const nc = await connect(); diff --git a/jetstream/examples/js_readme_publish_examples.ts b/jetstream/examples/js_readme_publish_examples.ts index c36725d8..a8adb1f5 100644 --- a/jetstream/examples/js_readme_publish_examples.ts +++ b/jetstream/examples/js_readme_publish_examples.ts @@ -1,5 +1,20 @@ -import { connect, Empty } from "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import { jetstream, jetstreamManager } from "../src/mod.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, Empty } from "jsr:@nats-io/transport-deno@3.0.0-7"; +import { jetstream, jetstreamManager } from "jsr:@nats-io/jetstream@3.0.0-18"; import type { PubAck } from "../src/mod.ts"; const nc = await connect(); diff --git a/jetstream/examples/jsm_readme_jsm_example.ts b/jetstream/examples/jsm_readme_jsm_example.ts index 6969f06f..41fcdf83 100644 --- a/jetstream/examples/jsm_readme_jsm_example.ts +++ b/jetstream/examples/jsm_readme_jsm_example.ts @@ -1,5 +1,20 @@ +/* + * 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 "jsr:@nats-io/nats-transport-deno@3.0.0-5"; -import { AckPolicy, jetstreamManager } from "../src/mod.ts"; +import { AckPolicy, jetstreamManager } from "jsr:@nats-io/jetstream@3.0.0-18"; const nc = await connect(); const jsm = await jetstreamManager(nc); diff --git a/jetstream/examples/util.ts b/jetstream/examples/util.ts index a3296971..c0820f99 100644 --- a/jetstream/examples/util.ts +++ b/jetstream/examples/util.ts @@ -14,8 +14,8 @@ */ import { createConsumer, fill, initStream } from "../tests/jstest_util.ts"; -import type { NatsConnection } from "jsr:@nats-io/nats-core@3.0.0-27"; -import { nuid } from "jsr:@nats-io/nats-core@3.0.0-27"; +import type { NatsConnection } from "jsr:@nats-io/nats-core@3.0.0-31"; +import { nuid } from "jsr:@nats-io/nats-core@3.0.0-31"; export async function setupStreamAndConsumer( nc: NatsConnection, diff --git a/jetstream/import_map.json b/jetstream/import_map.json index bafe819c..3d1a7d73 100644 --- a/jetstream/import_map.json +++ b/jetstream/import_map.json @@ -2,8 +2,8 @@ "imports": { "@nats-io/nkeys": "jsr:@nats-io/nkeys@1.2.0-4", "@nats-io/nuid": "jsr:@nats-io/nuid@2.0.1-2", - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30", - "@nats-io/nats-core/internal": "jsr:@nats-io/nats-core@~3.0.0-30/internal", + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34", + "@nats-io/nats-core/internal": "jsr:@nats-io/nats-core@~3.0.0-34/internal", "test_helpers": "../test_helpers/mod.ts", "@std/io": "jsr:@std/io@0.224.0" } diff --git a/jetstream/package.json b/jetstream/package.json index f75421ff..d3a5b7f7 100644 --- a/jetstream/package.json +++ b/jetstream/package.json @@ -1,6 +1,6 @@ { "name": "@nats-io/jetstream", - "version": "3.0.0-15", + "version": "3.0.0-21", "files": [ "lib/", "LICENSE", @@ -34,7 +34,7 @@ }, "description": "jetstream library - this library implements all the base functionality for NATS JetStream for javascript clients", "dependencies": { - "@nats-io/nats-core": "~3.0.0-30" + "@nats-io/nats-core": "3.0.0-34" }, "devDependencies": { "@types/node": "^22.7.6", diff --git a/jetstream/src/consumer.ts b/jetstream/src/consumer.ts index 9f98ddb6..64a544ee 100644 --- a/jetstream/src/consumer.ts +++ b/jetstream/src/consumer.ts @@ -16,7 +16,6 @@ import type { CallbackFn, Delay, - MsgHdrs, MsgImpl, QueuedIterator, Status, @@ -28,16 +27,15 @@ import { backoff, createInbox, delay, + errors, Events, IdleHeartbeatMonitor, nanos, - NatsError, nuid, QueuedIteratorImpl, timeout, } from "@nats-io/nats-core/internal"; import type { ConsumerAPIImpl } from "./jsmconsumer_api.ts"; -import { isHeartbeatMsg } from "./jsutil.ts"; import type { JsMsg } from "./jsmsg.ts"; import { toJsMsg } from "./jsmsg.ts"; @@ -48,7 +46,6 @@ import type { PullOptions, } from "./jsapi_types.ts"; import { AckPolicy, DeliverPolicy } from "./jsapi_types.ts"; -import { ConsumerDebugEvents, ConsumerEvents, JsHeaders } from "./types.ts"; import type { ConsumeMessages, ConsumeOptions, @@ -63,6 +60,8 @@ import type { OrderedConsumerOptions, PullConsumerOptions, } from "./types.ts"; +import { ConsumerDebugEvents, ConsumerEvents } from "./types.ts"; +import { JetStreamStatus } from "./jserrors.ts"; enum PullConsumerType { Unset = -1, @@ -169,20 +168,18 @@ export class PullConsumerMessagesImpl extends QueuedIteratorImpl const isProtocol = msg.subject === this.inbox; if (isProtocol) { - if (isHeartbeatMsg(msg)) { - const natsLastConsumer = msg.headers?.get("Nats-Last-Consumer"); - const natsLastStream = msg.headers?.get("Nats-Last-Stream"); - this.notify(ConsumerDebugEvents.Heartbeat, { - natsLastConsumer, - natsLastStream, - }); + const status = new JetStreamStatus(msg); + + if (status.isIdleHeartbeat()) { + this.notify(ConsumerDebugEvents.Heartbeat, status.parseHeartbeat()); return; } - const code = msg.headers?.code; - const description = msg.headers?.description?.toLowerCase() || - "unknown"; - const { msgsLeft, bytesLeft } = this.parseDiscard(msg.headers); - if (msgsLeft > 0 || bytesLeft > 0) { + const code = status.code; + const description = status.description; + + const { msgsLeft, bytesLeft } = status.parseDiscard(); + console.log("pending", msgsLeft, bytesLeft); + if ((msgsLeft && msgsLeft > 0) || (bytesLeft && bytesLeft > 0)) { this.pending.msgs -= msgsLeft; this.pending.bytes -= bytesLeft; this.pending.requests--; @@ -200,10 +197,10 @@ export class PullConsumerMessagesImpl extends QueuedIteratorImpl // we got a bad request - no progress here switch (code) { case 400: - this.stop(new NatsError(description, `${code}`)); + this.stop(status.toError()); return; case 409: { - const err = this.handle409(code, description); + const err = this.handle409(status); if (err) { this.stop(err); return; @@ -341,22 +338,20 @@ export class PullConsumerMessagesImpl extends QueuedIteratorImpl /** * Handle the notification of 409 error and whether * it should reject the operation by returning an Error or null - * @param code - * @param description + * @param status */ - handle409(code: number, description: string): Error | null { - const e = description === "consumer deleted" - ? ConsumerEvents.ConsumerDeleted - : ConsumerEvents.ExceededLimit; - this.notify(e, { code, description }); + handle409(status: JetStreamStatus): Error | null { + const { code, description } = status; + if (status.isConsumerDeleted()) { + this.notify(ConsumerEvents.ConsumerDeleted, { code, description }); + } else if (status.isExceededLimit()) { + this.notify(ConsumerEvents.ExceededLimit, { code, description }); + } if (!this.isConsume) { - // terminate the fetch/next - return new NatsError(description, `${code}`); - } else if ( - e === ConsumerEvents.ConsumerDeleted && this.abortOnMissingResource - ) { - // terminate the consume if abortOnMissingResource - return new NatsError(description, `${code}`); + return status.toError(); + } + if (status.isConsumerDeleted() && this.abortOnMissingResource) { + return status.toError(); } return null; } @@ -568,25 +563,6 @@ export class PullConsumerMessagesImpl extends QueuedIteratorImpl return { batch, max_bytes, idle_heartbeat, expires }; } - parseDiscard( - headers?: MsgHdrs, - ): { msgsLeft: number; bytesLeft: number } { - const discard = { - msgsLeft: 0, - bytesLeft: 0, - }; - const msgsLeft = headers?.get(JsHeaders.PendingMessagesHdr); - if (msgsLeft) { - discard.msgsLeft = parseInt(msgsLeft); - } - const bytesLeft = headers?.get(JsHeaders.PendingBytesHdr); - if (bytesLeft) { - discard.bytesLeft = parseInt(bytesLeft); - } - - return discard; - } - trackTimeout(t: Timeout) { this.timeout = t; } @@ -652,7 +628,10 @@ export class PullConsumerMessagesImpl extends QueuedIteratorImpl args.expires = args.expires || 30_000; if (args.expires < 1000) { - throw new Error("expires should be at least 1000ms"); + throw errors.InvalidArgumentError.format( + "expires", + "must be at least 1000ms", + ); } // require idle_heartbeat @@ -729,16 +708,22 @@ export class PullConsumerImpl implements Consumer { ): Promise { if (this.ordered) { if (opts.bind) { - return Promise.reject(new Error("bind is not supported")); + return Promise.reject( + errors.InvalidArgumentError.format("bind", "is not supported"), + ); } if (this.type === PullConsumerType.Fetch) { return Promise.reject( - new Error("ordered consumer initialized as fetch"), + new errors.InvalidOperationError( + "ordered consumer initialized as fetch", + ), ); } if (this.type === PullConsumerType.Consume) { return Promise.reject( - new Error("ordered consumer doesn't support concurrent consume"), + new errors.InvalidOperationError( + "ordered consumer doesn't support concurrent consume", + ), ); } this.type = PullConsumerType.Consume; @@ -756,16 +741,22 @@ export class PullConsumerImpl implements Consumer { ): Promise { if (this.ordered) { if (opts.bind) { - return Promise.reject(new Error("bind is not supported")); + return Promise.reject( + errors.InvalidArgumentError.format("bind", "is not supported"), + ); } if (this.type === PullConsumerType.Consume) { return Promise.reject( - new Error("ordered consumer already initialized as consume"), + new errors.InvalidOperationError( + "ordered consumer already initialized as consume", + ), ); } if (this.messages?.done === false) { return Promise.reject( - new Error("ordered consumer doesn't support concurrent fetch"), + new errors.InvalidOperationError( + "ordered consumer doesn't support concurrent fetch", + ), ); } if (this.ordered) { diff --git a/jetstream/src/internal_mod.ts b/jetstream/src/internal_mod.ts index 34ac7d9f..15fb2e72 100644 --- a/jetstream/src/internal_mod.ts +++ b/jetstream/src/internal_mod.ts @@ -12,7 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { checkJsError, isFlowControlMsg, isHeartbeatMsg } from "./jsutil.ts"; export { AdvisoryKind, @@ -138,3 +137,11 @@ export { export type { DeliveryInfo, StreamInfoRequestOptions } from "./jsapi_types.ts"; export { ListerImpl } from "./jslister.ts"; + +export { + isMessageNotFound, + JetStreamApiCodes, + JetStreamApiError, + JetStreamError, + jserrors, +} from "./jserrors.ts"; diff --git a/jetstream/src/jsapi_types.ts b/jetstream/src/jsapi_types.ts index 4bec38b1..0f8097be 100644 --- a/jetstream/src/jsapi_types.ts +++ b/jetstream/src/jsapi_types.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -import type { ApiError, Nanos } from "@nats-io/nats-core"; +import type { Nanos } from "@nats-io/nats-core"; import { nanos } from "@nats-io/nats-core"; export interface ApiPaged { @@ -31,6 +31,21 @@ export interface ApiResponse { error?: ApiError; } +export interface ApiError { + /** + * HTTP like error code in the 300 to 500 range + */ + code: number; + /** + * A human friendly description of the error + */ + description: string; + /** + * The NATS error code unique to each kind of error + */ + err_code: number; +} + /** * An alternate location to read mirrored data */ diff --git a/jetstream/src/jsbaseclient_api.ts b/jetstream/src/jsbaseclient_api.ts index 4badfa40..9f48457f 100644 --- a/jetstream/src/jsbaseclient_api.ts +++ b/jetstream/src/jsbaseclient_api.ts @@ -17,19 +17,25 @@ import { backoff, delay, Empty, - ErrorCode, + errors, extend, + RequestError, } from "@nats-io/nats-core/internal"; import type { Msg, NatsConnection, NatsConnectionImpl, - NatsError, RequestOptions, } from "@nats-io/nats-core/internal"; -import { checkJsErrorCode } from "./jsutil.ts"; import type { ApiResponse } from "./jsapi_types.ts"; import type { JetStreamOptions } from "./types.ts"; +import { + ConsumerNotFoundError, + JetStreamApiCodes, + JetStreamApiError, + JetStreamNotEnabled, + StreamNotFoundError, +} from "./jserrors.ts"; const defaultPrefix = "$JS.API"; const defaultTimeout = 5000; @@ -72,7 +78,7 @@ export class BaseApiClientImpl { _parseOpts() { let prefix = this.opts.apiPrefix; if (!prefix || prefix.length === 0) { - throw new Error("invalid empty prefix"); + throw errors.InvalidArgumentError.format("prefix", "cannot be empty"); } const c = prefix[prefix.length - 1]; if (c === ".") { @@ -111,14 +117,18 @@ export class BaseApiClientImpl { ); return this.parseJsResponse(m); } catch (err) { - const ne = err as NatsError; + const re = err instanceof RequestError ? err as RequestError : null; if ( - (ne.code === "503" || ne.code === ErrorCode.Timeout) && - i + 1 < retries + err instanceof errors.TimeoutError || + re?.isNoResponders() && i + 1 < retries ) { await delay(bo.backoff(i)); } else { - throw err; + throw re?.isNoResponders() + ? new JetStreamNotEnabled("jetstream is not enabled", { + cause: err, + }) + : err; } } } @@ -129,7 +139,7 @@ export class BaseApiClientImpl { const r = await this._request(`${this.prefix}.STREAM.NAMES`, q); const names = r as StreamNames; if (!names.streams || names.streams.length !== 1) { - throw new Error("no stream matches subject"); + throw StreamNotFoundError.fromMessage("no stream matches subject"); } return names.streams[0]; } @@ -142,13 +152,17 @@ export class BaseApiClientImpl { const v = JSON.parse(new TextDecoder().decode(m.data)); const r = v as ApiResponse; if (r.error) { - const err = checkJsErrorCode(r.error.code, r.error.description); - if (err !== null) { - err.api_error = r.error; - if (r.error.description !== "") { - err.message = r.error.description; + switch (r.error.err_code) { + case JetStreamApiCodes.ConsumerNotFound: + throw new ConsumerNotFoundError(r.error); + case JetStreamApiCodes.StreamNotFound: + throw new StreamNotFoundError(r.error); + case JetStreamApiCodes.JetStreamNotEnabledForAccount: { + const jserr = new JetStreamApiError(r.error); + throw new JetStreamNotEnabled(jserr.message, { cause: jserr }); } - throw err; + default: + throw new JetStreamApiError(r.error); } } return v; diff --git a/jetstream/src/jsclient.ts b/jetstream/src/jsclient.ts index 8f3dfe61..af1a2345 100644 --- a/jetstream/src/jsclient.ts +++ b/jetstream/src/jsclient.ts @@ -15,12 +15,7 @@ import { BaseApiClientImpl } from "./jsbaseclient_api.ts"; import { ConsumerAPIImpl } from "./jsmconsumer_api.ts"; -import { - delay, - Empty, - NatsError, - QueuedIteratorImpl, -} from "@nats-io/nats-core/internal"; +import { delay, Empty, QueuedIteratorImpl } from "@nats-io/nats-core/internal"; import { ConsumersImpl, StreamAPIImpl, StreamsImpl } from "./jsmstream_api.ts"; @@ -39,7 +34,7 @@ import type { StreamAPI, Streams, } from "./types.ts"; -import { ErrorCode, headers } from "@nats-io/nats-core/internal"; +import { errors, headers } from "@nats-io/nats-core/internal"; import type { Msg, @@ -54,6 +49,7 @@ import type { JetStreamAccountStats, } from "./jsapi_types.ts"; import { DirectStreamAPIImpl } from "./jsm.ts"; +import { JetStreamError } from "./jserrors.ts"; export function toJetStreamClient( nc: NatsConnection | JetStreamClient, @@ -91,11 +87,7 @@ export async function jetstreamManager( try { await adm.getAccountInfo(); } catch (err) { - const ne = err as NatsError; - if (ne.code === ErrorCode.NoResponders) { - ne.code = ErrorCode.JetStreamNotEnabled; - } - throw ne; + throw err; } } return adm; @@ -227,8 +219,9 @@ export class JetStreamClientImpl extends BaseApiClientImpl // if here we succeeded break; } catch (err) { - const ne = err as NatsError; - if (ne.code === "503" && i + 1 < retries) { + if ( + err instanceof errors.RequestError && err.isNoResponders() + ) { await delay(retry_delay); } else { throw err; @@ -237,7 +230,7 @@ export class JetStreamClientImpl extends BaseApiClientImpl } const pa = this.parseJsResponse(r!) as PubAck; if (pa.stream === "") { - throw NatsError.errorForCode(ErrorCode.JetStreamInvalidAck); + throw new JetStreamError("invalid ack response"); } pa.duplicate = pa.duplicate ? pa.duplicate : false; return pa; diff --git a/jetstream/src/jserrors.ts b/jetstream/src/jserrors.ts new file mode 100644 index 00000000..aacbad08 --- /dev/null +++ b/jetstream/src/jserrors.ts @@ -0,0 +1,246 @@ +/* + * 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 type { Msg } from "@nats-io/nats-core"; +import { JsHeaders } from "./types.ts"; +import type { ApiError } from "./jsapi_types.ts"; + +export class JetStreamNotEnabled extends Error { + constructor(message: string, opts?: ErrorOptions) { + super(message, opts); + this.name = "JetStreamNotEnabled"; + } +} + +export class JetStreamError extends Error { + constructor(message: string, opts?: ErrorOptions) { + super(message, opts); + this.name = "JetStreamError"; + } +} + +export class JetStreamStatusError extends JetStreamError { + code: number; + constructor(message: string, code: number, opts?: ErrorOptions) { + super(message, opts); + this.code = code; + this.name = "JetStreamStatusError"; + } +} + +export class JetStreamStatus { + msg: Msg; + _description: string; + + constructor(msg: Msg) { + this.msg = msg; + this._description = ""; + } + + static maybeParseStatus(msg: Msg): JetStreamStatus | null { + const status = new JetStreamStatus(msg); + return status.code === 0 ? null : status; + } + + toError(): JetStreamStatusError { + return new JetStreamStatusError(this.description, this.code); + } + + debug() { + console.log({ + message: this.description, + status: this.code, + headers: this.msg.headers, + }); + } + + get code(): number { + return this.msg.headers?.code || 0; + } + + get description(): string { + if (this._description === "") { + this._description = this.msg.headers?.description?.toLowerCase() || + "unknown"; + } + return this._description; + } + + isIdleHeartbeat(): boolean { + return this.code === 100 && this.description === "idle heartbeat"; + } + + isFlowControlRequest(): boolean { + return this.code === 100 && this.description === "flowcontrol request"; + } + + parseHeartbeat(): + | { natsLastConsumer: number; natsLastStream: number } + | null { + if (this.isIdleHeartbeat()) { + return { + natsLastConsumer: parseInt( + this.msg.headers?.get("Nats-Last-Consumer") || "0", + ), + natsLastStream: parseInt( + this.msg.headers?.get("Nats-Last-Stream") || "0", + ), + }; + } + return null; + } + + isRequestTimeout(): boolean { + return this.code === 408 && this.description === "request timeout"; + } + + parseDiscard(): { msgsLeft: number; bytesLeft: number } { + const discard = { + msgsLeft: 0, + bytesLeft: 0, + }; + const msgsLeft = this.msg.headers?.get(JsHeaders.PendingMessagesHdr); + if (msgsLeft) { + discard.msgsLeft = parseInt(msgsLeft); + } + const bytesLeft = this.msg.headers?.get(JsHeaders.PendingBytesHdr); + if (bytesLeft) { + discard.bytesLeft = parseInt(bytesLeft); + } + + return discard; + } + + isBadRequest() { + return this.code === 400; + } + + isConsumerDeleted() { + return this.code === 409 && this.description === "consumer deleted"; + } + + isStreamDeleted(): boolean { + return this.code === 409 && this.description === "stream deleted"; + } + + isIdleHeartbeatMissed(): boolean { + return this.code === 409 && this.description === "idle heartbeats missed"; + } + + isMaxWaitingExceeded(): boolean { + return this.code === 409 && this.description === "exceeded maxwaiting"; + } + + isConsumerIsPushBased(): boolean { + return this.code === 409 && this.description === "consumer is push based"; + } + + isExceededMaxWaiting(): boolean { + return this.code === 409 && + this.description.includes("exceeded maxwaiting"); + } + + isExceededMaxRequestBatch(): boolean { + return this.code === 409 && + this.description.includes("exceeded maxrequestbatch"); + } + + isExceededMaxExpires(): boolean { + return this.code === 409 && + this.description.includes("exceeded maxrequestexpires"); + } + + isExceededLimit(): boolean { + return this.isExceededMaxExpires() || this.isExceededMaxWaiting() || + this.isExceededMaxRequestBatch(); + } + + isMessageNotFound(): boolean { + return this.code === 404 && this.description === "message not found"; + } +} + +export enum JetStreamApiCodes { + ConsumerNotFound = 10014, + StreamNotFound = 10059, + JetStreamNotEnabledForAccount = 10039, + StreamWrongLastSequence = 10071, + NoMessageFound = 10037, +} + +export function isMessageNotFound(err: Error): boolean { + return err instanceof JetStreamApiError && + err.code === JetStreamApiCodes.NoMessageFound; +} + +export class InvalidNameError extends Error { + constructor(name: string, message: string = "", opts?: ErrorOptions) { + super(`'${name} ${message}`, opts); + this.name = "InvalidNameError"; + } +} + +export class JetStreamApiError extends Error { + #apiError: ApiError; + + constructor(jsErr: ApiError, opts?: ErrorOptions) { + super(jsErr.description, opts); + this.#apiError = jsErr; + this.name = "JetStreamApiError"; + } + + get code(): number { + return this.#apiError.err_code; + } + + get status(): number { + return this.#apiError.code; + } + + apiError(): ApiError { + return Object.assign({}, this.#apiError); + } +} + +export class ConsumerNotFoundError extends JetStreamApiError { + constructor(jsErr: ApiError, opts?: ErrorOptions) { + super(jsErr, opts); + this.name = "ConsumerNotFoundError"; + } +} + +export class StreamNotFoundError extends JetStreamApiError { + constructor(jsErr: ApiError, opts?: ErrorOptions) { + super(jsErr, opts); + this.name = "StreamNotFoundError"; + } + + static fromMessage(message: string): JetStreamApiError { + return new StreamNotFoundError({ + err_code: JetStreamApiCodes.StreamNotFound, + description: message, + code: 404, + }); + } +} + +export const jserrors = { + InvalidNameError, + ConsumerNotFoundError, + StreamNotFoundError, + JetStreamError, + JetStreamApiError, + JetStreamNotEnabled, +}; diff --git a/jetstream/src/jslister.ts b/jetstream/src/jslister.ts index 2f783a30..02a191f9 100644 --- a/jetstream/src/jslister.ts +++ b/jetstream/src/jslister.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 The NATS Authors + * 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 @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { errors } from "@nats-io/nats-core/internal"; import type { BaseApiClientImpl } from "./jsbaseclient_api.ts"; import type { ApiPaged, @@ -38,7 +39,7 @@ export class ListerImpl implements Lister, AsyncIterable { payload?: unknown, ) { if (!subject) { - throw new Error("subject is required"); + throw errors.InvalidArgumentError.format("subject", "is required"); } this.subject = subject; this.jsm = jsm; diff --git a/jetstream/src/jsm.ts b/jetstream/src/jsm.ts index 9cf213c9..948b28dc 100644 --- a/jetstream/src/jsm.ts +++ b/jetstream/src/jsm.ts @@ -14,13 +14,13 @@ */ import { BaseApiClientImpl } from "./jsbaseclient_api.ts"; -import { DirectMsgHeaders } from "./types.ts"; import type { DirectMsg, DirectStreamAPI, JetStreamOptions, StoredMsg, } from "./types.ts"; +import { DirectMsgHeaders } from "./types.ts"; import type { Codec, Msg, @@ -31,6 +31,7 @@ import type { } from "@nats-io/nats-core"; import { Empty, + errors, QueuedIteratorImpl, RequestStrategy, TD, @@ -40,7 +41,12 @@ import type { DirectMsgRequest, LastForMsgRequest, } from "./jsapi_types.ts"; -import { checkJsError, validateStreamName } from "./jsutil.ts"; +import { validateStreamName } from "./jsutil.ts"; +import { + JetStreamApiCodes, + JetStreamApiError, + JetStreamStatus, +} from "./jserrors.ts"; export class DirectStreamAPIImpl extends BaseApiClientImpl implements DirectStreamAPI { @@ -72,11 +78,24 @@ export class DirectStreamAPIImpl extends BaseApiClientImpl payload, ); - // response is not a JS.API response - const err = checkJsError(r); - if (err) { - return Promise.reject(err); + if (r.headers?.code !== 0) { + const status = new JetStreamStatus(r); + if (status.isMessageNotFound()) { + // this so to simplify things that handle a non-existing messages + // as null (such as KV). + return Promise.reject( + new JetStreamApiError( + { + code: status.code, + err_code: JetStreamApiCodes.NoMessageFound, + description: status.description, + }, + ), + ); + } + return Promise.reject(status.toError()); } + const dm = new DirectMsgImpl(r); return Promise.resolve(dm); } @@ -89,7 +108,9 @@ export class DirectStreamAPIImpl extends BaseApiClientImpl const pre = this.opts.apiPrefix || "$JS.API"; const subj = `${pre}.DIRECT.GET.${stream}`; if (!Array.isArray(opts.multi_last) || opts.multi_last.length === 0) { - return Promise.reject("multi_last is required"); + return Promise.reject( + errors.InvalidArgumentError.format("multi_last", "is required"), + ); } const payload = JSON.stringify(opts, (key, value) => { if (key === "up_to_time" && value instanceof Date) { @@ -111,13 +132,12 @@ export class DirectStreamAPIImpl extends BaseApiClientImpl (async () => { let gotFirst = false; let badServer = false; - let badRequest: string | undefined; + let status: JetStreamStatus | null = null; for await (const m of raw) { if (!gotFirst) { gotFirst = true; - const code = m.headers?.code || 0; - if (code !== 0 && code < 200 || code > 299) { - badRequest = m.headers?.description.toLowerCase(); + status = JetStreamStatus.maybeParseStatus(m); + if (status) { break; } // inspect the message and make sure that we have a supported server @@ -137,12 +157,14 @@ export class DirectStreamAPIImpl extends BaseApiClientImpl if (badServer) { throw new Error("batch direct get not supported by the server"); } - if (badRequest) { - throw new Error(`bad request: ${badRequest}`); + if (status) { + throw status.toError(); } iter.stop(); }); - })(); + })().catch((err) => { + iter.stop(err); + }); return Promise.resolve(iter); } diff --git a/jetstream/src/jsmconsumer_api.ts b/jetstream/src/jsmconsumer_api.ts index 4e3db947..e507ca24 100644 --- a/jetstream/src/jsmconsumer_api.ts +++ b/jetstream/src/jsmconsumer_api.ts @@ -24,7 +24,7 @@ import type { NatsConnection, NatsConnectionImpl, } from "@nats-io/nats-core/internal"; -import { Feature } from "@nats-io/nats-core/internal"; +import { Feature, InvalidArgumentError } from "@nats-io/nats-core/internal"; import { ConsumerApiAction } from "./jsapi_types.ts"; import type { @@ -56,13 +56,15 @@ export class ConsumerAPIImpl extends BaseApiClientImpl implements ConsumerAPI { validateStreamName(stream); if (cfg.deliver_group && cfg.flow_control) { - throw new Error( - "jetstream flow control is not supported with queue groups", + throw InvalidArgumentError.format( + ["flow_control", "deliver_group"], + "are mutually exclusive", ); } if (cfg.deliver_group && cfg.idle_heartbeat) { - throw new Error( - "jetstream idle heartbeat is not supported with queue groups", + throw InvalidArgumentError.format( + ["idle_heartbeat", "deliver_group"], + "are mutually exclusive", ); } @@ -82,7 +84,7 @@ export class ConsumerAPIImpl extends BaseApiClientImpl implements ConsumerAPI { const name = cfg.name === "" ? undefined : cfg.name; if (name && !newAPI) { - throw new Error(`consumer 'name' requires server ${min}`); + throw InvalidArgumentError.format("name", `requires server ${min}`); } if (name) { try { @@ -106,14 +108,17 @@ export class ConsumerAPIImpl extends BaseApiClientImpl implements ConsumerAPI { if (Array.isArray(cfg.filter_subjects)) { const { min, ok } = nci.features.get(Feature.JS_MULTIPLE_CONSUMER_FILTER); if (!ok) { - throw new Error(`consumer 'filter_subjects' requires server ${min}`); + throw InvalidArgumentError.format( + "filter_subjects", + `requires server ${min}`, + ); } newAPI = false; } if (cfg.metadata) { const { min, ok } = nci.features.get(Feature.JS_STREAM_CONSUMER_METADATA); if (!ok) { - throw new Error(`consumer 'metadata' requires server ${min}`); + throw InvalidArgumentError.format("metadata", `requires server ${min}`); } } if (newAPI) { diff --git a/jetstream/src/jsmsg.ts b/jetstream/src/jsmsg.ts index c16d06fc..c39ab45d 100644 --- a/jetstream/src/jsmsg.ts +++ b/jetstream/src/jsmsg.ts @@ -17,7 +17,6 @@ import type { Msg, MsgHdrs, MsgImpl, - NatsError, ProtocolHandler, RequestOptions, } from "@nats-io/nats-core/internal"; @@ -77,7 +76,7 @@ export interface JsMsg { /** * Indicate to the JetStream server that processing of the message - * failed, and that it should be resent after the spefied number of + * failed, and that it should be resent after the specified number of * milliseconds. * @param millis */ @@ -148,7 +147,7 @@ export function parseInfo(s: string): DeliveryInfo { if ( (tokens.length < 11) || tokens[0] !== "$JS" || tokens[1] !== "ACK" ) { - throw new Error(`not js message`); + throw new Error(`unable to parse delivery info - not a jetstream message`); } // old @@ -243,7 +242,7 @@ export class JsMsgImpl implements JsMsg { const proto = mi.publisher as unknown as ProtocolHandler; const trace = !(proto.options?.noAsyncTraces || false); const r = new RequestOne(proto.muxSubscriptions, this.msg.reply, { - timeout: this.timeout, + timeout: opts.timeout, }, trace); proto.request(r); try { @@ -255,13 +254,13 @@ export class JsMsgImpl implements JsMsg { }, ); } catch (err) { - r.cancel(err as NatsError); + r.cancel(err as Error); } try { await Promise.race([r.timer, r.deferred]); d.resolve(true); } catch (err) { - r.cancel(err as NatsError); + r.cancel(err as Error); d.reject(err); } } else { diff --git a/jetstream/src/jsmstream_api.ts b/jetstream/src/jsmstream_api.ts index 547e0097..cb1f4d30 100644 --- a/jetstream/src/jsmstream_api.ts +++ b/jetstream/src/jsmstream_api.ts @@ -13,25 +13,27 @@ * limitations under the License. */ +import type { + Codec, + MsgHdrs, + NatsConnection, + NatsConnectionImpl, + ReviverFn, +} from "@nats-io/nats-core/internal"; import { createInbox, Empty, + errors, Feature, headers, + InvalidArgumentError, MsgHdrsImpl, nanos, nuid, TD, } from "@nats-io/nats-core/internal"; -import type { - Codec, - MsgHdrs, - NatsConnection, - NatsConnectionImpl, - ReviverFn, -} from "@nats-io/nats-core/internal"; -import { BaseApiClientImpl } from "./jsbaseclient_api.ts"; import type { StreamNames } from "./jsbaseclient_api.ts"; +import { BaseApiClientImpl } from "./jsbaseclient_api.ts"; import { ListerImpl } from "./jslister.ts"; import { minValidation, validateStreamName } from "./jsutil.ts"; import type { @@ -50,11 +52,10 @@ import type { StreamAPI, Streams, } from "./types.ts"; - -import { isOrderedPushConsumerOptions } from "./types.ts"; - -import { isBoundPushConsumerOptions } from "./types.ts"; -import { AckPolicy, DeliverPolicy } from "./jsapi_types.ts"; +import { + isBoundPushConsumerOptions, + isOrderedPushConsumerOptions, +} from "./types.ts"; import type { ApiPagedRequest, ConsumerConfig, @@ -76,6 +77,7 @@ import type { StreamUpdateConfig, SuccessResponse, } from "./jsapi_types.ts"; +import { AckPolicy, DeliverPolicy } from "./jsapi_types.ts"; import { PullConsumerImpl } from "./consumer.ts"; import { ConsumerAPIImpl } from "./jsmconsumer_api.ts"; import type { PushConsumerInternalOptions } from "./pushconsumer.ts"; @@ -96,7 +98,10 @@ export function convertStreamSourceDomain(s?: StreamSource) { return copy; } if (copy.external) { - throw new Error("domain and external are both set"); + throw InvalidArgumentError.format( + ["domain", "external"], + "are mutually exclusive", + ); } copy.external = { api: `$JS.${domain}.API` } as ExternalStream; return copy; @@ -212,7 +217,9 @@ export class ConsumersImpl implements Consumers { new PushConsumerImpl(this.api, ci, { bound: true }), ); } else { - return Promise.reject(new Error("deliver_subject is required")); + return Promise.reject( + errors.InvalidArgumentError.format("deliver_subject", "is required"), + ); } } @@ -584,7 +591,10 @@ export class StreamAPIImpl extends BaseApiClientImpl implements StreamAPI { if (opts) { const { keep, seq } = opts as PurgeBySeq & PurgeTrimOpts; if (typeof keep === "number" && typeof seq === "number") { - throw new Error("can specify one of keep or seq"); + throw InvalidArgumentError.format( + ["keep", "seq"], + "are mutually exclusive", + ); } } validateStreamName(name); diff --git a/jetstream/src/jsutil.ts b/jetstream/src/jsutil.ts index 830babbf..03d950de 100644 --- a/jetstream/src/jsutil.ts +++ b/jetstream/src/jsutil.ts @@ -12,20 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - Empty, - ErrorCode, - headers, - MsgImpl, - NatsError, -} from "@nats-io/nats-core/internal"; - -import type { - Msg, - MsgArg, - MsgHdrsImpl, - Publisher, -} from "@nats-io/nats-core/internal"; export function validateDurableName(name?: string) { return minValidation("durable", name); @@ -91,133 +77,3 @@ export function validName(name = ""): string { } return ""; } - -/** - * Returns true if the message is a flow control message - * @param msg - */ -export function isFlowControlMsg(msg: Msg): boolean { - if (msg.data.length > 0) { - return false; - } - const h = msg.headers; - if (!h) { - return false; - } - return h.code >= 100 && h.code < 200; -} - -/** - * Returns true if the message is a heart beat message - * @param msg - */ -export function isHeartbeatMsg(msg: Msg): boolean { - return isFlowControlMsg(msg) && msg.headers?.description === "Idle Heartbeat"; -} - -export function newJsErrorMsg( - code: number, - description: string, - subject: string, -): Msg { - const h = headers(code, description) as MsgHdrsImpl; - - const arg = { hdr: 1, sid: 0, size: 0 } as MsgArg; - const msg = new MsgImpl(arg, Empty, {} as Publisher); - msg._headers = h; - msg._subject = subject; - - return msg; -} - -export function checkJsError(msg: Msg): NatsError | null { - // JS error only if no payload - otherwise assume it is application data - if (msg.data.length !== 0) { - return null; - } - const h = msg.headers; - if (!h) { - return null; - } - return checkJsErrorCode(h.code, h.description); -} - -export enum Js409Errors { - MaxBatchExceeded = "exceeded maxrequestbatch of", - MaxExpiresExceeded = "exceeded maxrequestexpires of", - MaxBytesExceeded = "exceeded maxrequestmaxbytes of", - MaxMessageSizeExceeded = "message size exceeds maxbytes", - PushConsumer = "consumer is push based", - MaxWaitingExceeded = "exceeded maxwaiting", // not terminal - IdleHeartbeatMissed = "idle heartbeats missed", - ConsumerDeleted = "consumer deleted", - // FIXME: consumer deleted - instead of no responder (terminal error) - // leadership changed - -} - -let MAX_WAITING_FAIL = false; -export function setMaxWaitingToFail(tf: boolean) { - MAX_WAITING_FAIL = tf; -} - -export function isTerminal409(err: NatsError): boolean { - if (err.code !== ErrorCode.JetStream409) { - return false; - } - const fatal = [ - Js409Errors.MaxBatchExceeded, - Js409Errors.MaxExpiresExceeded, - Js409Errors.MaxBytesExceeded, - Js409Errors.MaxMessageSizeExceeded, - Js409Errors.PushConsumer, - Js409Errors.IdleHeartbeatMissed, - Js409Errors.ConsumerDeleted, - ]; - if (MAX_WAITING_FAIL) { - fatal.push(Js409Errors.MaxWaitingExceeded); - } - - return fatal.find((s) => { - return err.message.indexOf(s) !== -1; - }) !== undefined; -} - -export function checkJsErrorCode( - code: number, - description = "", -): NatsError | null { - if (code < 300) { - return null; - } - description = description.toLowerCase(); - switch (code) { - case 404: - // 404 for jetstream will provide different messages ensure we - // keep whatever the server returned - return new NatsError(description, ErrorCode.JetStream404NoMessages); - case 408: - return new NatsError(description, ErrorCode.JetStream408RequestTimeout); - case 409: { - // the description can be exceeded max waiting or max ack pending, which are - // recoverable, but can also be terminal errors where the request exceeds - // some value in the consumer configuration - const ec = description.startsWith(Js409Errors.IdleHeartbeatMissed) - ? ErrorCode.JetStreamIdleHeartBeat - : ErrorCode.JetStream409; - return new NatsError( - description, - ec, - ); - } - case 503: - return NatsError.errorForCode( - ErrorCode.JetStreamNotEnabled, - new Error(description), - ); - default: - if (description === "") { - description = ErrorCode.Unknown; - } - return new NatsError(description, `${code}`); - } -} diff --git a/jetstream/src/mod.ts b/jetstream/src/mod.ts index 50ecb368..3bcb2d7e 100644 --- a/jetstream/src/mod.ts +++ b/jetstream/src/mod.ts @@ -12,13 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { - checkJsError, - isFlowControlMsg, - isHeartbeatMsg, - jetstream, - jetstreamManager, -} from "./internal_mod.ts"; +export { jetstream, jetstreamManager } from "./internal_mod.ts"; export { AckPolicy, @@ -30,6 +24,9 @@ export { DiscardPolicy, isPullConsumer, isPushConsumer, + JetStreamApiCodes, + JetStreamApiError, + JetStreamError, JsHeaders, ReplayPolicy, RepublishHeaders, diff --git a/jetstream/src/pushconsumer.ts b/jetstream/src/pushconsumer.ts index 985cce0e..4155e717 100644 --- a/jetstream/src/pushconsumer.ts +++ b/jetstream/src/pushconsumer.ts @@ -1,3 +1,18 @@ +/* + * 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 { toJsMsg } from "./jsmsg.ts"; import type { JsMsg } from "./jsmsg.ts"; import { AckPolicy, DeliverPolicy } from "./jsapi_types.ts"; @@ -17,11 +32,11 @@ import { backoff, createInbox, delay, + errors, Events, IdleHeartbeatMonitor, millis, nanos, - NatsError, nuid, QueuedIteratorImpl, } from "@nats-io/nats-core/internal"; @@ -32,7 +47,7 @@ import type { Status, Subscription, } from "@nats-io/nats-core/internal"; -import { isFlowControlMsg, isHeartbeatMsg } from "./mod.ts"; +import { JetStreamStatus } from "./jserrors.ts"; export class PushConsumerMessagesImpl extends QueuedIteratorImpl implements ConsumerMessages { @@ -123,18 +138,18 @@ export class PushConsumerMessagesImpl extends QueuedIteratorImpl this.stop(err); } const bo = backoff(); + const c = delay(bo.backoff(this.createFails)); c.then(() => { - const idx = this.cancelables.indexOf(c); - if (idx !== -1) { - this.cancelables = this.cancelables.splice(idx, idx); - } if (!this.done) { this.reset(); } - }) - .catch((_) => { - // canceled + }).catch(() => {}) + .finally(() => { + const idx = this.cancelables.indexOf(c); + if (idx !== -1) { + this.cancelables = this.cancelables.splice(idx, idx); + } }); this.cancelables.push(c); }); @@ -268,7 +283,8 @@ export class PushConsumerMessagesImpl extends QueuedIteratorImpl const isProtocol = msg.subject === subject; if (isProtocol) { - if (isHeartbeatMsg(msg)) { + const status = new JetStreamStatus(msg); + if (status.isIdleHeartbeat()) { const natsLastConsumer = msg.headers?.get("Nats-Last-Consumer"); const natsLastStream = msg.headers?.get("Nats-Last-Stream"); this.notify(ConsumerDebugEvents.Heartbeat, { @@ -277,7 +293,8 @@ export class PushConsumerMessagesImpl extends QueuedIteratorImpl }); return; } - if (isFlowControlMsg(msg)) { + if (status.isFlowControlRequest()) { + status.debug(); this._push(() => { msg.respond(); this.notify(ConsumerDebugEvents.FlowControl, null); @@ -285,11 +302,10 @@ export class PushConsumerMessagesImpl extends QueuedIteratorImpl return; } - const code = msg.headers?.code; - const description = msg.headers?.description?.toLowerCase() || - "unknown"; + const code = status.code; + const description = status.description; - if (code === 409 && description === "consumer deleted") { + if (status.isConsumerDeleted()) { this.notify( ConsumerEvents.ConsumerDeleted, `${code} ${description}`, @@ -297,8 +313,7 @@ export class PushConsumerMessagesImpl extends QueuedIteratorImpl } if (this.abortOnMissingResource) { this._push(() => { - const error = new NatsError(description, `${code}`); - this.stop(error); + this.stop(status.toError()); }); return; } @@ -386,7 +401,9 @@ export class PushConsumerImpl implements PushConsumer { userOptions: Partial = {}, ): Promise { if (this.started) { - return Promise.reject(new Error("consumer already started")); + return Promise.reject( + new errors.InvalidOperationError("consumer already started"), + ); } if (!this._info.config.deliver_subject) { @@ -395,7 +412,9 @@ export class PushConsumerImpl implements PushConsumer { ); } if (!this._info.config.deliver_group && this._info.push_bound) { - return Promise.reject(new Error("consumer is already bound")); + return Promise.reject( + new errors.InvalidOperationError("consumer is already bound"), + ); } const v = new PushConsumerMessagesImpl(this, userOptions, this.opts); this.started = true; @@ -407,7 +426,9 @@ export class PushConsumerImpl implements PushConsumer { delete(): Promise { if (this.bound) { - return Promise.reject(new Error("bound consumers cannot delete")); + return Promise.reject( + new errors.InvalidOperationError("bound consumers cannot delete"), + ); } const { stream_name, name } = this._info; return this.api.delete(stream_name, name); @@ -415,7 +436,9 @@ export class PushConsumerImpl implements PushConsumer { async info(cached?: boolean): Promise { if (this.bound) { - return Promise.reject(new Error("bound consumers cannot info")); + return Promise.reject( + new errors.InvalidOperationError("bound consumers cannot info"), + ); } if (cached) { return Promise.resolve(this._info); diff --git a/jetstream/src/types.ts b/jetstream/src/types.ts index 97521133..957cf574 100644 --- a/jetstream/src/types.ts +++ b/jetstream/src/types.ts @@ -544,7 +544,7 @@ export enum ConsumerDebugEvents { /** * Notifies that the client received a server-side heartbeat. The payload the data - * portion has the format `{natsLastConsumer: string, natsLastStream: string}`; + * portion has the format `{natsLastConsumer: number, natsLastStream: number}`; */ Heartbeat = "heartbeat", @@ -659,7 +659,7 @@ export function isPushConsumer(v: PushConsumer | Consumer): v is PushConsumer { export interface JetStreamClient { /** * Publishes a message to a stream. If not stream is configured to store the message, the - * request will fail with {@link ErrorCode.NoResponders} error. + * request will fail with RequestError error with a nested NoRespondersError. * * @param subj - the subject for the message * @param payload - the message's data @@ -786,7 +786,10 @@ export interface DirectStreamAPI { * @param stream * @param query */ - getMessage(stream: string, query: DirectMsgRequest): Promise; + getMessage( + stream: string, + query: DirectMsgRequest, + ): Promise; /** * Retrieves all last subject messages for the specified subjects diff --git a/jetstream/tests/consume_test.ts b/jetstream/tests/consume_test.ts index 3104108f..387e22a3 100644 --- a/jetstream/tests/consume_test.ts +++ b/jetstream/tests/consume_test.ts @@ -32,6 +32,7 @@ import { deadline, deferred, delay, + errors, nanos, syncIterator, } from "@nats-io/nats-core"; @@ -93,8 +94,8 @@ Deno.test("consumers - consume callback rejects iter", async () => { // should fail } }, - Error, - "unsupported iterator", + errors.InvalidOperationError, + "iterator cannot be used when a callback is registered", ); iter.stop(); diff --git a/jetstream/tests/consumers_ordered_test.ts b/jetstream/tests/consumers_ordered_test.ts index 025828af..d41e8e38 100644 --- a/jetstream/tests/consumers_ordered_test.ts +++ b/jetstream/tests/consumers_ordered_test.ts @@ -43,6 +43,7 @@ import type { import { StreamImpl } from "../src/jsmstream_api.ts"; import { delayUntilAssetNotFound } from "./util.ts"; import { flakyTest } from "../../test_helpers/mod.ts"; +import { ConsumerNotFoundError } from "../src/jserrors.ts"; Deno.test("ordered consumers - get", async () => { const { ns, nc } = await _setup(connect, jetstreamServerConf()); @@ -875,7 +876,7 @@ Deno.test("ordered consumers - bind is rejected", async () => { return c.next({ bind: true }); }, Error, - "bind is not supported", + "'bind' is not supported", ); await assertRejects( @@ -883,7 +884,7 @@ Deno.test("ordered consumers - bind is rejected", async () => { return c.fetch({ bind: true }); }, Error, - "bind is not supported", + "'bind' is not supported", ); await assertRejects( @@ -891,7 +892,7 @@ Deno.test("ordered consumers - bind is rejected", async () => { return c.consume({ bind: true }); }, Error, - "bind is not supported", + "'bind' is not supported", ); await cleanup(ns, nc); @@ -1106,14 +1107,16 @@ Deno.test("ordered consumers - initial creation fails, consumer fails", async () Deno.test( "ordered consumers - stale reference recovers", - flakyTest(async () => { + async () => { const { ns, nc } = await _setup(connect, jetstreamServerConf()); const jsm = await jetstreamManager(nc); await jsm.streams.add({ name: "A", subjects: ["a"] }); const js = jsm.jetstream(); - await js.publish("a", JSON.stringify(1)); - await js.publish("a", JSON.stringify(2)); + 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 }); @@ -1124,23 +1127,21 @@ Deno.test( // continue until the server says the consumer doesn't exist await delayUntilAssetNotFound(c); - // so should get that error once + // so should get CnF once await assertRejects( () => { return c.next({ expires: 1000 }); }, - Error, - "consumer not found", + ConsumerNotFoundError, ); // but now it will be created in line - m = await c.next({ expires: 1000 }); assertExists(m); assertEquals(m.json(), 2); await cleanup(ns, nc); - }), + }, ); Deno.test( diff --git a/jetstream/tests/consumers_test.ts b/jetstream/tests/consumers_test.ts index a286f864..c63a67af 100644 --- a/jetstream/tests/consumers_test.ts +++ b/jetstream/tests/consumers_test.ts @@ -258,7 +258,7 @@ Deno.test("consumers - bad options", async () => { await c.consume({ expires: 500 }); }, Error, - "expires should be at least 1000ms", + "'expires' must be at least 1000ms", ); await cleanup(ns, nc); diff --git a/jetstream/tests/jetstream_pushconsumer_test.ts b/jetstream/tests/jetstream_pushconsumer_test.ts index 3101428b..442748bf 100644 --- a/jetstream/tests/jetstream_pushconsumer_test.ts +++ b/jetstream/tests/jetstream_pushconsumer_test.ts @@ -30,6 +30,7 @@ import { delay, Empty, Events, + InvalidArgumentError, nanos, nuid, syncIterator, @@ -107,7 +108,7 @@ Deno.test("jetstream - queue error checks", async () => { }); }, Error, - "jetstream idle heartbeat is not supported with queue groups", + "'idle_heartbeat','deliver_group' are mutually exclusive", undefined, ); @@ -120,9 +121,8 @@ Deno.test("jetstream - queue error checks", async () => { flow_control: true, }); }, - Error, - "jetstream flow control is not supported with queue groups", - undefined, + InvalidArgumentError, + "'flow_control','deliver_group' are mutually exclusive", ); await cleanup(ns, nc); diff --git a/jetstream/tests/jetstream_test.ts b/jetstream/tests/jetstream_test.ts index 9a8c1f86..0a5579e3 100644 --- a/jetstream/tests/jetstream_test.ts +++ b/jetstream/tests/jetstream_test.ts @@ -26,7 +26,6 @@ import { } from "../src/mod.ts"; import type { Advisory } from "../src/mod.ts"; -import type { NatsError } from "@nats-io/nats-core/internal"; import { deferred, delay, @@ -53,6 +52,7 @@ import { notCompatible, } from "test_helpers"; import { PubHeaders } from "../src/jsapi_types.ts"; +import { JetStreamApiError } from "../src/jserrors.ts"; Deno.test("jetstream - default options", () => { const opts = defaultJsOptions(); @@ -747,19 +747,11 @@ Deno.test("jetstream - detailed errors", async () => { num_replicas: 3, subjects: ["foo"], }); - }) as NatsError; + }, JetStreamApiError); - assert(ne.api_error); - assertEquals( - ne.message, - "replicas > 1 not supported in non-clustered mode", - ); - assertEquals( - ne.api_error.description, - "replicas > 1 not supported in non-clustered mode", - ); - assertEquals(ne.api_error.code, 500); - assertEquals(ne.api_error.err_code, 10074); + assertEquals(ne.message, "replicas > 1 not supported in non-clustered mode"); + assertEquals(ne.code, 10074); + assertEquals(ne.status, 500); await cleanup(ns, nc); }); diff --git a/jetstream/tests/jscluster_test.ts b/jetstream/tests/jscluster_test.ts index a43e4a27..3594da7b 100644 --- a/jetstream/tests/jscluster_test.ts +++ b/jetstream/tests/jscluster_test.ts @@ -1,5 +1,12 @@ import { jetstream, jetstreamManager } from "../src/jsclient.ts"; -import { connect, flakyTest, NatsServer, notCompatible } from "test_helpers"; +import { + cleanup, + connect, + flakyTest, + jetstreamServerConf, + NatsServer, + notCompatible, +} from "test_helpers"; import { DiscardPolicy, RetentionPolicy, @@ -15,7 +22,6 @@ import { assertRejects, fail, } from "jsr:@std/assert"; -import { cleanup, jetstreamServerConf } from "../../test_helpers/mod.ts"; Deno.test("jetstream - mirror alternates", async () => { const servers = await NatsServer.jetstreamCluster(3); diff --git a/jetstream/tests/jsm_test.ts b/jetstream/tests/jsm_test.ts index a2ff776e..e542fed2 100644 --- a/jetstream/tests/jsm_test.ts +++ b/jetstream/tests/jsm_test.ts @@ -24,12 +24,13 @@ import { import type { NatsConnectionImpl } from "@nats-io/nats-core/internal"; import { Feature } from "@nats-io/nats-core/internal"; -import type { NatsConnection, NatsError } from "@nats-io/nats-core"; +import type { NatsConnection } from "@nats-io/nats-core"; import { deferred, Empty, - ErrorCode, + errors, headers, + InvalidArgumentError, jwtAuthenticator, nanos, nkeys, @@ -55,7 +56,6 @@ import { import { initStream } from "./jstest_util.ts"; import { _setup, - assertThrowsAsyncErrorCode, cleanup, connect, flakyTest, @@ -75,6 +75,7 @@ import type { ConsumerAPIImpl } from "../src/jsmconsumer_api.ts"; import { ConsumerApiAction, StoreCompression } from "../src/jsapi_types.ts"; import type { JetStreamManagerImpl } from "../src/jsclient.ts"; import { stripNatsMetadata } from "./util.ts"; +import { jserrors } from "../src/jserrors.ts"; const StreamNameRequired = "stream name required"; const ConsumerNameRequired = "durable name required"; @@ -82,9 +83,12 @@ const ConsumerNameRequired = "durable name required"; Deno.test("jsm - jetstream not enabled", async () => { // start a regular server - no js conf const { ns, nc } = await _setup(connect); - await assertThrowsAsyncErrorCode(async () => { - await jetstreamManager(nc); - }, ErrorCode.JetStreamNotEnabled); + await assertRejects( + () => { + return jetstreamManager(nc); + }, + jserrors.JetStreamNotEnabled, + ); await cleanup(ns, nc); }); @@ -110,9 +114,12 @@ Deno.test("jsm - account not enabled", async () => { }, }; const { ns, nc } = await _setup(connect, jetstreamServerConf(conf)); - await assertThrowsAsyncErrorCode(async () => { - await jetstreamManager(nc); - }, ErrorCode.JetStreamNotEnabled); + await assertRejects( + () => { + return jetstreamManager(nc); + }, + jserrors.JetStreamNotEnabled, + ); const a = await connect( { port: ns.port, user: "a", pass: "a" }, @@ -208,9 +215,7 @@ Deno.test("jsm - info msg not found stream name fails", async () => { async () => { await jsm.streams.info(name); }, - Error, - "stream not found", - undefined, + jserrors.StreamNotFoundError, ); await cleanup(ns, nc); }); @@ -237,9 +242,7 @@ Deno.test("jsm - delete msg not found stream name fails", async () => { async () => { await jsm.streams.deleteMessage(name, 1); }, - Error, - "stream not found", - undefined, + jserrors.StreamNotFoundError, ); await cleanup(ns, nc); }); @@ -310,9 +313,7 @@ Deno.test("jsm - purge not found stream name fails", async () => { async () => { await jsm.streams.purge(name); }, - Error, - "stream not found", - undefined, + jserrors.StreamNotFoundError, ); await cleanup(ns, nc); }); @@ -541,7 +542,7 @@ Deno.test("jsm - purge seq and keep fails", async () => { return jsm.streams.purge("a", { keep: 10, seq: 5 }); }, Error, - "can specify one of keep or seq", + "'keep','seq' are mutually exclusive", ); await cleanup(ns, nc); }); @@ -557,8 +558,7 @@ Deno.test("jsm - stream delete", async () => { async () => { await jsm.streams.info(stream); }, - Error, - "stream not found", + jserrors.StreamNotFoundError, ); await cleanup(ns, nc); }); @@ -638,9 +638,7 @@ Deno.test("jsm - consumer info on not found stream fails", async () => { async () => { await jsm.consumers.info("foo", "dur"); }, - Error, - "stream not found", - undefined, + jserrors.StreamNotFoundError, ); await cleanup(ns, nc); }); @@ -653,9 +651,7 @@ Deno.test("jsm - consumer info on not found consumer", async () => { async () => { await jsm.consumers.info(stream, "dur"); }, - Error, - "consumer not found", - undefined, + jserrors.ConsumerNotFoundError, ); await cleanup(ns, nc); }); @@ -1016,25 +1012,19 @@ Deno.test( Deno.test("jsm - jetstream error info", async () => { const { ns, nc } = await _setup(connect, jetstreamServerConf({})); const jsm = await jetstreamManager(nc); - try { - await jsm.streams.add({ - name: "a", - num_replicas: 3, - subjects: ["a.>"], - }); - fail("should have failed"); - } catch (err) { - const ne = err as NatsError; - assert(ne.isJetStreamError()); - const jerr = ne.jsError(); - assert(jerr); - assertEquals(jerr.code, 500); - assertEquals(jerr.err_code, 10074); - assertEquals( - jerr.description, - "replicas > 1 not supported in non-clustered mode", - ); - } + 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); }); @@ -1204,18 +1194,23 @@ Deno.test("jsm - direct getMessage", async () => { await js.publish("foo", "e", { expect: { lastSequence: 4 } }); let m = await jsm.direct.getMessage("A", { seq: 0, next_by_subj: "bar" }); + assertExists(m); assertEquals(m.seq, 4); m = await jsm.direct.getMessage("A", { last_by_subj: "foo" }); + assertExists(m); assertEquals(m.seq, 5); m = await jsm.direct.getMessage("A", { seq: 0, next_by_subj: "foo" }); + assertExists(m); assertEquals(m.seq, 1); m = await jsm.direct.getMessage("A", { seq: 4, next_by_subj: "foo" }); + assertExists(m); assertEquals(m.seq, 5); m = await jsm.direct.getMessage("A", { seq: 2, next_by_subj: "foo" }); + assertExists(m); assertEquals(m.seq, 2); await cleanup(ns, nc); @@ -1340,8 +1335,8 @@ async function testConsumerNameAPI(nc: NatsConnection) { name: "a", }, "$JS.API.CONSUMER.CREATE.A"); }, - Error, - "consumer 'name' requires server", + errors.InvalidArgumentError, + "'name' requires server", ); const ci = await addC({ @@ -1950,11 +1945,13 @@ Deno.test("jsm - direct msg decode", async () => { await js.publish("a.a", "hello"); await js.publish("a.a", JSON.stringify({ one: "two", a: [1, 2, 3] })); - assertEquals( - (await jsm.direct.getMessage(name, { seq: 1 })).string(), - "hello", - ); - assertEquals((await jsm.direct.getMessage(name, { seq: 2 })).json(), { + let m = await jsm.direct.getMessage(name, { seq: 1 }); + assertExists(m); + assertEquals(m.string(), "hello"); + + m = await jsm.direct.getMessage(name, { seq: 2 }); + assertExists(m); + assertEquals(m.json(), { one: "two", a: [1, 2, 3], }); @@ -2070,8 +2067,8 @@ Deno.test("jsm - stream/consumer metadata", async () => { async () => { await addConsumer(stream, consumer, { hello: "world" }); }, - Error, - "consumer 'metadata' requires server 2.10.0", + InvalidArgumentError, + "'metadata' requires server", ); // add w/o metadata await addConsumer(stream, consumer); @@ -2080,8 +2077,8 @@ Deno.test("jsm - stream/consumer metadata", async () => { async () => { await updateConsumer(stream, consumer, { hello: "world" }); }, - Error, - "consumer 'metadata' requires server 2.10.0", + InvalidArgumentError, + "'metadata' requires server", ); await cleanup(ns, nc); @@ -2132,7 +2129,6 @@ Deno.test("jsm - validate stream name in operations", async () => { const jsm = await jetstreamManager(nc); const names = ["", ".", "*", ">", "\r", "\n", "\t", " "]; - type fn = (name: string) => Promise; const tests = [ { name: "add stream", diff --git a/jetstream/tests/jsmsg_test.ts b/jetstream/tests/jsmsg_test.ts index ab7c7090..2d7a3b43 100644 --- a/jetstream/tests/jsmsg_test.ts +++ b/jetstream/tests/jsmsg_test.ts @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assertEquals, assertRejects, fail } from "jsr:@std/assert"; +import { assert, assertEquals, assertRejects, fail } from "jsr:@std/assert"; import { AckPolicy, jetstream, @@ -21,8 +21,7 @@ import { } from "../src/mod.ts"; import { createInbox, Empty, nanos } from "@nats-io/nats-core"; -import type { Msg } from "@nats-io/nats-core"; -import type { MsgImpl } from "@nats-io/nats-core/internal"; +import type { Msg, MsgImpl } from "@nats-io/nats-core/internal"; import type { JsMsgImpl } from "../src/jsmsg.ts"; import { parseInfo, toJsMsg } from "../src/jsmsg.ts"; @@ -34,6 +33,7 @@ import { jetstreamServerConf, } from "test_helpers"; import type { JetStreamManagerImpl } from "../src/jsclient.ts"; +import { errors } from "../../core/src/mod.ts"; Deno.test("jsmsg - parse", () => { // "$JS.ACK....." @@ -159,14 +159,15 @@ Deno.test("jsmsg - no ack consumer is ackAck 503", async () => { const c = await js.consumers.get("A", "a"); const jm = await c.next(); - await assertRejects( + const err = await assertRejects( (): Promise => { return jm!.ackAck(); }, - Error, - "503", + errors.RequestError, ); + assert(err.isNoResponders()); + await cleanup(ns, nc); }); @@ -217,12 +218,11 @@ Deno.test("jsmsg - explicit consumer ackAck timeout", async () => { const start = Date.now(); await assertRejects( (): Promise => { - return jm!.ackAck({ timeout: 1500 }); + return jm!.ackAck({ timeout: 1000 }); }, - Error, - "TIMEOUT", + errors.TimeoutError, ); - assertBetween(Date.now() - start, 1300, 1700); + assertBetween(Date.now() - start, 1000, 1500); await cleanup(ns, nc); }); @@ -252,8 +252,7 @@ Deno.test("jsmsg - ackAck js options timeout", async () => { (): Promise => { return jm!.ackAck(); }, - Error, - "TIMEOUT", + errors.TimeoutError, ); assertBetween(Date.now() - start, 1300, 1700); @@ -285,8 +284,7 @@ Deno.test("jsmsg - ackAck legacy timeout", async () => { (): Promise => { return jm!.ackAck(); }, - Error, - "TIMEOUT", + errors.TimeoutError, ); assertBetween(Date.now() - start, 1300, 1700); diff --git a/jetstream/tests/next_test.ts b/jetstream/tests/next_test.ts index bce359cd..a41c298d 100644 --- a/jetstream/tests/next_test.ts +++ b/jetstream/tests/next_test.ts @@ -27,6 +27,11 @@ import { delay, nanos } from "@nats-io/nats-core"; import type { NatsConnectionImpl } from "@nats-io/nats-core/internal"; import { jetstream, jetstreamManager } from "../src/mod.ts"; import { delayUntilAssetNotFound } from "./util.ts"; +import { + ConsumerNotFoundError, + JetStreamStatusError, + StreamNotFoundError, +} from "../src/jserrors.ts"; Deno.test("next - basics", async () => { const { ns, nc } = await _setup(connect, jetstreamServerConf()); @@ -140,8 +145,7 @@ Deno.test( () => { return c.next({ expires: 1000 }); }, - Error, - "consumer not found", + ConsumerNotFoundError, ); await cleanup(ns, nc); @@ -167,7 +171,7 @@ Deno.test("next - deleted consumer", async () => { () => { return c.next({ expires: 4000 }); }, - Error, + JetStreamStatusError, "consumer deleted", ); await delay(1000); @@ -180,7 +184,7 @@ Deno.test("next - deleted consumer", async () => { Deno.test( "next - stream not found", - flakyTest(async () => { + async () => { const { ns, nc } = await _setup(connect, jetstreamServerConf()); const jsm = await jetstreamManager(nc); @@ -202,14 +206,13 @@ Deno.test( await assertRejects( () => { - return c.next({ expires: 4000 }); + return c.next({ expires: 1000 }); }, - Error, - "stream not found", + StreamNotFoundError, ); await cleanup(ns, nc); - }), + }, ); Deno.test("next - consumer bind", async () => { diff --git a/jetstream/tests/pushconsumers_ordered_test.ts b/jetstream/tests/pushconsumers_ordered_test.ts index 2236f589..2fe6ee73 100644 --- a/jetstream/tests/pushconsumers_ordered_test.ts +++ b/jetstream/tests/pushconsumers_ordered_test.ts @@ -21,6 +21,7 @@ import { _setup, cleanup, connect, + flakyTest, jetstreamServerConf, notCompatible, } from "test_helpers"; @@ -30,7 +31,6 @@ import type { } from "../src/pushconsumer.ts"; import { delay } from "@nats-io/nats-core/internal"; import type { NatsConnectionImpl } from "@nats-io/nats-core/internal"; -import { flakyTest } from "../../test_helpers/mod.ts"; Deno.test("ordered push consumers - get", async () => { const { ns, nc } = await _setup(connect, jetstreamServerConf()); diff --git a/jetstream/tests/streams_test.ts b/jetstream/tests/streams_test.ts index 25eb68d1..0efbb968 100644 --- a/jetstream/tests/streams_test.ts +++ b/jetstream/tests/streams_test.ts @@ -13,7 +13,13 @@ * limitations under the License. */ -import { connect, notCompatible } from "test_helpers"; +import { + _setup, + cleanup, + connect, + jetstreamServerConf, + notCompatible, +} from "test_helpers"; import { AckPolicy, jetstream, jetstreamManager } from "../src/mod.ts"; import { @@ -21,7 +27,6 @@ import { assertExists, assertRejects, } from "https://deno.land/std@0.221.0/assert/mod.ts"; -import { _setup, cleanup, jetstreamServerConf } from "test_helpers"; import { initStream } from "./jstest_util.ts"; import type { NatsConnectionImpl } from "@nats-io/nats-core/internal"; diff --git a/jetstream/tests/util.ts b/jetstream/tests/util.ts index e1c7a241..28047d12 100644 --- a/jetstream/tests/util.ts +++ b/jetstream/tests/util.ts @@ -1,7 +1,23 @@ +/* + * 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 { fail } from "node:assert"; import type { Consumer, Stream } from "../src/types.ts"; +import { fail } from "jsr:@std/assert"; import { StreamImpl } from "../src/jsmstream_api.ts"; +import { ConsumerNotFoundError, StreamNotFoundError } from "../src/jserrors.ts"; export function stripNatsMetadata(md?: Record) { if (md) { @@ -17,17 +33,20 @@ export async function delayUntilAssetNotFound( a: Consumer | Stream, ): Promise { const expected = a instanceof StreamImpl ? "stream" : "consumer"; - const m = `${expected} not found`; while (true) { try { await a.info(); await delay(20); } catch (err) { - if ((err as Error).message === m) { + if (err instanceof ConsumerNotFoundError && expected === "consumer") { + await delay(1000); + break; + } + if (err instanceof StreamNotFoundError && expected === "stream") { + await delay(1000); break; - } else { - fail((err as Error).message); } + fail((err as Error).message); } } } diff --git a/kv/deno.json b/kv/deno.json index dbbccd06..ac60b43e 100644 --- a/kv/deno.json +++ b/kv/deno.json @@ -1,6 +1,6 @@ { "name": "@nats-io/kv", - "version": "3.0.0-12", + "version": "3.0.0-16", "exports": { ".": "./src/mod.ts", "./internal": "./src/internal_mod.ts" @@ -33,7 +33,7 @@ "test": "deno test -A --parallel --reload --quiet tests/ --import-map=import_map.json" }, "imports": { - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30", - "@nats-io/jetstream": "jsr:@nats-io/jetstream@~3.0.0-15" + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34", + "@nats-io/jetstream": "jsr:@nats-io/jetstream@~3.0.0-21" } } diff --git a/kv/import_map.json b/kv/import_map.json index b0d4b9ee..3b2e41e4 100644 --- a/kv/import_map.json +++ b/kv/import_map.json @@ -1,9 +1,9 @@ { "imports": { - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30", - "@nats-io/nats-core/internal": "jsr:@nats-io/nats-core@~3.0.0-30/internal", - "@nats-io/jetstream": "jsr:@nats-io/jetstream@~3.0.0-15", - "@nats-io/jetstream/internal": "jsr:@nats-io/jetstream@~3.0.0-15/internal", + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34", + "@nats-io/nats-core/internal": "jsr:@nats-io/nats-core@~3.0.0-34/internal", + "@nats-io/jetstream": "jsr:@nats-io/jetstream@~3.0.0-21", + "@nats-io/jetstream/internal": "jsr:@nats-io/jetstream@~3.0.0-21/internal", "test_helpers": "../test_helpers/mod.ts", "@nats-io/nkeys": "jsr:@nats-io/nkeys@1.2.0-4", "@nats-io/nuid": "jsr:@nats-io/nuid@2.0.1-2", diff --git a/kv/package.json b/kv/package.json index 2c9c691a..ff1c99f5 100644 --- a/kv/package.json +++ b/kv/package.json @@ -1,6 +1,6 @@ { "name": "@nats-io/kv", - "version": "3.0.0-12", + "version": "3.0.0-16", "files": [ "lib/", "LICENSE", @@ -34,8 +34,8 @@ }, "description": "kv library - this library implements all the base functionality for NATS KV javascript clients", "dependencies": { - "@nats-io/jetstream": "~3.0.0-15", - "@nats-io/nats-core": "~3.0.0-30" + "@nats-io/jetstream": "3.0.0-21", + "@nats-io/nats-core": "3.0.0-34" }, "devDependencies": { "@types/node": "^22.7.6", diff --git a/kv/src/kv.ts b/kv/src/kv.ts index 4f1839d2..9878a96c 100644 --- a/kv/src/kv.ts +++ b/kv/src/kv.ts @@ -16,7 +16,6 @@ import { compare, Empty, - ErrorCode, Feature, headers, millis, @@ -30,7 +29,6 @@ import type { MsgHdrs, NatsConnection, NatsConnectionImpl, - NatsError, Payload, QueuedIterator, } from "@nats-io/nats-core/internal"; @@ -39,6 +37,8 @@ import { AckPolicy, DeliverPolicy, DiscardPolicy, + JetStreamApiCodes, + JetStreamApiError, JsHeaders, ListerImpl, PubHeaders, @@ -567,8 +567,11 @@ export class Bucket implements KV, KvRemove { return Promise.resolve(n); } catch (err) { firstErr = err; - if ((err as NatsError)?.api_error?.err_code !== 10071) { - return Promise.reject(err); + if (err instanceof JetStreamApiError) { + const jserr = err as JetStreamApiError; + if (jserr.code !== JetStreamApiCodes.StreamWrongLastSequence) { + return Promise.reject(err); + } } } let rev = 0; @@ -610,12 +613,6 @@ export class Bucket implements KV, KvRemove { const pa = await this.js.publish(this.subjectForKey(ek, true), data, o); return pa.seq; } catch (err) { - const ne = err as NatsError; - if (ne.isJetStreamError()) { - ne.message = ne.api_error?.description!; - ne.code = `${ne.api_error?.code!}`; - return Promise.reject(ne); - } return Promise.reject(err); } } @@ -647,10 +644,11 @@ export class Bucket implements KV, KvRemove { } return ke; } catch (err) { - if ( - (err as NatsError).code === ErrorCode.JetStream404NoMessages - ) { - return null; + if (err instanceof JetStreamApiError) { + const jserr = err as JetStreamApiError; + if (jserr.code === JetStreamApiCodes.NoMessageFound) { + return null; + } } throw err; } diff --git a/kv/tests/kv_test.ts b/kv/tests/kv_test.ts index c004636b..d84962f7 100644 --- a/kv/tests/kv_test.ts +++ b/kv/tests/kv_test.ts @@ -1635,8 +1635,8 @@ Deno.test("kv - create after delete", async () => { const kv = await new Kvm(js).create("K"); await kv.create("a", Empty); - await assertRejects(async () => { - await kv.create("a", Empty); + await assertRejects(() => { + return kv.create("a", Empty); }); await kv.delete("a"); await kv.create("a", Empty); @@ -1645,6 +1645,23 @@ Deno.test("kv - create after delete", async () => { await cleanup(ns, nc); }); +Deno.test("kv - get non-existing non-direct", async () => { + const { ns, nc } = await _setup(connect, 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(connect, 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(connect, jetstreamServerConf({})); diff --git a/migration.md b/migration.md index 0dbefe9c..3c7dabf9 100644 --- a/migration.md +++ b/migration.md @@ -2,7 +2,7 @@ The NATS ecosystem has grown a lot since the 2.0 release of the `nats` (nats.js) client. NATS currently runs in several JavaScript runtimes: Deno, Browser, and -Node (Bun). +Node/Bun. While the organization of the library has served developers well, there are a number of issues we would like to address going forward: @@ -13,7 +13,7 @@ number of issues we would like to address going forward: - Better presentation of NATS technologies to developers that are interested in KV, ObjectStore or JetStream. - Smaller dependencies for those that are only interested in the NATS core - functionality (no JetStream) + functionality. - More agility and independence to each of the modules, as well as their own version. - Easier understanding of the functionality in question, as each repository @@ -35,26 +35,26 @@ The transports have also been migrated: - `@nats-io/transport-node` has all the functionality of the original `nats.js` - `@nats-io/transport-deno` has all the functionality of `nats.deno` - `nats.ws` is now part of `@nats-io/nats-core` as it can be used from Deno or - latest version of Node directly. + latest version of Node directly or any runtime that has standard W3C Websocket + support. Note that when installing `@nats-io/transport-node` or `@nats-io/transport-deno`, the `@nats-io/core` APIs are also made available. Your library selection process will start by selecting your runtime, and importing any additional functionality you may be interested in. The -`@nats-io/node`, `@nats-io/deno`, `@nats-io/es-websocket` depend and re-export +`@nats-io/transport-node`, `@nats-io/transport-deno` depend on and re-export `@nats-io/core`. To use the extended functionality (JetStream, KV, ObjectStore, Services) you -will need to install and import from the other libraries and call API to create -an instance of the functionality the need. +will need to install and import from the other libraries to access those APIs. For example, developers that use JetStream can access it by using the functions `jetstream()` and `jetstreamManager()` and provide their NATS connection. Note that the `NatsConnection#jetstream/Manager()` APIs are no longer available. Developers interested in KV or ObjectStore can access the resources by calling -creating a Kvm and calling `create()` or `open()` using wither a +creating a Kvm and calling `create()` or `open()` using either a `JetStreamClient` or a plain `NatsConnection`. Note that the `JetStreamClient#views` API is also no longer available. @@ -76,14 +76,20 @@ these modules for cross-runtime consumption. - QueuedIterator type incorrectly exposed a `push()` operation - this operation is not public API and was removed from the interface. - The internal type `TypedSubscription` and associated interfaces have been - removed, these were supporting legacy JetStream APIs. If you were using these - internal types to transform the types in the subscription, take a look at + removed, these were supporting legacy JetStream APIs + (`subscribe/pullSubscribe()`. If you were using these internal types to + transform the types in the subscription, take a look at [messagepipeline](https://github.com/synadia-io/orbit.js/tree/main/messagepipeline). - The utilities `JSONCodec` and `StringCodec` have been removed, the `Msg` types - and derivatives can set string or Uint8Array payalods. To read payloads as - string or JSON use `string()` and `json()` methods. For publishing JSON - payloads, simply specify the output of `JSON.stringify()` to the publish - operation. + and derivatives can set string or Uint8Array payloads. To read payloads as + string or JSON use `string()` and `json()` methods on Msg or its derivatives. + For publishing JSON payloads, simply specify the output of `JSON.stringify()` + to the publish or request operation. +- NatsError was removed in favor of more descriptive types. For example, if you + make a request, the request could fail with a RequestError or TimeoutError. + The RequestError in turn will contain the `cause` such as `NoRespondersError`. + This also means that in TypeScript, the callback signature has been relaxed to + just `(Error, Msg)=>void`. For more information see the JsDocs. ## Changes in JetStream @@ -106,9 +112,14 @@ To use JetStream, you must install and import `@nats/jetstream`. - `JetStreamClient.pull()` was deprecated and was removed. Use `Consumer.next()`. - The utility function `consumerOpts()` and associated function - `isConsumerOptsBuilder()` have been removed. Along side of it + `isConsumerOptsBuilder()` have been removed. Alongside of it `ConsumerOptsBuilder` which was used by `subscribe()` and `pullSubscribe()` - type has been removed. + type has also been removed. +- JetStream errors are now expressed by the type `JetStreamError` and + `JetStreamAPIError`. Common errors such as `ConsumerNotFound`, and + `StreamNotFound`, `JetStreamNotEnabled` are subtypes of the above. For API + calls where the server could return an error, these are `JetStreamAPIError` + and contain all the information returned by the server. ## Changes to KV @@ -139,12 +150,12 @@ await kvm.open("mykv"); Previous versions of `Kv.watch()` allowed the client to specify a function that was called when the watch was done providing history values. In this version, -you can find out if a watch is yielding an update by examining into -`KvEntry.isUpdate`. Note that an empty Kv will not yield any watch information. -You can test for this initial condition, by getting the status of the KV, and -inspecting the `values` property, which will state the number of entries in the -Kv. Also note that watches with the option to do updates only, cannot notify -until there's an update. +you can find out if a watch is yielding an update by examining the `isUpdate` +property. Note that an empty Kv will not yield any watch information. You can +test for this initial condition, by getting the status of the KV, and inspecting +the `values` property, which will state the number of entries in the Kv. Also +note that watches with the option to do updates only, cannot notify until +there's an update. ## Changes to ObjectStore diff --git a/obj/deno.json b/obj/deno.json index 42d2e23f..602234ba 100644 --- a/obj/deno.json +++ b/obj/deno.json @@ -1,6 +1,6 @@ { "name": "@nats-io/obj", - "version": "3.0.0-12", + "version": "3.0.0-17", "exports": { ".": "./src/mod.ts", "./internal": "./src/internal_mod.ts" @@ -33,7 +33,7 @@ "test": "deno test -A --parallel --reload --quiet tests/ --import-map=import_map.json" }, "imports": { - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30", - "@nats-io/jetstream": "jsr:@nats-io/jetstream@~3.0.0-15" + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34", + "@nats-io/jetstream": "jsr:@nats-io/jetstream@~3.0.0-21" } } diff --git a/obj/import_map.json b/obj/import_map.json index b0d4b9ee..3b2e41e4 100644 --- a/obj/import_map.json +++ b/obj/import_map.json @@ -1,9 +1,9 @@ { "imports": { - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30", - "@nats-io/nats-core/internal": "jsr:@nats-io/nats-core@~3.0.0-30/internal", - "@nats-io/jetstream": "jsr:@nats-io/jetstream@~3.0.0-15", - "@nats-io/jetstream/internal": "jsr:@nats-io/jetstream@~3.0.0-15/internal", + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34", + "@nats-io/nats-core/internal": "jsr:@nats-io/nats-core@~3.0.0-34/internal", + "@nats-io/jetstream": "jsr:@nats-io/jetstream@~3.0.0-21", + "@nats-io/jetstream/internal": "jsr:@nats-io/jetstream@~3.0.0-21/internal", "test_helpers": "../test_helpers/mod.ts", "@nats-io/nkeys": "jsr:@nats-io/nkeys@1.2.0-4", "@nats-io/nuid": "jsr:@nats-io/nuid@2.0.1-2", diff --git a/obj/package.json b/obj/package.json index 8d228e52..cf034449 100644 --- a/obj/package.json +++ b/obj/package.json @@ -1,6 +1,6 @@ { "name": "@nats-io/obj", - "version": "3.0.0-12", + "version": "3.0.0-17", "files": [ "lib/", "LICENSE", @@ -34,8 +34,8 @@ }, "description": "obj library - this library implements all the base functionality for NATS objectstore for javascript clients", "dependencies": { - "@nats-io/jetstream": "~3.0.0-15", - "@nats-io/nats-core": "~3.0.0-30" + "@nats-io/jetstream": "3.0.0-21", + "@nats-io/nats-core": "3.0.0-34" }, "devDependencies": { "@types/node": "^22.7.6", diff --git a/obj/src/objectstore.ts b/obj/src/objectstore.ts index f6c37c3b..29b19194 100644 --- a/obj/src/objectstore.ts +++ b/obj/src/objectstore.ts @@ -16,7 +16,6 @@ import type { MsgHdrs, NatsConnection, - NatsError, QueuedIterator, } from "@nats-io/nats-core/internal"; import { @@ -50,6 +49,9 @@ import type { import { DeliverPolicy, DiscardPolicy, + isMessageNotFound, + JetStreamApiCodes, + JetStreamApiError, JsHeaders, ListerImpl, PubHeaders, @@ -364,7 +366,10 @@ export class ObjectStoreImpl implements ObjectStore { soi.revision = m.seq; return soi; } catch (err) { - if ((err as NatsError).code === "404") { + if ( + err instanceof JetStreamApiError && + err.code === JetStreamApiCodes.NoMessageFound + ) { return null; } return Promise.reject(err); @@ -377,8 +382,10 @@ export class ObjectStoreImpl implements ObjectStore { try { return await this.jsm.streams.info(this.stream, opts); } catch (err) { - const nerr = err as NatsError; - if (nerr.code === "404") { + if ( + err instanceof JetStreamApiError && + err.code === JetStreamApiCodes.StreamNotFound + ) { return null; } return Promise.reject(err); @@ -808,7 +815,7 @@ export class ObjectStoreImpl implements ObjectStore { try { await this.jsm.streams.getMessage(this.stream, { last_by_subj: subj }); } catch (err) { - if ((err as NatsError).code !== "404") { + if (!isMessageNotFound(err as Error)) { qi.stop(err as Error); } } diff --git a/services/deno.json b/services/deno.json index ee58151d..acfb300e 100644 --- a/services/deno.json +++ b/services/deno.json @@ -1,6 +1,6 @@ { "name": "@nats-io/services", - "version": "3.0.0-9", + "version": "3.0.0-12", "exports": { ".": "./src/mod.ts", "./internal": "./src/internal_mod.ts" @@ -33,6 +33,6 @@ "test": "deno test -A --parallel --reload --quiet tests/ --import-map=import_map.json" }, "imports": { - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30" + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34" } } diff --git a/services/examples/01_services.ts b/services/examples/01_services.ts index 8ecb2fc1..d2b87bf1 100644 --- a/services/examples/01_services.ts +++ b/services/examples/01_services.ts @@ -132,7 +132,7 @@ function decoder(r: ServiceMsg): Promise { return Promise.resolve(a); } catch (err) { // this is JSON.parse() - in JSONCodec failing to parse JSON - return Promise.reject(new ServiceError(400, err.message)); + return Promise.reject(new ServiceError(400, (err as Error).message)); } } diff --git a/services/import_map.json b/services/import_map.json index 0c5e9ca1..6c3ae4d4 100644 --- a/services/import_map.json +++ b/services/import_map.json @@ -1,7 +1,7 @@ { "imports": { - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30", - "@nats-io/nats-core/internal": "jsr:@nats-io/nats-core@~3.0.0-30/internal", + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34", + "@nats-io/nats-core/internal": "jsr:@nats-io/nats-core@~3.0.0-34/internal", "test_helpers": "../test_helpers/mod.ts", "@nats-io/nkeys": "jsr:@nats-io/nkeys@1.2.0-4", "@nats-io/nuid": "jsr:@nats-io/nuid@2.0.1-2", diff --git a/services/package.json b/services/package.json index d27e0f39..cb37f4b8 100644 --- a/services/package.json +++ b/services/package.json @@ -1,6 +1,6 @@ { "name": "@nats-io/services", - "version": "3.0.0-9", + "version": "3.0.0-12", "files": [ "lib/", "LICENSE", @@ -34,7 +34,7 @@ }, "description": "services library - this library implements all the base functionality for NATS services for javascript clients", "dependencies": { - "@nats-io/nats-core": "~3.0.0-30" + "@nats-io/nats-core": "3.0.0-34" }, "devDependencies": { "@types/node": "^22.7.6", diff --git a/services/src/service.ts b/services/src/service.ts index 3ddd1814..c6d0e1b2 100644 --- a/services/src/service.ts +++ b/services/src/service.ts @@ -27,7 +27,6 @@ import type { MsgHdrs, Nanos, NatsConnection, - NatsError, Payload, PublishOptions, QueuedIterator, @@ -69,7 +68,7 @@ function validateName(context: string, name = "") { } function validName(name = ""): string { - if (name === "") { + if (!name) { throw Error(`name required`); } const RE = /^[-\w]+$/g; @@ -373,7 +372,7 @@ export class ServiceImpl implements Service { sv.queue = queue; const callback = handler - ? (err: NatsError | null, msg: Msg) => { + ? (err: Error | null, msg: Msg) => { if (err) { this.close(err); return; @@ -458,7 +457,7 @@ export class ServiceImpl implements Service { addInternalHandler( verb: ServiceVerb, - handler: (err: NatsError | null, msg: Msg) => Promise, + handler: (err: Error | null, msg: Msg) => Promise, ) { const v = `${verb}`.toUpperCase(); this._doAddInternalHandler(`${v}-all`, verb, handler); @@ -475,7 +474,7 @@ export class ServiceImpl implements Service { _doAddInternalHandler( name: string, verb: ServiceVerb, - handler: (err: NatsError | null, msg: Msg) => Promise, + handler: (err: Error | null, msg: Msg) => Promise, kind = "", id = "", ) { diff --git a/services/src/types.ts b/services/src/types.ts index c3c777d3..ae9cbb7e 100644 --- a/services/src/types.ts +++ b/services/src/types.ts @@ -1,7 +1,21 @@ +/* + * 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 type { Msg, Nanos, - NatsError, Payload, PublishOptions, QueuedIterator, @@ -16,7 +30,7 @@ export interface ServiceMsg extends Msg { ): boolean; } -export type ServiceHandler = (err: NatsError | null, msg: ServiceMsg) => void; +export type ServiceHandler = (err: Error | null, msg: ServiceMsg) => void; /** * A service Endpoint */ diff --git a/services/tests/service_test.ts b/services/tests/service_test.ts index 6b490a1a..e578c42c 100644 --- a/services/tests/service_test.ts +++ b/services/tests/service_test.ts @@ -19,6 +19,7 @@ import { assertArrayIncludes, assertEquals, assertExists, + assertInstanceOf, assertRejects, assertThrows, fail, @@ -28,14 +29,12 @@ import { collect, createInbox, delay, - ErrorCode, - nuid, + errors, } from "@nats-io/nats-core/internal"; import type { Msg, NatsConnection, NatsConnectionImpl, - NatsError, QueuedIterator, SubscriptionImpl, } from "@nats-io/nats-core/internal"; @@ -230,8 +229,7 @@ Deno.test("service - basics", async () => { async () => { await collect(await m.ping("test", "c")); }, - Error, - ErrorCode.NoResponders, + errors.NoRespondersError, ); assertEquals(await count(m.info()), 2); @@ -241,8 +239,7 @@ Deno.test("service - basics", async () => { async () => { await collect(await m.info("test", "c")); }, - Error, - ErrorCode.NoResponders, + errors.NoRespondersError, ); assertEquals(await count(m.stats()), 2); @@ -252,8 +249,7 @@ Deno.test("service - basics", async () => { async () => { await collect(await m.stats("test", "c")); }, - Error, - ErrorCode.NoResponders, + errors.NoRespondersError, ); await srvA.stop(); @@ -291,11 +287,8 @@ Deno.test("service - stop error", async () => { fail("shouldn't have subscribed"); }); - const err = await service.stopped as NatsError; - assertEquals( - err.code, - ErrorCode.PermissionsViolation, - ); + const err = await service.stopped as Error; + assertInstanceOf(err, errors.PermissionViolationError); await cleanup(ns, nc); }); @@ -325,11 +318,8 @@ Deno.test("service - start error", async () => { msg?.respond(); }); - const err = await service.stopped as NatsError; - assertEquals( - err.code, - ErrorCode.PermissionsViolation, - ); + const err = await service.stopped as Error; + assertInstanceOf(err, errors.PermissionViolationError); await cleanup(ns, nc); }); @@ -663,70 +653,70 @@ Deno.test("service - service errors", async () => { 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 => { - 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 - 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 => { +// 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(connect); diff --git a/test_helpers/asserts.ts b/test_helpers/asserts.ts index d1e5e3fa..df6d2206 100644 --- a/test_helpers/asserts.ts +++ b/test_helpers/asserts.ts @@ -13,49 +13,9 @@ * limitations under the License. */ -import { assert, assertThrows, fail } from "jsr:@std/assert"; -import type { NatsError } from "../core/src/mod.ts"; -import { isNatsError } from "../core/src/internal_mod.ts"; - -export function assertErrorCode(err?: Error, ...codes: string[]) { - if (!err) { - fail(`expected an error to be thrown`); - } - if (isNatsError(err)) { - const { code } = err as NatsError; - assert(code); - const ok = codes.find((c) => { - return code.indexOf(c) !== -1; - }); - if (ok === "") { - fail(`got ${code} - expected any of [${codes.join(", ")}]`); - } - } else { - fail(`didn't get a nats error - got: ${err.message}`); - } -} - -export function assertThrowsErrorCode( - fn: () => T, - ...codes: string[] -) { - const err = assertThrows(fn); - assertErrorCode(err as Error, ...codes); -} - -export async function assertThrowsAsyncErrorCode( - fn: () => Promise, - ...codes: string[] -) { - try { - await fn(); - fail("expected to throw"); - } catch (err) { - assertErrorCode(err as Error, ...codes); - } -} +import { assertGreaterOrEqual, assertLessOrEqual } from "jsr:@std/assert"; export function assertBetween(n: number, low: number, high: number) { - console.assert(n >= low, `${n} >= ${low}`); - console.assert(n <= high, `${n} <= ${low}`); + assertGreaterOrEqual(n, low, `${n} >= ${low}`) + assertLessOrEqual(n, high, `${n} <= ${high}`) } diff --git a/test_helpers/mod.ts b/test_helpers/mod.ts index 142148d0..2b73040f 100644 --- a/test_helpers/mod.ts +++ b/test_helpers/mod.ts @@ -29,9 +29,6 @@ export { Lock } from "./lock.ts"; export { Connection, TestServer } from "./test_server.ts"; export { assertBetween, - assertErrorCode, - assertThrowsAsyncErrorCode, - assertThrowsErrorCode, } from "./asserts.ts"; export { NatsServer, ServerSignals } from "./launcher.ts"; diff --git a/transport-deno/deno.json b/transport-deno/deno.json index b7aaef40..857603ab 100644 --- a/transport-deno/deno.json +++ b/transport-deno/deno.json @@ -1,6 +1,6 @@ { "name": "@nats-io/transport-deno", - "version": "3.0.0-6", + "version": "3.0.0-9", "exports": { ".": "./src/mod.ts" }, @@ -19,8 +19,8 @@ ] }, "imports": { - "@std/io": "jsr:@std/io@0.224.0", - "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-30", + "@std/io": "jsr:@std/io@0.225.0", + "@nats-io/nats-core": "jsr:@nats-io/nats-core@~3.0.0-34", "@nats-io/nkeys": "jsr:@nats-io/nkeys@1.2.0-4", "@nats-io/nuid": "jsr:@nats-io/nuid@2.0.1-2" } diff --git a/transport-deno/src/deno_transport.ts b/transport-deno/src/deno_transport.ts index 9330699d..a59cfa86 100644 --- a/transport-deno/src/deno_transport.ts +++ b/transport-deno/src/deno_transport.ts @@ -19,10 +19,9 @@ import { DataBuffer, deferred, Empty, - ErrorCode, + errors, extractProtocolMessage, INFO, - NatsError, render, } from "@nats-io/nats-core/internal"; @@ -100,9 +99,10 @@ export class DenoTransport implements Transport { } } catch (err) { this.conn?.close(); - throw (err as NatsError)?.name === "ConnectionRefused" - ? NatsError.errorForCode(ErrorCode.ConnectionRefused) - : err; + const _err = err as Error; + throw new errors.ConnectionError(_err.message.toLowerCase(), { + cause: err, + }); } } diff --git a/transport-deno/src/version.ts b/transport-deno/src/version.ts index 9386be88..75951750 100644 --- a/transport-deno/src/version.ts +++ b/transport-deno/src/version.ts @@ -1,2 +1,2 @@ // This file is generated - do not edit -export const version = "3.0.0-6"; +export const version = "3.0.0-9"; diff --git a/transport-node/examples/bench.js b/transport-node/examples/bench.js index 8794f2c8..0da02924 100755 --- a/transport-node/examples/bench.js +++ b/transport-node/examples/bench.js @@ -1,9 +1,8 @@ #!/usr/bin/env node const parse = require("minimist"); -const { Nuid, connect } = require("../index"); -const { Bench, Metric } = require("../lib/nats-base-client/bench"); -const { process } = require("node:process"); +const { Bench, Metric, connect, Nuid } = require("../lib/mod"); +const process = require("node:process"); const defaults = { s: "127.0.0.1:4222", @@ -43,7 +42,7 @@ const argv = parse( if (argv.h || argv.help || (!argv.sub && !argv.pub && !argv.req && !argv.rep)) { console.log( - "usage: bench.ts [--json] [--callbacks] [--csv] [--csvheader] [--pub] [--sub] [--req (--asyncRequests)] [--count <#messages>=100000] [--payload <#bytes>=128] [--iterations <#loop>=1>] [--server server] [--subject ]\n", + "usage: bench.js [--json] [--callbacks] [--csv] [--csvheader] [--pub] [--sub] [--req (--asyncRequests)] [--count <#messages>=100000] [--payload <#bytes>=128] [--iterations <#loop>=1>] [--server server] [--subject ]\n", ); process.exit(0); } diff --git a/transport-node/examples/nats-events.js b/transport-node/examples/nats-events.js index 2a17d1cc..23fde71f 100755 --- a/transport-node/examples/nats-events.js +++ b/transport-node/examples/nats-events.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const { process } = require("node:process"); +const process = require("node:process"); const parse = require("minimist"); const { connect } = require("../index"); diff --git a/transport-node/examples/nats-pub.js b/transport-node/examples/nats-pub.js index 3ea22378..d788d52a 100755 --- a/transport-node/examples/nats-pub.js +++ b/transport-node/examples/nats-pub.js @@ -1,12 +1,11 @@ #!/usr/bin/env node const parse = require("minimist"); -const { connect, StringCodec, headers, credsAuthenticator } = require( +const { connect, StringCodec, headers, credsAuthenticator, delay } = require( "../index", ); -const { delay } = require("./util"); const fs = require("node:fs"); -const { process } = require("node:process"); +const process = require("node:process"); const argv = parse( process.argv.slice(2), diff --git a/transport-node/examples/nats-rep.js b/transport-node/examples/nats-rep.js index 292957b5..a4b68bbc 100755 --- a/transport-node/examples/nats-rep.js +++ b/transport-node/examples/nats-rep.js @@ -5,7 +5,7 @@ const { connect, StringCodec, headers, credsAuthenticator } = require( "../index", ); const fs = require("node:fs"); -const { process } = require("node:process"); +const process = require("node:process"); const argv = parse( process.argv.slice(2), diff --git a/transport-node/examples/nats-req.js b/transport-node/examples/nats-req.js index df314a70..84da85ac 100755 --- a/transport-node/examples/nats-req.js +++ b/transport-node/examples/nats-req.js @@ -1,12 +1,11 @@ #!/usr/bin/env node const parse = require("minimist"); -const { connect, StringCodec, headers, credsAuthenticator } = require( +const { connect, StringCodec, headers, credsAuthenticator, delay } = require( "../index", ); -const { delay } = require("./util"); const fs = require("node:fs"); -const { process } = require("node:process"); +const process = require("node:process"); const argv = parse( process.argv.slice(2), diff --git a/transport-node/examples/nats-sub.js b/transport-node/examples/nats-sub.js index 07fdd191..d6aa809a 100755 --- a/transport-node/examples/nats-sub.js +++ b/transport-node/examples/nats-sub.js @@ -3,7 +3,7 @@ const parse = require("minimist"); const { connect, StringCodec, credsAuthenticator } = require("../index"); const fs = require("node:fs"); -const { process } = require("node:process"); +const process = require("node:process"); const argv = parse( process.argv.slice(2), diff --git a/transport-node/examples/util.js b/transport-node/examples/util.js index cb2b646f..9d257a7f 100644 --- a/transport-node/examples/util.js +++ b/transport-node/examples/util.js @@ -1,10 +1,17 @@ -exports.delay = function delay(ms) { - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, ms); - }); -}; +/* + * 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; diff --git a/transport-node/package.json b/transport-node/package.json index 8bb3a98e..c186a64d 100644 --- a/transport-node/package.json +++ b/transport-node/package.json @@ -1,6 +1,6 @@ { "name": "@nats-io/transport-node", - "version": "3.0.0-15", + "version": "3.0.0-19", "description": "Node.js client for NATS, a lightweight, high-performance cloud native messaging system", "keywords": [ "nats", @@ -54,9 +54,9 @@ "node": ">= 18.0.0" }, "dependencies": { - "@nats-io/nats-core": "~3.0.0-30", - "@nats-io/nkeys": "~1.2.0-7", - "@nats-io/nuid": "^2.0.1-2" + "@nats-io/nats-core": "3.0.0-34", + "@nats-io/nkeys": "1.2.0-7", + "@nats-io/nuid": "2.0.1-2" }, "devDependencies": { "@types/node": "^22.7.6", @@ -64,8 +64,8 @@ "nats-jwt": "^0.0.9", "shx": "^0.3.3", "typescript": "5.6.3", - "@nats-io/jetstream": "^3.0.0-3", - "@nats-io/kv": "^3.0.0-2", - "@nats-io/obj": "^3.0.0-1" + "@nats-io/jetstream": "3.0.0-21", + "@nats-io/kv": "3.0.0-16", + "@nats-io/obj": "3.0.0-17" } } diff --git a/transport-node/src/node_transport.ts b/transport-node/src/node_transport.ts index 0b292563..02b28a54 100644 --- a/transport-node/src/node_transport.ts +++ b/transport-node/src/node_transport.ts @@ -18,11 +18,10 @@ import { DataBuffer, Deferred, deferred, - ErrorCode, + errors, extend, extractProtocolMessage, INFO, - NatsError, render, ServerInfo, Transport, @@ -88,7 +87,10 @@ export class NodeTransport implements Transport { //@ts-ignore: this is possibly a TlsSocket if (tlsRequired && this.socket.encrypted !== true) { - throw new NatsError("tls", ErrorCode.ServerOptionNotAvailable); + throw errors.InvalidArgumentError.format( + "tls", + "is not available on this server", + ); } this.connected = true; @@ -101,14 +103,14 @@ export class NodeTransport implements Transport { // this seems to be possible in Kubernetes // where an error is thrown, but it is undefined // when something like istio-init is booting up - err = NatsError.errorForCode( - ErrorCode.ConnectionRefused, - new Error("node provided an undefined error!"), + err = new errors.ConnectionError( + "error connecting - node provided an undefined error", ); } + // @ts-ignore: node error const { code } = err; const perr = code === "ECONNREFUSED" - ? NatsError.errorForCode(ErrorCode.ConnectionRefused, err) + ? new errors.ConnectionError("connection refused", { cause: err }) : err; this.socket?.destroy(); throw perr; @@ -248,7 +250,9 @@ export class NodeTransport implements Transport { const certOpts = await this.loadClientCerts() || {}; tlsOpts = extend(tlsOpts, this.options.tls, certOpts); } catch (err) { - return Promise.reject(new NatsError(err.message, ErrorCode.Tls, err)); + return Promise.reject( + new errors.ConnectionError((err as Error).message, { cause: err }), + ); } } const d = deferred(); @@ -277,7 +281,9 @@ export class NodeTransport implements Transport { tlsSocket.setNoDelay(true); } catch (err) { // tls throws errors on bad certs see nats.js#310 - d.reject(NatsError.errorForCode(ErrorCode.Tls, err)); + d.reject( + new errors.ConnectionError((err as Error).message, { cause: err }), + ); } return d; } @@ -294,7 +300,11 @@ export class NodeTransport implements Transport { const certOpts = await this.loadClientCerts() || {}; tlsOpts = extend(tlsOpts, this.options.tls, certOpts); } catch (err) { - return Promise.reject(new NatsError(err.message, ErrorCode.Tls, err)); + return Promise.reject( + new errors.ConnectionError((err as Error).message, { + cause: err, + }), + ); } } const d = deferred(); @@ -321,7 +331,9 @@ export class NodeTransport implements Transport { }); } catch (err) { // tls throws errors on bad certs see nats.js#310 - d.reject(NatsError.errorForCode(ErrorCode.Tls, err)); + d.reject( + new errors.ConnectionError((err as Error).message, { cause: err }), + ); } return d; } diff --git a/transport-node/src/version.ts b/transport-node/src/version.ts index 7e38d46f..327f3fe4 100644 --- a/transport-node/src/version.ts +++ b/transport-node/src/version.ts @@ -1,2 +1,2 @@ // This file is generated - do not edit -export const version = "3.0.0-15"; +export const version = "3.0.0-19"; diff --git a/transport-node/tests/basics_test.js b/transport-node/tests/basics_test.js index f1b751a8..cb8373ec 100644 --- a/transport-node/tests/basics_test.js +++ b/transport-node/tests/basics_test.js @@ -16,7 +16,7 @@ const { describe, it } = require("node:test"); const assert = require("node:assert").strict; const { connect, - ErrorCode, + errors, createInbox, } = require( "../lib/mod", @@ -74,13 +74,13 @@ describe( }); it("basics - fail connect", async () => { - await connect({ servers: "127.0.0.1:32001" }) - .then(() => { - assert.fail("should have not connected"); - }) - .catch((err) => { - assert.equal(err.code, ErrorCode.ConnectionRefused); - }); + await assert.rejects( + () => { + return connect({ servers: "127.0.0.1:32001" }); + }, + errors.ConnectionError, + "connection refused", + ); }); it("basics - pubsub", async () => { @@ -137,7 +137,8 @@ describe( const closed = nc.closed(); await ns.stop(); const err = await closed; - assert.equal(err?.code, ErrorCode.ConnectionRefused); + assert.ok(err instanceof errors.ConnectionError); + assert.equal(err.message, "connection refused"); }); it("basics - server error", async () => { @@ -147,7 +148,7 @@ describe( nc.protocol.sendCommand("X\r\n"); }); const err = await nc.closed(); - assert.equal(err?.code, ErrorCode.ProtocolError); + assert(err instanceof errors.ProtocolError); await ns.stop(); }); diff --git a/transport-node/tests/reconnect_test.js b/transport-node/tests/reconnect_test.js index 816d63f7..d4ecce4e 100644 --- a/transport-node/tests/reconnect_test.js +++ b/transport-node/tests/reconnect_test.js @@ -21,7 +21,6 @@ const { NatsServer } = require("./helpers/launcher"); const { createInbox, Events, - ErrorCode, deferred, DebugEvents, } = require("@nats-io/nats-core/internal"); @@ -81,7 +80,7 @@ describe( try { await nc.closed(); } catch (err) { - assert.equal(err.code, ErrorCode.ConnectionRefused); + assert.equal(err.message, "connection refused"); } assert.equal(disconnects, 1, "disconnects"); assert.equal(reconnecting, 10, "reconnecting"); @@ -124,7 +123,7 @@ describe( nc.protocol.servers.getCurrentServer().lastConnect; const dt = deferred(); - const _ = (async () => { + (async () => { for await (const e of nc.status()) { switch (e.type) { case DebugEvents.Reconnecting: diff --git a/transport-node/tests/tls_test.js b/transport-node/tests/tls_test.js index 5b0e0c5c..64b28c15 100644 --- a/transport-node/tests/tls_test.js +++ b/transport-node/tests/tls_test.js @@ -16,7 +16,6 @@ const { describe, it } = require("node:test"); const assert = require("node:assert").strict; const { connect, - ErrorCode, } = require( "../index", ); @@ -47,7 +46,7 @@ describe("tls", { timeout: 20_000, concurrency: true, forceExit: true }, () => { assert.fail("shouldn't have connected"); }) .catch((err) => { - assert.equal(err.code, ErrorCode.ServerOptionNotAvailable); + assert.equal(err.message, "server does not support 'tls'"); lock.unlock(); }); await lock; @@ -189,9 +188,7 @@ describe("tls", { timeout: 20_000, concurrency: true, forceExit: true }, () => { await assert.rejects(() => { return connect({ servers: `localhost:${ns.port}`, tls: conf }); }, (err) => { - assert.equal(err.code, ErrorCode.Tls); - assert.ok(err.chainedError); - assert.ok(re.exec(err.chainedError.message)); + assert.ok(re.exec(err.message)); return true; }); await ns.stop(); @@ -233,7 +230,6 @@ describe("tls", { timeout: 20_000, concurrency: true, forceExit: true }, () => { await connect({ servers: `localhost:${ns.port}`, tls: conf }); assert.fail("shouldn't have connected"); } catch (err) { - assert.equal(err.code, ErrorCode.Tls); const v = conf[arg]; assert.equal(err.message, `${v} doesn't exist`); }