@wandelbots/nova-js
Version:
Official JS client for the Wandelbots API
156 lines (155 loc) • 5.71 kB
JavaScript
import { a as parseNovaInstanceUrl } from "../../converters-DnG1fX23.mjs";
import { wsconnect } from "@nats-io/nats-core";
//#region src/lib/experimental/nats/buildNatsServerUrl.ts
/**
* Builds the WebSocket URL for a NOVA instance's NATS gateway from its
* instance URL, e.g. `https://foo.instance.wandelbots.io` becomes
* `wss://foo.instance.wandelbots.io/api/nats`.
*
* Pass the result as `servers` in the `NovaNatsClientConfig` passed to
* `NovaNatsClient`.
*/
function buildNatsServerUrl(instanceUrl) {
const url = parseNovaInstanceUrl(instanceUrl);
return `${url.protocol === "https:" ? "wss:" : "ws:"}//${url.host}/api/nats`;
}
//#endregion
//#region src/lib/experimental/nats/buildSubject.ts
/**
* Builds a NATS subject from an AsyncAPI-style channel address template
* (e.g. `"{instance}.v2.cells.{cell}"`) by substituting each `{param}`
* placeholder with the corresponding value from `params`.
*/
function isValidSubjectChar(char) {
const code = char.charCodeAt(0);
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || char === "-" || char === "_";
}
function isValidSubjectValue(value) {
if (value === "*") return true;
if (value.length === 0) return false;
for (const char of value) if (!isValidSubjectChar(char)) return false;
return true;
}
function buildSubject(template, params) {
let result = "";
let cursor = 0;
while (cursor < template.length) {
const openIndex = template.indexOf("{", cursor);
if (openIndex === -1) {
result += template.slice(cursor);
break;
}
const closeIndex = template.indexOf("}", openIndex + 1);
if (closeIndex === -1) {
result += template.slice(cursor);
break;
}
result += template.slice(cursor, openIndex);
const paramName = template.slice(openIndex + 1, closeIndex);
const value = params[paramName];
if (value === void 0) throw new Error(`Missing value for subject parameter "${paramName}" in template "${template}"`);
if (!isValidSubjectValue(value)) throw new Error(`Invalid value for subject parameter "${paramName}": "${value}" (must be non-empty and contain only letters, digits, "-", and "_")`);
result += value;
cursor = closeIndex + 1;
}
return result;
}
//#endregion
//#region src/lib/experimental/nats/NovaNatsClient.ts
/**
* Typed NATS client for the Wandelbots NOVA messaging API, generated from
* src/asyncapi.yaml (see scripts/generate-nats-client.ts).
*
* Connects over WebSocket via `@nats-io/nats-core`'s `wsconnect`.
*/
var NovaNatsClient = class {
config;
connectionPromise = null;
constructor(nova, config = {}) {
this.config = {
servers: buildNatsServerUrl(nova.instanceUrl.href),
...nova.accessToken ? { token: nova.accessToken } : {},
...config
};
}
/**
* Connects to NATS if not already connected or connecting, and returns the
* connection. Safe to call concurrently: all callers share the same
* in-flight connection attempt instead of each starting their own.
*/
connect() {
if (!this.connectionPromise) this.connectionPromise = wsconnect(this.config).catch((err) => {
this.connectionPromise = null;
throw err;
});
return this.connectionPromise;
}
/** Closes the underlying NATS connection, if open or connecting. */
async close() {
const connectionPromise = this.connectionPromise;
this.connectionPromise = null;
if (!connectionPromise) return;
try {
await (await connectionPromise).close();
} catch {}
}
/**
* Subscribes to a NATS subject published by the server, invoking `handler`
* with the JSON-decoded payload of every message received.
*
* `subject` is the subject template as it appears on the wire, e.g.
* `"nova.v2.cells.{cell}"`, with `{param}` placeholders filled in from
* `params`.
*
* Errors decoding a message or thrown/rejected by `handler` are caught and
* logged per-message, so one bad message doesn't stop later messages on
* the same subscription from being handled.
*
* Returns a function that unsubscribes when called.
*/
async subscribe(subject, ...args) {
const [params, handler] = args.length === 1 ? [{}, args[0]] : [args[0], args[1]];
const nc = await this.connect();
const resolvedSubject = buildSubject(subject, params);
const sub = nc.subscribe(resolvedSubject);
(async () => {
for await (const msg of sub) try {
await handler(msg.json(), msg);
} catch (err) {
console.error(`Error handling NATS message on subject "${resolvedSubject}"`, err);
}
})().catch((err) => {
console.error(`NATS subscription iterator failed for "${resolvedSubject}"`, err);
});
return () => sub.unsubscribe();
}
/**
* Sends a request payload for a NATS subject the server receives, and
* waits for the JSON-decoded reply.
*
* `subject` is the subject template as it appears on the wire, e.g.
* `"nova.v2.cells.{cell}.bus-ios.ios.set"`, with `{param}` placeholders
* filled in from `params`.
*/
async request(subject, params, payload, opts = {}) {
const nc = await this.connect();
const resolvedSubject = buildSubject(subject, params);
return (await nc.request(resolvedSubject, JSON.stringify(payload), { timeout: opts.timeout ?? 5e3 })).json();
}
/**
* Publishes a JSON payload to any NATS subject defined in the spec,
* without waiting for a reply.
*
* `subject` is the subject template as it appears on the wire, e.g.
* `"nova.v2.cells.{cell}.bus-ios.ios.set"`, with `{param}` placeholders
* filled in from `params`.
*/
async publish(subject, params, payload) {
const nc = await this.connect();
const resolvedSubject = buildSubject(subject, params);
nc.publish(resolvedSubject, JSON.stringify(payload));
}
};
//#endregion
export { NovaNatsClient, buildNatsServerUrl };
//# sourceMappingURL=index.mjs.map