ryuu.js
Version:
Ryuu JavaScript Utility Library
261 lines (237 loc) • 9.4 kB
text/typescript
import { handleNode } from "./utils/domoutils";
import { handleDataUpdated, onDataUpdated } from "./models/services/dataset";
import { handleFiltersUpdated, onFiltersUpdated, requestFiltersUpdate } from "./models/services/filters";
import { handleVariablesUpdated, onVariablesUpdated, requestVariablesUpdate } from "./models/services/variables";
import { handleAppData, onAppDataUpdated, requestAppDataUpdate } from "./models/services/appdata";
import { navigate } from "./models/services/navigation";
import {
get,
getAll,
post,
put,
delete as del,
domoHttp,
} from "./models/services/http";
import {
isSuccess,
isVerifiedOrigin,
getQueryParams,
setFormatHeaders,
generateUniqueId,
} from "./utils/general";
import { DomoEvent, getToken } from "./models/constants/general";
import { AskReplyMap } from "./models/interfaces/ask-reply";
import { handleAck, handleReply } from "./utils/ask-reply";
/**
* The Domo class provides a unified API for interacting with Domo platform features in client applications.
*
* It exposes HTTP methods, event listeners, emitters, and utility functions for working with datasets, filters, variables, app data, and navigation.
*
* Key features:
* - HTTP request methods (get, post, put, delete, domoHttp)
* - Batch request support via getAll
* - Event listeners for data, filters, variables, and app data updates
* - Emitters for sending variables, app data, and navigation events
* - Utility functions for environment, origin verification, and query parsing
* - Handles cross-frame communication and DOM mutation observation for token injection
*/
class Domo {
private static requests: AskReplyMap = {};
public static channel?: MessageChannel;
public static connected = false;
public static listeners: { [index: string]: Function[] } = {
onDataUpdated: [],
onFiltersUpdated: [],
onAppDataUpdated: [],
onVariablesUpdated: [],
};
////////////////////////////////////
// DOMO API
//////////////////////////////////
static get: typeof get = get;
static getAll: typeof getAll = getAll;
static post: typeof post = post;
static put: typeof put = put;
static delete: typeof del = del;
static domoHttp: typeof domoHttp = domoHttp;
////////////////////////////////////////////
// Event Listeners
//
// These receive messages from the parent window via port1 of the MessageChannel
//////////////////////////////////////////
static onDataUpdated = onDataUpdated;
static onFiltersUpdated = onFiltersUpdated;
static onAppDataUpdated = onAppDataUpdated;
static onVariablesUpdated = onVariablesUpdated;
/* @deprecated */
static readonly onFiltersUpdate = this.onFiltersUpdated;
/* @deprecated */
static readonly onDataUpdate = this.onDataUpdated;
/* @deprecated */
static readonly onAppData = this.onAppDataUpdated;
/////////////////////////////////////////////
// Emitters
//
// These send messages to the parent window via port2 of the MessageChannel
///////////////////////////////////////////
static requestFiltersUpdate = requestFiltersUpdate;
static requestVariablesUpdate = requestVariablesUpdate;
static requestAppDataUpdate = requestAppDataUpdate;
static navigate = navigate;
/* @deprecated */
static readonly filterContainer = this.requestFiltersUpdate;
/* @deprecated */
static readonly sendVariables = this.requestVariablesUpdate;
/* @deprecated */
static readonly sendAppData = this.requestAppDataUpdate;
///////////////////////////////////////////
// General
/////////////////////////////////////////
static handleAck = handleAck;
static handleReply = handleReply;
static getRequests = () => this.requests;
static getRequest = (requestId: string) => this.requests[requestId];
static readonly env = getQueryParams();
static readonly __util = {
isVerifiedOrigin,
getQueryParams,
setFormatHeaders,
isSuccess,
};
/**
* Connects to the parent window's Domo instance using a MessageChannel.
* This method sets up message handlers for various events like filtersUpdated, appData, and variablesUpdated.
* It also sends a subscription message to the parent window.
*
* Also sets up a legacy window.postMessage listener for backward compatibility with v4.7.0 and earlier.
*
* @param skipFilters - If true, skips the initial filter updates.
*/
private static connect = (skipFilters = false) => {
if (this.connected) return;
this.connected = true;
this.channel = new MessageChannel();
window.parent.postMessage(
JSON.stringify({ requestId: generateUniqueId(), event: "subscribe", skipFilters }),
"*",
[this.channel.port2]
);
const eventHandlers: {
[event in keyof typeof DomoEvent]: (data: any, responsePort?: MessagePort) => void;
} = {
[DomoEvent.dataUpdated]: handleDataUpdated.bind(this),
[DomoEvent.filtersUpdated]: handleFiltersUpdated.bind(this),
[DomoEvent.appData]: handleAppData.bind(this),
[DomoEvent.variablesUpdated]: handleVariablesUpdated.bind(this),
[DomoEvent.ack]: handleAck.bind(this),
};
// MessageChannel listener (current/new implementation)
this.channel.port1.onmessage = (e: MessageEvent) => {
const [responsePort] = e.ports;
const handler = eventHandlers[e.data.event as keyof typeof DomoEvent];
handler?.(e.data, responsePort);
};
// Legacy window.postMessage listener (v4.7.0 and earlier compatibility)
const legacyMessageHandler = (event: MessageEvent) => {
// Verify origin for security
if (!isVerifiedOrigin(event.origin)) {
return;
}
// Parse message
let message: any;
try {
if (typeof event.data === 'string' && event.data.length > 0) {
message = JSON.parse(event.data);
} else if (typeof event.data === 'object') {
message = event.data;
} else {
return;
}
} catch (err) {
// Invalid JSON, ignore
return;
}
// Detect legacy data update format (has 'alias' property but no 'event' property)
if (message.hasOwnProperty('alias') && !message.hasOwnProperty('event')) {
// Legacy data update message
const handler = eventHandlers[DomoEvent.dataUpdated];
if (handler) {
handler(message);
// Send legacy acknowledgment back to parent
if (event.source && typeof (event.source as any).postMessage === 'function') {
const ack = JSON.stringify({ event: "ack", alias: message.alias });
(event.source as any).postMessage(ack, event.origin);
}
}
return;
}
// Handle standard event-based messages
if (message.event) {
const handler = eventHandlers[message.event as keyof typeof DomoEvent];
if (handler) {
handler(message);
// Send acknowledgment back for non-ack events
if (message.event !== 'ack' && event.source && typeof (event.source as any).postMessage === 'function') {
const ack: any = {
requestId: message.requestId,
event: "ack"
};
// Include relevant data in ack based on event type
if (message.event === DomoEvent.dataUpdated) {
ack.alias = message.alias;
} else if (message.event === DomoEvent.filtersUpdated) {
ack.filters = message.filters;
} else if (message.event === DomoEvent.variablesUpdated) {
ack.variables = message.variables;
}
(event.source as any).postMessage(JSON.stringify(ack), event.origin);
}
}
}
};
window.addEventListener('message', legacyMessageHandler);
};
/**
* Allows consumers to override or extend static methods/properties of the Domo class.
*
* Example Usage:
* import Domo, { get as originalGet } from 'domo.js';
*
* Domo.extend({
* get: (url, options) => {
* // custom logic
* return originalGet(url, options);
* }
* });
*
* @param overrides An object whose keys are static method/property names and values are the new implementations.
*/
static extend(overrides: Partial<Record<keyof typeof Domo, any>>) {
for (const key in overrides) {
if (Object.prototype.hasOwnProperty.call(Domo, key))
(Domo as any)[key as keyof typeof Domo] =
overrides[key as keyof typeof Domo];
}
}
}
/**
* MutationObserver callback that injects the authentication token into any newly added HTML elements.
*
* This function is triggered whenever nodes are added to the DOM (either in the document or head).
* It retrieves the current token and applies it to any new HTMLElement using the handleNode utility.
*
* @param mutations - An array of MutationRecord objects representing the changes to the DOM.
*/
const __mutationObserverCallback = (mutations: any) => {
const token = getToken();
for (const record of mutations) {
record.addedNodes.forEach((node: any) => {
if (node instanceof HTMLElement) handleNode(node, token);
});
}
};
const ob = new MutationObserver(__mutationObserverCallback);
ob.observe(document.documentElement, { childList: true });
ob.observe(document.head, { childList: true });
export default Domo;
export { Domo, __mutationObserverCallback };