ryuu.js
Version:
Ryuu JavaScript Utility Library
1,623 lines (1,245 loc) • 48 kB
Markdown
# CLAUDE.md - Development Guide for AI Assistance
This document provides comprehensive information about the domo.js/ryuu.js codebase architecture, implementation patterns, and internal workings. It is specifically designed to help AI assistants (like Claude) understand and work effectively with this codebase.
## Table of Contents
1. [Project Overview](#project-overview)
2. [Architecture](#architecture)
3. [Directory Structure](#directory-structure)
4. [Core Components](#core-components)
5. [Communication Patterns](#communication-patterns)
6. [Type System](#type-system)
7. [Services Layer](#services-layer)
8. [Utilities Layer](#utilities-layer)
9. [Mobile Integration](#mobile-integration)
10. [Design Patterns & Conventions](#design-patterns--conventions)
11. [Testing Strategy](#testing-strategy)
12. [Build & Development](#build--development)
13. [Common Development Tasks](#common-development-tasks)
## Project Overview
**Name:** ryuu.js (published name) / domo.js (development name)
**Purpose:** JavaScript SDK for developing custom applications within the Domo platform
**Version:** 5.1.3
**Package:** Published as `ryuu.js` on npm
### What This Library Does
ryuu.js enables developers to build custom applications (iframed web apps) that run within the Domo platform. The library provides:
1. **Bidirectional Communication**: Facilitates message passing between the custom app (child iframe) and the Domo platform (parent window)
2. **HTTP API Access**: Authenticated requests to Domo APIs (datasets, datastores, environment, etc.)
3. **Event System**: Real-time updates for dataset changes, filter updates, and variable changes
4. **Mobile Support**: Cross-platform compatibility with iOS and Android mobile apps using platform-specific APIs
5. **Type Safety**: Full TypeScript support with comprehensive type definitions
### Key Technical Decisions
- **Zero Runtime Dependencies**: Minimizes bundle size and avoids dependency conflicts
- **Fetch API**: Modern replacement for XMLHttpRequest
- **MessageChannel**: Primary communication mechanism (more reliable than postMessage alone)
- **Static Class Pattern**: No instantiation required, all methods are static
- **MutationObserver**: Automatic token injection into DOM for seamless authentication
## Architecture
### High-Level Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Parent Window │
│ (Domo Platform/Mobile App) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Custom App (iframe) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Public API (Domo class) │ │ │
│ │ │ - HTTP methods, events, navigation │ │ │
│ │ └───────────────────┬──────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Services Layer │ │ │
│ │ │ HTTP | Filters | Variables | AppData │ │ │
│ │ │ Dataset | Navigation │ │ │
│ │ └───────────────────┬──────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Utilities Layer │ │ │
│ │ │ Validation | Headers | DOM | Type Guards │ │ │
│ │ └───────────────────┬──────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Models (Interfaces, Enums, Types) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ MessageChannel / postMessage │
│ webkit.messageHandlers (iOS) │
│ window.domovariable (Android) │
└─────────────────────────────────────────────────────────────┘
```
### Communication Flow
**Desktop/Web:**
```
Custom App (iframe)
↕ MessageChannel (Port1 ↔ Port2)
Parent Window (Domo Platform)
```
**Mobile:**
```
Custom App (WebView)
↕ webkit.messageHandlers (iOS) OR window.domovariable/domofilter (Android)
Native Mobile App (iOS/Android)
↕ Platform-specific bridge
Parent Window (Domo Platform)
```
### Request-Reply Pattern
The library implements an acknowledgment-based async communication pattern:
1. **ASK**: App sends a request to parent with unique ID
2. **ACK**: Parent acknowledges receipt (optional callback)
3. **REPLY**: Parent sends response with matching ID (completion callback)
This pattern enables reliable async operations with status tracking via `Domo.getRequests()`.
## Directory Structure
```
src/
├── index.ts # Entry point - exports all public APIs
├── domo.ts # Core Domo class implementation
├── domo.test.ts # Domo class unit tests
│
├── types/
│ └── global.d.ts # Global TypeScript declarations (mobile)
│
├── models/
│ ├── constants/
│ │ └── general.ts # DomoEvent, eventToListenerMap, getToken
│ │
│ ├── enums/
│ │ ├── askReply.ts # EventType (ASK, ACK, REPLY)
│ │ ├── data-formats.ts # DataFormats enum
│ │ ├── domo-data-types.ts # DomoDataTypes enum
│ │ └── request-methods.ts # RequestMethods enum
│ │
│ ├── interfaces/
│ │ ├── ask-reply.ts # AskReplyMap interface
│ │ ├── filter.ts # Filter types and operators
│ │ ├── json.ts # Json type definitions
│ │ ├── request.ts # Request/Response interfaces
│ │ └── variable.ts # Variable interface
│ │
│ └── services/
│ ├── appdata.ts # App data communication
│ ├── dataset.ts # Dataset update events
│ ├── filters.ts # Filter management
│ ├── http.ts # HTTP request service (CORE)
│ ├── navigation.ts # Navigation service
│ └── variables.ts # Variable management
│
└── utils/
├── ask-reply.ts # Request tracking utilities
├── data-helpers.ts # Format conversion
├── domoutils.ts # DOM and header utilities
├── filter.ts # Filter validation
├── general.ts # General utilities
└── variable.ts # Variable validation
```
### File Organization Rationale
- **models/**: Pure data structures - interfaces, enums, types, constants
- **services/**: Business logic - communication, event handling, HTTP operations
- **utils/**: Helper functions - validation, conversion, DOM manipulation
- **types/**: Global type declarations - environment-specific typings
## Core Components
### The Domo Class
Location: [src/domo.ts](src/domo.ts)
The `Domo` class is the main entry point and only public-facing class. It's a static class (no instantiation required).
#### Key Properties
```typescript
class Domo {
// Communication
static channel?: MessageChannel;
static connected: boolean = false;
// Request tracking
private static requests: AskReplyMap = {};
// Event listeners registry
static listeners: { [index: string]: Function[] } = {
[DomoEvent.DATA_UPDATED]: [],
[DomoEvent.FILTERS_UPDATED]: [],
[DomoEvent.VARIABLES_UPDATED]: [],
[DomoEvent.APP_DATA]: []
};
// Environment info (from URL query params)
static env: QueryParams;
// Internal utilities (exposed for advanced use)
static __util: {
isSuccess,
isVerifiedOrigin,
getQueryParams,
setFormatHeaders
};
}
```
#### Initialization Flow
1. **Lazy Initialization**: `connect()` is called when first event listener is registered
2. **MessageChannel Creation**: Creates channel with two ports
3. **Subscribe Message**: Posts 'subscribe' message with port2 to parent (includes `skipFilters` flag)
4. **Dual Message Handlers**:
- **MessageChannel listener**: Handles modern communication via `port1.onmessage`
- **Legacy window.postMessage listener**: Maintains backward compatibility with v4.7.0 and earlier
5. **Origin Verification**: Legacy handler uses `isVerifiedOrigin()` for security
6. **Event Dispatching**: Both handlers map events to appropriate service handlers
See [src/domo.ts:118-228](src/domo.ts#L118-L228) for the `connect()` implementation.
#### Methods Overview
**HTTP Methods** (see [Services Layer](#services-layer)):
- `get(url, options?)` - GET request
- `getAll(urls[], options?)` - Parallel GET requests
- `post(url, body?, options?)` - POST request
- `put(url, body?, options?)` - PUT request
- `delete(url, options?)` - DELETE request
- `domoHttp(method, url, options?, body?)` - Low-level HTTP
**Event Listeners** (return unsubscribe function):
- `onDataUpdated(callback)` - Dataset updates
- `onFiltersUpdated(callback)` - Filter changes
- `onVariablesUpdated(callback)` - Variable changes
- `onAppDataUpdated(callback)` - Custom app data
**Emitters** (send to parent):
- `requestFiltersUpdate(filters, pageStateUpdate?, onAck?, onReply?)` - Update filters
- `requestVariablesUpdate(variables, onAck?, onReply?)` - Update variables
- `requestAppDataUpdate(appData, onAck?, onReply?)` - Send app data
- `navigate(url, isNewWindow?)` - Navigate to page
**Utilities**:
- `env` - Environment query parameters
- `extend(overrides)` - Override static methods/properties
- `getRequests()` - Get all tracked requests
- `getRequest(id)` - Get specific request by ID
#### Deprecated Methods
These methods still exist for backward compatibility but redirect to new names:
- `onDataUpdate` → `onDataUpdated`
- `onFiltersUpdate` → `onFiltersUpdated`
- `onAppData` → `onAppDataUpdated`
- `filterContainer` → `requestFiltersUpdate`
- `sendVariables` → `requestVariablesUpdate`
- `sendAppData` → `requestAppDataUpdate`
See [src/domo.ts:330-345](src/domo.ts#L330-L345) for deprecation implementations.
## Communication Patterns
### MessageChannel Communication
**Setup Process:**
1. App creates `MessageChannel` with two ports
2. App posts 'subscribe' message to parent with `port2` via `postMessage`
3. Parent receives port2 and stores it for future communication
4. Parent sends messages to app via port2
5. App receives messages on port1
**Code Location:** [src/domo.ts:250-284](src/domo.ts#L250-L284)
### Message Structure
All messages follow this structure:
```typescript
{
event: string, // Event name (e.g., 'filtersUpdated')
eventType?: EventType, // ASK, ACK, or REPLY
requestId?: string, // Unique ID for request tracking
data?: any // Payload
}
```
### Event Types
Defined in [src/models/enums/askReply.ts](src/models/enums/askReply.ts):
- **ASK**: Initial request from app to parent
- **ACK**: Acknowledgment from parent (request received)
- **REPLY**: Final response from parent (request completed)
### Communication Methods
#### From Parent to App (Received):
1. **Data Updated**: `{ event: 'dataUpdated', data: datasetAlias }`
2. **Filters Updated**: `{ event: 'filtersUpdated', data: filters[] }`
3. **Variables Updated**: `{ event: 'variablesUpdated', data: variables }`
4. **App Data Updated**: `{ event: 'appData', data: appData }`
5. **Acknowledgment**: `{ event: 'ack', eventType: 'ACK', requestId }`
6. **Reply**: `{ event: eventName, eventType: 'REPLY', requestId, data }`
#### From App to Parent (Sent):
1. **Request Filters Update**: `{ event: 'filtersUpdate', eventType: 'ASK', requestId, data: filters }`
2. **Request Variables Update**: `{ event: 'variablesUpdate', eventType: 'ASK', requestId, data: variables }`
3. **Request App Data Update**: `{ event: 'appData', eventType: 'ASK', data: appData }`
4. **Navigate**: `{ event: 'navigate', data: { route, isNewWindow } }`
### Request Tracking
The library tracks all ASK-type requests in `Domo.requests`:
```typescript
interface AskReplyMap {
[requestId: string]: {
request: AskRequestStatus; // { status: 'PENDING' | 'SENT', timestamp }
response?: AskResponseStatus; // { status: 'SUCCESS' | 'FAILURE', timestamp, data }
}
}
```
**Workflow:**
1. App calls `requestFiltersUpdate(filters, true, onAck, onReply)`
2. Request stored with unique ID, status 'pending', and callbacks
```typescript
this.requests[requestId] = {
request: {
payload: message,
onAck,
onReply,
status: "pending",
sentAt: Date.now()
}
};
```
3. Message sent to parent (via postMessage or mobile bridge)
4. Status remains 'pending' until acknowledgment
5. Parent sends ACK → `handleAck()` invokes stored `onAck()` callback
6. Parent sends REPLY → `handleReply()` invokes stored `onReply(data)` callback
7. Response stored in requests map with success/error data
**Callback Support:**
- `onAck`: Called when parent acknowledges receipt (request processed)
- `onReply`: Called when operation completes with result data
- Both callbacks are optional - supports `() => void` or `() => null` patterns
See [src/utils/ask-reply.ts](src/utils/ask-reply.ts) for implementation.
## Type System
### Core Interfaces
#### Filter Interface
Location: [src/models/interfaces/filter.ts](src/models/interfaces/filter.ts)
```typescript
interface Filter {
column: string;
operator: FilterOperatorsString | FilterOperatorsNumeric;
values: any[];
dataType: FilterDataTypes;
dataSourceId?: string;
label?: string;
}
type FilterOperatorsString =
| 'IN' | 'NOT_IN'
| 'CONTAINS' | 'NOT_CONTAINS'
| 'STARTS_WITH' | 'NOT_STARTS_WITH'
| 'ENDS_WITH' | 'NOT_ENDS_WITH';
type FilterOperatorsNumeric =
| 'EQUALS' | 'NOT_EQUALS'
| 'GREATER_THAN' | 'GREAT_THAN_EQUALS_TO'
| 'LESS_THAN' | 'LESS_THAN_EQUALS_TO'
| 'BETWEEN';
type FilterDataTypes = 'STRING' | 'NUMERIC' | 'DATE' | 'DATETIME';
```
#### Variable Interface
Location: [src/models/interfaces/variable.ts](src/models/interfaces/variable.ts)
```typescript
interface Variable {
functionId: number;
value: any;
}
```
#### Request/Response Interfaces
Location: [src/models/interfaces/request.ts](src/models/interfaces/request.ts)
```typescript
// Request options with format-specific typing
interface RequestOptions<F extends DomoDataFormats = 'array-of-objects'> {
format?: F;
query?: { [key: string]: any };
contentType?: string;
fetchImpl?: typeof fetch;
}
// Response body types (format-specific)
type ObjectResponseBody = { [key: string]: any };
interface ArrayResponseBody {
datasource: string;
metadata: Array<{ type: DomoDataTypes }>;
columns: string[];
rows: any[][];
numRows: number;
numColumns: number;
fromcache: boolean;
}
type ResponseBody<F extends DomoDataFormats> =
F extends 'array-of-arrays' ? ArrayResponseBody :
F extends 'csv' ? string :
F extends 'excel' ? Blob :
F extends 'plain' ? string :
ObjectResponseBody[];
```
**Key Design:** Conditional types enable format-specific return types. When you call `Domo.get(url, { format: 'csv' })`, TypeScript knows the return type is `string`.
### Enums
#### DataFormats
Location: [src/models/enums/data-formats.ts](src/models/enums/data-formats.ts)
```typescript
enum DataFormats {
ARRAY_OF_OBJECTS = 'array-of-objects',
JSON = 'array-of-arrays',
CSV = 'csv',
EXCEL = 'excel',
PLAIN = 'plain'
}
```
**Note:** `JSON` enum value maps to 'array-of-arrays' format string (historical naming).
#### DomoDataTypes
Location: [src/models/enums/domo-data-types.ts](src/models/enums/domo-data-types.ts)
```typescript
enum DomoDataTypes {
STRING = 'STRING',
LONG = 'LONG',
DECIMAL = 'DECIMAL',
DOUBLE = 'DOUBLE',
DATE = 'DATE',
DATETIME = 'DATETIME'
}
```
#### RequestMethods
Location: [src/models/enums/request-methods.ts](src/models/enums/request-methods.ts)
```typescript
enum RequestMethods {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE'
}
```
### Type Guards
Type guards narrow TypeScript types and validate runtime values.
#### Filter Type Guards
Location: [src/utils/filter.ts](src/utils/filter.ts)
```typescript
function isFilter(filter: any): filter is Filter {
return (
typeof filter === 'object' &&
filter !== null &&
typeof filter.column === 'string' &&
typeof filter.operator === 'string' &&
Array.isArray(filter.values) &&
typeof filter.dataType === 'string'
);
}
function isFilterArray(filters: any): filters is Filter[] {
return Array.isArray(filters) && filters.every(isFilter);
}
function guardAgainstInvalidFilters(filters: any): void {
if (!isFilterArray(filters)) {
throw new TypeError(
'Invalid filters: expected array of Filter objects with column, operator, values, and dataType'
);
}
}
```
#### Variable Type Guards
Location: [src/utils/variable.ts](src/utils/variable.ts)
Similar pattern for Variable validation.
**Usage Pattern:**
```typescript
// In service functions
export function requestFiltersUpdate(filters: Filter[]) {
guardAgainstInvalidFilters(filters); // Throws if invalid
// Proceed with valid filters
}
```
## Services Layer
Services implement the core business logic and are bound to the Domo class via `this` context.
### HTTP Service
Location: [src/models/services/http.ts](src/models/services/http.ts)
The HTTP service is the most critical component, handling all API requests.
#### Key Functions
**domoHttp** (Low-level HTTP):
```typescript
function domoHttp<F extends DomoDataFormats = 'array-of-objects'>(
method: RequestMethods,
url: string,
options?: RequestOptions<F>,
body?: RequestBody
): Promise<ResponseBody<F>>
```
**Implementation Details:**
1. **Format Header Setup**: Converts user-facing format to Accept header
2. **Auth Token**: Retrieves `__RYUU_SID__` token and adds to headers
3. **Fetch Request**: Uses native fetch or custom implementation
4. **Response Processing**: Parses response based on format
5. **Error Handling**: Throws detailed error objects
See [src/models/services/http.ts:122-186](src/models/services/http.ts#L122-L186) for implementation.
**Overloaded Functions:**
```typescript
// Format-specific overloads for type safety
function get<F extends DomoDataFormats>(
url: string,
options: RequestOptions<F>
): Promise<ResponseBody<F>>;
function get(url: string): Promise<ObjectResponseBody[]>;
```
This enables TypeScript to infer correct return types:
```typescript
const objects = await Domo.get('/data/v1/sales'); // ObjectResponseBody[]
const csv = await Domo.get('/data/v1/sales', { format: 'csv' }); // string
const arrays = await Domo.get('/data/v1/sales', { format: 'array-of-arrays' }); // ArrayResponseBody
```
#### getAll Implementation
Parallel requests using `Promise.all`:
```typescript
function getAll<F extends DomoDataFormats = 'array-of-objects'>(
urls: string[],
options?: RequestOptions<F>
): Promise<ResponseBody<F>[]> {
return Promise.all(urls.map(url => get.call(this, url, options)));
}
```
### Filters Service
Location: [src/models/services/filters.ts](src/models/services/filters.ts)
#### requestFiltersUpdate
Sends filter update request to parent (and mobile platforms).
```typescript
function requestFiltersUpdate(
filters: Filter[],
pageStateUpdate: boolean = true,
onAck?: () => void,
onReply?: (data: any) => void
): string
```
**Implementation:**
1. **Validation**: `guardAgainstInvalidFilters(filters)`
2. **Request Tracking**: Generates unique ID, stores in `this.requests` with onAck/onReply callbacks
3. **Mobile Detection**: Uses `isMobile()` to detect iOS/Android devices
4. **Platform-Specific Sending**:
- **Web (desktop)**: Posts to `window.parent` via postMessage
- **Mobile**: Tries `domofilter.postMessage()` first, then falls back to:
- iOS: `webkit.messageHandlers.domofilter.postMessage()`
- Other: `window.parent.postMessage()`
5. **Returns**: Request ID for tracking
See [src/models/services/filters.ts](src/models/services/filters.ts).
#### onFiltersUpdated
Registers callback for filter changes. When the first listener is registered, automatically sends an initial filter request.
```typescript
function onFiltersUpdated(callback: (filters?: Filter[]) => unknown): () => void
```
**Returns**: Unsubscribe function to remove listener.
**Implementation:**
```typescript
const hasHandlers = this.listeners.onFiltersUpdated.length > 0;
this.connect(); // Ensure MessageChannel is initialized
this.listeners.onFiltersUpdated.push(callback);
// If this is the first listener, request initial filters
if (!hasHandlers) {
this.requestFiltersUpdate(null, false);
}
// Return unsubscribe function
return () => {
const index = this.listeners.onFiltersUpdated.indexOf(callback);
if (index >= 0) {
this.listeners.onFiltersUpdated.splice(index, 1);
}
};
```
**Key Behavior:** The first listener triggers an initial filter request to populate the app with current filter state.
#### handleFiltersUpdated
Internal handler that processes incoming filter messages and invokes callbacks. Also handles acknowledgments and reply tracking.
```typescript
function handleFiltersUpdated(message: any, responsePort?: MessagePort): void {
if (!message) return;
// Send acknowledgment back through response port if available
if (this.listeners.onFiltersUpdated.length) {
responsePort?.postMessage({
requestId: message.requestId,
event: "ack",
filters: message.filters
});
// Invoke all registered callbacks
this.listeners.onFiltersUpdated.forEach((cb: Function) =>
cb(message.filters)
);
}
// Handle reply for request tracking
this.handleReply(message.requestId, message.filters, message.error);
}
```
**Key Features:**
- Accepts optional `responsePort` for MessageChannel acknowledgments
- Automatically sends ACK message when callbacks exist
- Processes reply for request tracking (onAck/onReply callbacks)
### Variables Service
Location: [src/models/services/variables.ts](src/models/services/variables.ts)
Similar pattern to Filters service:
- `requestVariablesUpdate(variables, onAck?, onReply?)` - Send updates
- `onVariablesUpdated(callback)` - Listen for changes
- `handleVariablesUpdated(variables)` - Internal handler
**Mobile Integration:**
- Tries `domovariable.postMessage()` first (global object injected by mobile apps)
- Falls back to `webkit.messageHandlers.domovariable.postMessage()` for iOS
- Falls back to `window.parent.postMessage()` for other platforms
### Dataset Service
Location: [src/models/services/dataset.ts](src/models/services/dataset.ts)
Handles dataset update events:
- `onDataUpdated(callback)` - Register listener for dataset changes
- `handleDataUpdated(datasetAlias)` - Internal handler
**Use Case:** When a dataset backing your app is updated in Domo, your app receives notification to refresh data without page reload.
### AppData Service
Location: [src/models/services/appdata.ts](src/models/services/appdata.ts)
Custom data communication between apps on the same page:
- `requestAppDataUpdate(data, onAck?, onReply?)` - Send custom data
- `onAppDataUpdated(callback)` - Listen for custom data
- `handleAppData(data)` - Internal handler
### Navigation Service
Location: [src/models/services/navigation.ts](src/models/services/navigation.ts)
```typescript
function navigate(route: string, isNewWindow: boolean = false): void {
this.channel?.port1.postMessage({
event: 'navigate',
data: { route, isNewWindow }
});
}
```
Simple message posting to trigger navigation in parent window.
## Utilities Layer
### General Utilities
Location: [src/utils/general.ts](src/utils/general.ts)
#### isSuccess
```typescript
function isSuccess(status: number): boolean {
return status >= 200 && status < 300;
}
```
Checks if HTTP status code indicates success.
#### isVerifiedOrigin
```typescript
function isVerifiedOrigin(origin: string): boolean {
const domoDomains = [
'domo.com',
'domolabs.io',
'domo-dev.com',
'domo-qa.com',
'localhost'
];
return domoDomains.some(domain =>
origin.includes(domain) ||
origin.startsWith('file://') ||
origin === 'null'
);
}
```
Security check for message origin validation. Used to verify messages come from trusted Domo domains.
#### getQueryParams
```typescript
function getQueryParams(): QueryParams {
const params = new URLSearchParams(window.location.search);
return {
userId: params.get('userId'),
customer: params.get('customer'),
locale: params.get('locale'),
// ... all other params
};
}
```
Parses URL query parameters into `Domo.env` object.
#### generateUniqueId
```typescript
function generateUniqueId(): string {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
```
Generates request IDs for tracking.
#### isIOS
```typescript
function isIOS(): boolean {
const userAgent = navigator.userAgent.toLowerCase();
const hasIOSUserAgent = /(?:iphone|ipad|ipod)/.test(userAgent);
const isPossibleIPadDesktopMode = /mac os x/.test(userAgent) &&
'ontouchend' in document &&
navigator.maxTouchPoints > 1;
// Additional checks for reliability
const hasIOSAPIs = webkit?.messageHandlers !== undefined;
const isStandalone = navigator.standalone === true;
const hasMobileScreenRatio = screen?.width < 1024;
return hasIOSUserAgent || isPossibleIPadDesktopMode ||
([hasIOSAPIs, isStandalone, hasMobileScreenRatio].filter(Boolean).length >= 2);
}
```
Multi-factor iOS detection with support for iPad desktop mode and enhanced reliability checks.
#### isMobile
```typescript
function isMobile(): boolean {
if (isIOS()) return true;
const userAgent = navigator.userAgent.toLowerCase();
const hasMobileUserAgent = /android|webos|blackberry|iemobile|opera mini|mobile|phone/.test(userAgent);
if (hasMobileUserAgent) return true;
// Require multiple indicators for edge cases
const hasMobileAPIs = window.domovariable !== undefined || window.domofilter !== undefined;
const hasTouchSupport = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const hasMobileScreenRatio = screen?.width < 1024;
return [hasMobileAPIs, hasTouchSupport, hasMobileScreenRatio].filter(Boolean).length >= 2;
}
```
Comprehensive mobile detection covering iOS, Android, and other mobile platforms.
### DOM Utilities
Location: [src/utils/domoutils.ts](src/utils/domoutils.ts)
#### setAuthTokenHeader
```typescript
function setAuthTokenHeader(headers: HeadersInit): HeadersInit {
const token = getToken();
if (token) {
return {
...headers,
'X-DOMO-Authentication': token
};
}
return headers;
}
```
Adds authentication token to request headers.
#### processBody
```typescript
function processBody(body: RequestBody): string {
return typeof body === 'string' ? body : JSON.stringify(body);
}
```
Serializes request body to JSON if needed.
#### handleNode
```typescript
function handleNode(mutation: MutationRecord): void {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) { // Element node
const element = node as Element;
if (element.tagName === 'SCRIPT' && element.hasAttribute('data-token')) {
const token = element.getAttribute('data-token');
window.__RYUU_SID__ = token;
}
}
});
}
```
MutationObserver callback that watches for token injection into DOM. The parent window injects a script tag with `data-token` attribute, and this function captures it.
### Data Helpers
Location: [src/utils/data-helpers.ts](src/utils/data-helpers.ts)
#### domoFormatToRequestFormat
```typescript
function domoFormatToRequestFormat(format: DomoDataFormats): string {
const formatMap: { [key: string]: string } = {
'array-of-objects': 'array-of-objects',
'array-of-arrays': 'array-of-arrays',
'csv': 'csv',
'excel': 'excel',
'plain': 'plain'
};
return formatMap[format] || 'array-of-objects';
}
```
Maps user-facing format names to internal format strings.
### Format Headers
```typescript
function setFormatHeaders(
format: DomoDataFormats,
headers: HeadersInit
): HeadersInit {
const acceptHeaders: { [key: string]: string } = {
'array-of-objects': 'application/json',
'array-of-arrays': 'application/array-of-arrays',
'csv': 'text/csv',
'excel': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'plain': 'text/plain'
};
return {
...headers,
'Accept': acceptHeaders[format] || 'application/json'
};
}
```
Sets appropriate Accept header based on requested format.
## Mobile Integration
### iOS Integration
Location: [src/models/services/filters.ts](src/models/services/filters.ts), [src/models/services/variables.ts](src/models/services/variables.ts)
**webkit.messageHandlers API:**
iOS provides native message handlers for bidirectional communication between WebView and native app:
```typescript
// Modern approach (try global object first)
try {
domofilter.postMessage(JSON.stringify(filters));
} catch (error) {
// Fallback to webkit handlers
if (isIOS()) {
window.webkit?.messageHandlers?.domofilter?.postMessage(filters);
}
}
```
**Available Handlers:**
- `webkit.messageHandlers.domofilter` - Filter communication
- `webkit.messageHandlers.domovariable` - Variable communication
**Detection:** Uses `isIOS()` which checks:
1. User agent for iPhone/iPad/iPod
2. iPad desktop mode (macOS UA + touch support)
3. Multiple iOS-specific indicators (webkit APIs, standalone mode, screen dimensions)
### Android/Flutter Integration
**Global Objects:**
Android/Flutter apps inject global objects into the WebView that are accessed directly:
```typescript
// Try global object (injected by native mobile apps)
try {
domovariable.postMessage(JSON.stringify(variables));
} catch (error) {
// Fallback to window.parent for web or other platforms
window.parent.postMessage(message, "*");
}
// Similar pattern for filters
try {
domofilter.postMessage(JSON.stringify(filters));
} catch (error) {
// Fallback to iOS or web
}
```
**Detection:** Uses `isMobile()` which checks:
1. `isIOS()` first (returns true if iOS)
2. User agent for Android/WebOS/BlackBerry/Mobile
3. Multiple mobile indicators (global APIs, touch support, screen dimensions)
**Type Declarations:**
Location: [src/types/global.d.ts](src/types/global.d.ts)
```typescript
declare global {
var domovariable: {
postMessage(message: string): void;
};
var domofilter: {
postMessage(message: string): void;
};
interface Window {
webkit?: {
messageHandlers?: {
domofilter?: { postMessage(message: any): void };
domovariable?: { postMessage(message: any): void };
};
};
domovariable?: { postMessage(message: string): void };
domofilter?: { postMessage(message: string): void };
}
}
```
### Platform Detection
The `isIOS()` utility uses multiple checks to reliably detect iOS:
1. Check for webkit.messageHandlers presence
2. Check navigator.platform for iOS devices
3. Fallback to user agent string
This multi-factor approach avoids false positives from browser spoofing.
## Design Patterns & Conventions
### 1. Static Class Pattern
All methods are static - no instantiation required:
```typescript
// No: new Domo()
// Yes: Domo.get()
```
**Rationale:**
- Simpler API (no need to create instances)
- Singleton behavior (one connection per app)
- Clean namespace (all methods under Domo.*)
### 2. Observer Pattern
Event listeners use observer pattern with callback registration:
```typescript
const unsubscribe = Domo.onFiltersUpdated((filters) => {
console.log('Filters updated:', filters);
});
// Later...
unsubscribe(); // Remove listener
```
**Implementation:**
- Listeners stored in `Domo.listeners` object by event name
- Subscription returns unsubscribe function
- Handlers invoke all registered callbacks
### 3. Request-Reply Pattern
Async operations use ASK-ACK-REPLY pattern:
```typescript
Domo.requestFiltersUpdate(
filters,
true,
() => console.log('ACK received'), // onAck callback
(data) => console.log('REPLY received:', data) // onReply callback
);
```
**Flow:**
1. ASK sent with unique ID
2. ACK received when parent processes request
3. REPLY received when operation completes
### 4. Type Guard Pattern
Validation functions that narrow TypeScript types:
```typescript
function isFilter(filter: any): filter is Filter {
return (
typeof filter === 'object' &&
filter !== null &&
typeof filter.column === 'string' &&
// ... other checks
);
}
```
**Benefits:**
- Runtime validation
- Type narrowing for TypeScript
- Reusable validation logic
### 5. Overloaded Functions
Multiple function signatures for type-safe responses:
```typescript
function get<F extends DomoDataFormats>(
url: string,
options: RequestOptions<F>
): Promise<ResponseBody<F>>;
function get(url: string): Promise<ObjectResponseBody[]>;
```
TypeScript selects correct overload based on arguments, ensuring return type matches format.
### 6. Decorator Pattern (MutationObserver)
Token injection uses MutationObserver to "decorate" HTTP requests with auth:
```typescript
const observer = new MutationObserver((mutations) => {
mutations.forEach(handleNode);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
```
Automatically captures auth token without requiring manual intervention.
### 7. Flexible Callback Pattern
Event listeners and emitters support flexible callback signatures:
```typescript
// All of these are valid:
Domo.onFiltersUpdated((filters) => {
console.log(filters);
});
Domo.onFiltersUpdated(() => {
// No parameters needed
});
Domo.onFiltersUpdated(() => null); // Explicit null return
// Emitters with optional callbacks
Domo.requestFiltersUpdate(
filters,
true,
() => console.log('Acknowledged'),
(data) => console.log('Completed:', data)
);
// Or without callbacks
Domo.requestFiltersUpdate(filters);
// Or with only one callback
Domo.requestFiltersUpdate(filters, true, () => console.log('Ack'));
```
**Implementation:** Uses TypeScript's `unknown` return type and `Function` type to allow maximum flexibility while maintaining type safety for parameters.
### Naming Conventions
**Services:**
- Listeners: `on[Event]Updated` (e.g., `onFiltersUpdated`)
- Emitters: `request[Event]Update` (e.g., `requestFiltersUpdate`)
- Handlers: `handle[Event]` (e.g., `handleFiltersUpdated`)
**Utilities:**
- Type Guards: `is[Type]` (e.g., `isFilter`)
- Validation: `guardAgainst[Invalid]` (e.g., `guardAgainstInvalidFilters`)
- Headers: `set[Type]Headers` (e.g., `setAuthTokenHeader`)
**Constants:**
- Enums: PascalCase (e.g., `DataFormats.ARRAY_OF_OBJECTS`)
- Event names: PascalCase with underscores (e.g., `DomoEvent.FILTERS_UPDATED`)
### Error Handling
Comprehensive error objects with context:
```typescript
throw {
message: 'Request failed',
status: response.status,
statusText: response.statusText,
headers: response.headers,
body: await response.text()
};
```
Provides full context for debugging failed requests.
### Context Binding
Service functions use `this` context, bound to Domo class:
```typescript
// In domo.ts
static get = get.bind(this);
static onFiltersUpdated = onFiltersUpdated.bind(this);
```
Allows services to access `this.channel`, `this.listeners`, etc.
## Testing Strategy
### Test Framework
**Jest** with **ts-jest** for TypeScript support.
Configuration: [jest.config.js](jest.config.js)
```javascript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
testMatch: ['**/*.test.ts']
};
```
### Test Structure
Each module has a corresponding `.test.ts` file:
- [src/domo.test.ts](src/domo.test.ts) - Main class tests
- [src/models/services/http.test.ts](src/models/services/http.test.ts) - HTTP service tests
- [src/models/services/filters.test.ts](src/models/services/filters.test.ts) - Filter service tests
- [src/models/services/variables.test.ts](src/models/services/variables.test.ts) - Variable service tests
- [src/models/services/appdata.test.ts](src/models/services/appdata.test.ts) - AppData service tests
- [src/models/services/dataset.test.ts](src/models/services/dataset.test.ts) - Dataset service tests
- [src/models/services/navigation.test.ts](src/models/services/navigation.test.ts) - Navigation service tests
- [src/utils/filter.test.ts](src/utils/filter.test.ts) - Filter utility tests
- [src/utils/general.test.ts](src/utils/general.test.ts) - General utility tests
- [src/utils/domoutils.test.ts](src/utils/domoutils.test.ts) - DOM utility tests
- [src/utils/ask-reply.test.ts](src/utils/ask-reply.test.ts) - Request tracking tests
### Common Test Patterns
#### Mocking MessageChannel
```typescript
const mockPostMessage = jest.fn();
Domo.channel = {
port1: {
onmessage: null,
postMessage: mockPostMessage
}
} as any;
```
#### Mocking Fetch
```typescript
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve([{ id: 1, name: 'Test' }])
})
) as jest.Mock;
```
#### Testing Event Listeners
```typescript
test('onFiltersUpdated registers callback', () => {
const callback = jest.fn();
const unsubscribe = Domo.onFiltersUpdated(callback);
// Trigger event
Domo.handleFiltersUpdated([mockFilter]);
expect(callback).toHaveBeenCalledWith([mockFilter]);
// Test unsubscribe
unsubscribe();
Domo.handleFiltersUpdated([mockFilter]);
expect(callback).toHaveBeenCalledTimes(1); // Should not be called again
});
```
### Running Tests
```bash
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- --coverage # Coverage report
```
## Build & Development
### Package Configuration
**package.json** key fields:
```json
{
"name": "ryuu.js",
"version": "5.1.3",
"main": "dist/domo.js",
"types": "dist/domo.d.ts",
"files": ["dist"],
"scripts": {
"build": "rm -rf dist && mkdir dist && NODE_ENV=production webpack && cp dist/domo.js demo/domo.js",
"test": "jest --silent",
"coverage": "jest --coverage --silent",
"type-check": "tsc --noEmit",
"release": "npm run build && npm publish",
"releaseAlpha": "npm run build && npm publish --tag alpha",
"releaseBeta": "npm run build && npm publish --tag beta",
"bumpAlpha": "npm run build && npm run bump -- --prerelease alpha",
"bumpBeta": "npm run build && npm run bump -- --prerelease beta",
"bump": "standard-version"
}
}
```
### Build System
**Webpack 5** configuration: [webpack.config.js](webpack.config.js)
**Key settings:**
- Entry: `src/index.ts`
- Output: `dist/domo.js` (UMD bundle)
- Library: Exposed as `Domo` global
- TypeScript: Compiled with `ts-loader`
- Type Declarations: Generated by TypeScript compiler
### TypeScript Configuration
**tsconfig.json** key settings:
```json
{
"compilerOptions": {
"target": "ES2015",
"module": "ESNext",
"lib": ["ES2015", "DOM"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
```
### Development Workflow
1. **Install dependencies:** `npm install`
2. **Run tests:** `npm test`
3. **Type check:** `npm run type-check`
4. **Build:** `npm run build`
5. **Test in app:** Copy `dist/domo.js` to custom app project
### Publishing
```bash
npm version patch # Increment version
npm run build # Build dist files
npm publish # Publish to npm
```
**Note:** Published as `ryuu.js`, but developed as `domo.js`.
## Common Development Tasks
### Adding a New HTTP Method
1. **Define in RequestMethods enum** (if needed):
```typescript
// src/models/enums/request-methods.ts
enum RequestMethods {
PATCH = 'PATCH' // Add new method
}
```
2. **Create service function**:
```typescript
// src/models/services/http.ts
export function patch<F extends DomoDataFormats = 'array-of-objects'>(
url: string,
body?: RequestBody,
options?: RequestOptions<F>
): Promise<ResponseBody<F>> {
return domoHttp.call(this, RequestMethods.PATCH, url, options, body);
}
```
3. **Add to Domo class**:
```typescript
// src/domo.ts
static patch = patch.bind(this);
```
4. **Export from index**:
```typescript
// src/index.ts
export { patch } from './models/services/http';
```
5. **Add tests**:
```typescript
// src/models/services/http.test.ts
test('patch sends PATCH request', async () => {
// Test implementation
});
```
### Adding a New Event Type
1. **Define event constant**:
```typescript
// src/models/constants/general.ts
export enum DomoEvent {
NEW_EVENT = 'newEvent'
}
```
2. **Initialize listener array**:
```typescript
// src/domo.ts
static listeners = {
[DomoEvent.NEW_EVENT]: []
};
```
3. **Create service functions**:
```typescript
// src/models/services/newevent.ts
export function onNewEvent(callback: (data: any) => void) {
this.listeners[DomoEvent.NEW_EVENT].push(callback);
return () => {
const index = this.listeners[DomoEvent.NEW_EVENT].indexOf(callback);
if (index > -1) {
this.listeners[DomoEvent.NEW_EVENT].splice(index, 1);
}
};
}
export function handleNewEvent(data: any) {
this.listeners[DomoEvent.NEW_EVENT].forEach(cb => cb(data));
}
```
4. **Add message handler**:
```typescript
// src/domo.ts, in connect() method
port1.onmessage = (event) => {
if (event.data.event === DomoEvent.NEW_EVENT) {
this.handleNewEvent(event.data.data);
}
};
```
5. **Bind to Domo class**:
```typescript
// src/domo.ts
static onNewEvent = onNewEvent.bind(this);
static handleNewEvent = handleNewEvent.bind(this);
```
### Adding a New Interface
1. **Define interface**:
```typescript
// src/models/interfaces/newtype.ts
export interface NewType {
id: string;
value: any;
}
```
2. **Create type guards**:
```typescript
// src/utils/newtype.ts
export function isNewType(obj: any): obj is NewType {
return (
typeof obj === 'object' &&
typeof obj.id === 'string' &&
obj.value !== undefined
);
}
export function guardAgainstInvalidNewType(obj: any): void {
if (!isNewType(obj)) {
throw new TypeError('Invalid NewType object');
}
}
```
3. **Export from index**:
```typescript
// src/index.ts
export { NewType } from './models/interfaces/newtype';
export { isNewType, guardAgainstInvalidNewType } from './utils/newtype';
```
4. **Add tests**:
```typescript
// src/utils/newtype.test.ts
describe('isNewType', () => {
test('validates correct NewType', () => {
expect(isNewType({ id: '123', value: 'test' })).toBe(true);
});
});
```
### Extending the Library (User-facing)
Users can extend the library using `Domo.extend()`:
```typescript
import Domo, { get as originalGet } from 'ryuu.js';
Domo.extend({
get: async (url, options) => {
console.log(`Fetching: ${url}`);
const result = await originalGet.call(Domo, url, options);
console.log(`Received ${result.length} records`);
return result;
}
});
```
**Implementation:**
```typescript
// src/domo.ts
static extend(overrides: Partial<typeof Domo>): void {
Object.assign(this, overrides);
}
```
This allows users to add custom logging, retry logic, caching, etc. without forking the library.
### Debugging Tips
1. **Check requests map**: `console.log(Domo.getRequests())` - See all tracked requests with status
2. **Monitor messages**: Add logging to `port1.onmessage` - Debug incoming messages
3. **Verify token**: `console.log(window.__RYUU_SID__)` - Check if auth token is present
4. **Check connection**: `console.log(Domo.connected)` - Verify MessageChannel initialized
5. **Inspect environment**: `console.log(Domo.env)` - View query parameters
6. **Test origin verification**: `console.log(Domo.__util.isVerifiedOrigin(window.location.origin))` - Security check
7. **Check mobile detection**: `console.log(isIOS(), isMobile())` - Verify platform detection
8. **Inspect specific request**: `console.log(Domo.getRequest(requestId))` - Debug single request
## Summary
This comprehensive guide covers the complete architecture and implementation of ryuu.js/domo.js. Key takeaways:
1. **Static Class Design**: All APIs exposed through static Domo class
2. **MessageChannel Communication**: Reliable bidirectional communication with parent
3. **Mobile Platform Support**: Cross-platform iOS/Android integration
4. **Type Safety**: Full TypeScript support with overloaded functions
5. **Event System**: Observer pattern for real-time updates
6. **Request Tracking**: Built-in acknowledgment system for reliability
7. **Extensibility**: Users can override methods via `extend()`
8. **Zero Dependencies**: Minimal bundle size, no runtime dependencies
When working with this codebase:
- Follow established naming conventions
- Add tests for all new functionality
- Use type guards for runtime validation
- Maintain backward compatibility for deprecated methods
- Document public APIs thoroughly
- Keep services bound to Domo class context
For questions or contributions, refer to the [README.md](README.md) for user-facing documentation and examples.