UNPKG

@wandelbots/nova-js

Version:

Official JS client for the Wandelbots API

156 lines (155 loc) 5.71 kB
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