@wandelbots/nova-js
Version:
Official JS client for the Wandelbots API
250 lines (249 loc) • 9.1 kB
JavaScript
import { u as tryStringifyJson } from "./converters-DnG1fX23.mjs";
import ReconnectingWebSocket from "reconnecting-websocket";
import { isAxiosError } from "axios";
//#region src/lib/AutoReconnectingWebsocket.ts
var AutoReconnectingWebsocket = class extends ReconnectingWebSocket {
receivedFirstMessage;
targetUrl;
disposed = false;
opts;
constructor(targetUrl, opts = {}) {
console.log("Opening websocket to", targetUrl);
super(() => this.targetUrl || targetUrl, void 0, { startClosed: true });
Object.defineProperty(this, "url", { get() {
return this.targetUrl;
} });
this.targetUrl = targetUrl;
this.opts = opts;
this.addEventListener("open", () => {
console.log(`Websocket to ${this.url} opened`);
});
this.addEventListener("message", (ev) => {
if (!this.receivedFirstMessage) this.receivedFirstMessage = ev;
});
this.addEventListener("close", () => {
console.log(`Websocket to ${this.url} closed`);
});
const origReconnect = this.reconnect;
this.reconnect = () => {
if (this.opts.mock) this.opts.mock.handleWebsocketConnection(this);
else origReconnect.apply(this);
};
this.reconnect();
}
changeUrl(targetUrl) {
this.receivedFirstMessage = void 0;
this.targetUrl = targetUrl;
this.reconnect();
}
sendJson(data) {
if (this.opts.mock) this.opts.mock.handleWebsocketMessage(this, JSON.stringify(data));
else this.send(JSON.stringify(data));
}
/**
* Permanently close this websocket and indicate that
* this object should not be used again.
**/
dispose() {
this.close();
this.disposed = true;
if (this.opts.onDispose) this.opts.onDispose();
}
/**
* Returns a promise that resolves once the websocket
* is in the OPEN state. */
async opened() {
return new Promise((resolve, reject) => {
if (this.readyState === WebSocket.OPEN) resolve();
else {
this.addEventListener("open", () => resolve());
this.addEventListener("error", reject);
}
});
}
/**
* Returns a promise that resolves once the websocket
* is in the CLOSED state. */
async closed() {
return new Promise((resolve, reject) => {
if (this.readyState === WebSocket.CLOSED) resolve();
else {
this.addEventListener("close", () => resolve());
this.addEventListener("error", reject);
}
});
}
/**
* Returns a promise that resolves when the first message
* is received from the websocket. Resolves immediately if
* the first message has already been received.
*/
async firstMessage() {
if (this.receivedFirstMessage) return this.receivedFirstMessage;
return new Promise((resolve, reject) => {
const onMessage = (ev) => {
this.receivedFirstMessage = ev;
this.removeEventListener("message", onMessage);
this.removeEventListener("error", onError);
resolve(ev);
};
const onError = (ev) => {
this.removeEventListener("message", onMessage);
this.removeEventListener("error", onError);
reject(ev);
};
this.addEventListener("message", onMessage);
this.addEventListener("error", onError);
});
}
/**
* Returns a promise that resolves when the next message
* is received from the websocket.
*/
async nextMessage() {
return new Promise((resolve, reject) => {
const onMessage = (ev) => {
this.removeEventListener("message", onMessage);
this.removeEventListener("error", onError);
resolve(ev);
};
const onError = (ev) => {
this.removeEventListener("message", onMessage);
this.removeEventListener("error", onError);
reject(ev);
};
this.addEventListener("message", onMessage);
this.addEventListener("error", onError);
});
}
};
//#endregion
//#region src/lib/availableStorage.ts
/**
* Safety wrapper around browser localStorage providing context availability
* checks and JSON parsing
*/
var AvailableStorage = class {
available = typeof window !== "undefined" && !!window.localStorage;
getJSON(key) {
if (!this.available) return null;
const result = window.localStorage.getItem(key);
if (result === null) return null;
try {
return JSON.parse(result);
} catch (err) {
return null;
}
}
setJSON(key, obj) {
if (!this.available) return null;
window.localStorage.setItem(key, JSON.stringify(obj));
}
delete(key) {
if (!this.available) return null;
window.localStorage.removeItem(key);
}
setString(key, value) {
if (!this.available) return null;
window.localStorage.setItem(key, value);
}
getString(key) {
if (!this.available) return null;
return window.localStorage.getItem(key);
}
};
const availableStorage = new AvailableStorage();
//#endregion
//#region src/lib/errorHandling.ts
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Attempts to make a helpful error message from an unknown thrown error
* or promise rejection.
*
* This function is mainly to aid debugging and good bug reports. For
* expected errors encountered by end users, it's more ideal to catch
* the specific error code and provide a localized app-specific error message.
*/
function makeErrorMessage(err) {
if (isAxiosError(err)) {
if (err.response) return `${err.response?.status} ${err.response?.statusText} from ${err.response?.config.method?.toUpperCase() || "accessing"} ${err.response?.config.url}: ${JSON.stringify(err.response?.data)}`;
else if (err.config) if (err.code === "ERR_NETWORK") return `${err.message} from ${err.config.method?.toUpperCase() || "accessing"} ${err.config.url}. This error can happen because of either connection issues or server CORS policy.`;
else return `${err.message} from ${err.config.method?.toUpperCase() || "accessing"} ${err.config.url}`;
} else if (err instanceof Error) return err.message;
else if (typeof err === "string") return err;
else if (typeof err === "object") return tryStringifyJson(err) || `Unserializable object ${err}`;
return `${err}`;
}
/**
* Reloads the page with protection against loops.
* Allows at most one reload per 10 seconds; otherwise throws
* an error.
*/
function guardedPageReload(key) {
const RELOAD_KEY = `novajs_reload_guard:${key}`;
const RELOAD_COOLDOWN_MS = 1e4;
const lastReloadAt = Number(window.sessionStorage.getItem(RELOAD_KEY) ?? "0");
if (Date.now() - lastReloadAt < RELOAD_COOLDOWN_MS) throw new Error(`Unhandled error caused a reload (${key}), but a reload was already attempted recently. Aborting to prevent reload loop.`);
window.sessionStorage.setItem(RELOAD_KEY, String(Date.now()));
window.location.reload();
return new Promise(() => {});
}
//#endregion
//#region src/lib/LoginWithAuth0.ts
/**
* Mapping of stages to Auth0 configurations.
* The client ids are public identifiers for a specific auth0 application
* and are safe to include in client-side code.
* https://auth0.com/docs/get-started/applications/application-settings
*/
const auth0ConfigMap = {
dev: {
domain: `https://auth.portal.dev.wandelbots.io`,
clientId: "fLbJD0RLp5r2Dpucm5j8BjwMR6Hunfha"
},
stg: {
domain: `https://auth.portal.stg.wandelbots.io`,
clientId: "joVDeD9e786WzFNSGCqoVq7HNkWt5j6s"
},
prod: {
domain: `https://auth.portal.wandelbots.io`,
clientId: "J7WJUi38xVQdJAEBNRT9Xw1b0fXDb4J2"
}
};
/** Determine which Auth0 configuration to use based on instance URL */
const getAuth0Config = (instanceUrl) => {
if (instanceUrl.host.endsWith(".dev.wandelbots.io")) return auth0ConfigMap.dev;
if (instanceUrl.host.endsWith(".stg.wandelbots.io")) return auth0ConfigMap.stg;
if (instanceUrl.host.endsWith(".wandelbots.io")) return auth0ConfigMap.prod;
throw new Error(`Unable to authenticate with NOVA instance "${instanceUrl}". Auth0 login is only supported for cloud instances with hosts of the form "**.wandelbots.io".`);
};
/**
* Initializes Auth0 login process using redirect if necessary and retrieves an access token.
* Returns null when an access token should not be needed to authenticate (i.e. cookie auth
* when deployed on the instance domain)
*/
const loginWithAuth0 = async (instanceUrl) => {
if (typeof window === "undefined") throw new Error(`Access token must be set to use Nova when not in a browser environment.`);
if (instanceUrl.origin === window.location.origin) return guardedPageReload("cloud_instance_auth");
const { Auth0Client } = await import("@auth0/auth0-spa-js");
const auth0Config = getAuth0Config(instanceUrl);
const auth0Client = new Auth0Client({
domain: auth0Config.domain,
clientId: auth0Config.clientId ?? "",
useRefreshTokens: false,
authorizationParams: {
audience: "nova-api",
redirect_uri: window.location.origin
}
});
if (window.location.search.includes("code=") && window.location.search.includes("state=")) {
const { appState } = await auth0Client.handleRedirectCallback();
window.history.replaceState({}, document.title, appState?.returnTo || window.location.pathname);
} else await auth0Client.loginWithRedirect();
return await auth0Client.getTokenSilently();
};
//#endregion
export { makeErrorMessage as a, guardedPageReload as i, loginWithAuth0 as n, availableStorage as o, delay as r, AutoReconnectingWebsocket as s, getAuth0Config as t };
//# sourceMappingURL=LoginWithAuth0-D92HnzHq.mjs.map