719 lines
26 KiB
JavaScript
719 lines
26 KiB
JavaScript
/**
|
|
* NATSBridge - Cross-Platform Bi-Directional Data Bridge
|
|
* JavaScript Implementation (Node.js and Browser)
|
|
*
|
|
* This module provides functionality for sending and receiving data across network boundaries
|
|
* using NATS as the message bus, with support for both direct payload transport and
|
|
* URL-based transport for larger payloads.
|
|
*
|
|
* File Server Handler Architecture:
|
|
* The system uses handler functions to abstract file server operations, allowing support
|
|
* for different file server implementations (e.g., Plik, AWS S3, custom HTTP server).
|
|
*
|
|
* Handler Function Signatures:
|
|
*
|
|
* ```javascript
|
|
* // Upload handler - uploads data to file server and returns URL
|
|
* // The handler is passed to smartsend as fileserver_upload_handler parameter
|
|
* // It receives: (fileserver_url, dataname, data)
|
|
* // Returns: Promise<{ status, uploadid, fileid, url }>
|
|
* fileserver_upload_handler(fileserver_url, dataname, data)
|
|
*
|
|
* // Download handler - fetches data from file server URL with exponential backoff
|
|
* // The handler is passed to smartreceive as fileserver_download_handler parameter
|
|
* // It receives: (url, max_retries, base_delay, max_delay, correlation_id)
|
|
* // Returns: Promise<Uint8Array>
|
|
* fileserver_download_handler(url, max_retries, base_delay, max_delay, correlation_id)
|
|
* ```
|
|
*
|
|
* Multi-Payload Support (Standard API):
|
|
* The system uses a standardized list-of-tuples format for all payload operations.
|
|
* Even when sending a single payload, the user must wrap it in a list.
|
|
*
|
|
* API Standard:
|
|
*
|
|
* ```javascript
|
|
* // Input format for smartsend (always a list of tuples with type info)
|
|
* [[dataname1, data1, type1], [dataname2, data2, type2], ...]
|
|
*
|
|
* // Output format for smartreceive (returns a dictionary with payloads field containing list of tuples)
|
|
* {
|
|
* "correlation_id": "...",
|
|
* "msg_id": "...",
|
|
* "timestamp": "...",
|
|
* "send_to": "...",
|
|
* "msg_purpose": "...",
|
|
* "sender_name": "...",
|
|
* "sender_id": "...",
|
|
* "receiver_name": "...",
|
|
* "receiver_id": "...",
|
|
* "reply_to": "...",
|
|
* "reply_to_msg_id": "...",
|
|
* "broker_url": "...",
|
|
* "metadata": {...},
|
|
* "payloads": [[dataname1, data1, type1], [dataname2, data2, type2], ...]
|
|
* }
|
|
* ```
|
|
*
|
|
* Supported types: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
|
*/
|
|
|
|
const nats = typeof require !== 'undefined' ? require('nats') : null;
|
|
const { v4: uuidv4 } = typeof require !== 'undefined' ? require('uuid') : null;
|
|
const fetch = typeof require !== 'undefined' ? require('node-fetch') : (typeof globalThis !== 'undefined' ? globalThis.fetch : undefined);
|
|
const arrow = typeof require !== 'undefined' ? require('apache-arrow') : null;
|
|
|
|
/**
|
|
* Default configuration values
|
|
*/
|
|
const DEFAULT_SIZE_THRESHOLD = 1_000_000; // 1MB - threshold for switching from direct to link transport
|
|
const DEFAULT_BROKER_URL = 'nats://localhost:4222'; // Default NATS server URL
|
|
const DEFAULT_FILESERVER_URL = 'http://localhost:8080'; // Default HTTP file server URL
|
|
|
|
/**
|
|
* Generate a UUID v4
|
|
* @returns {string} UUID string
|
|
*/
|
|
function generateUUID() {
|
|
if (uuidv4) {
|
|
return uuidv4();
|
|
}
|
|
// Fallback UUID generation for environments without uuid package
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log a trace message with correlation ID and timestamp
|
|
* @param {string} correlation_id - Correlation ID for tracing
|
|
* @param {string} message - Message content to log
|
|
*/
|
|
function logTrace(correlation_id, message) {
|
|
const timestamp = new Date().toISOString();
|
|
console.log(`[${timestamp}] [Correlation: ${correlation_id}] ${message}`);
|
|
}
|
|
|
|
/**
|
|
* Serialize data according to specified format
|
|
* @param {any} data - Data to serialize
|
|
* @param {string} payload_type - Target format: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
|
* @returns {Promise<Uint8Array>} Binary representation of the serialized data
|
|
*/
|
|
async function serializeData(data, payload_type) {
|
|
if (payload_type === 'text') {
|
|
if (typeof data === 'string') {
|
|
return new TextEncoder().encode(data);
|
|
} else {
|
|
throw new Error('Text data must be a string');
|
|
}
|
|
} else if (payload_type === 'dictionary') {
|
|
const jsonStr = JSON.stringify(data);
|
|
return new TextEncoder().encode(jsonStr);
|
|
} else if (payload_type === 'table') {
|
|
// Use Apache Arrow for table serialization
|
|
if (!arrow) {
|
|
throw new Error('apache-arrow not available. Install with: npm install apache-arrow');
|
|
}
|
|
|
|
// Convert array of objects to Arrow Table
|
|
if (!Array.isArray(data)) {
|
|
throw new Error('Table data must be an array of objects');
|
|
}
|
|
|
|
// Build schema from first row if not provided
|
|
const schemaFields = [];
|
|
if (data.length > 0) {
|
|
for (const key in data[0]) {
|
|
const value = data[0][key];
|
|
let arrowType;
|
|
if (typeof value === 'number') {
|
|
arrowType = arrow.float64;
|
|
} else if (typeof value === 'boolean') {
|
|
arrowType = arrow.bool;
|
|
} else if (value instanceof Date) {
|
|
arrowType = arrow.string; // Date as string
|
|
} else {
|
|
arrowType = arrow.string;
|
|
}
|
|
schemaFields.push(new arrow.Field(key, arrowType));
|
|
}
|
|
}
|
|
|
|
const schema = new arrow.Schema(schemaFields);
|
|
|
|
// Convert data to Arrow RecordBatch
|
|
const arrays = {};
|
|
for (const field of schema.fields) {
|
|
const name = field.name;
|
|
const type = field.type;
|
|
|
|
if (type instanceof arrow.Float64) {
|
|
arrays[name] = new arrow.Float64Array(data.length);
|
|
for (let i = 0; i < data.length; i++) {
|
|
arrays[name][i] = data[i][name] || 0;
|
|
}
|
|
} else if (type instanceof arrow.Boolean) {
|
|
arrays[name] = new arrow.BooleanArray(data.length);
|
|
for (let i = 0; i < data.length; i++) {
|
|
arrays[name][i] = data[i][name] || false;
|
|
}
|
|
} else {
|
|
// String type
|
|
const values = data.map(row => String(row[name] ?? ''));
|
|
const offsets = new Int32Array(values.length + 1);
|
|
let offset = 0;
|
|
for (let i = 0; i < values.length; i++) {
|
|
offsets[i + 1] = offset += values[i].length;
|
|
}
|
|
const buffer = new Uint8Array(offsets[values.length]);
|
|
for (let i = 0; i < values.length; i++) {
|
|
const encoder = new TextEncoder();
|
|
const bytes = encoder.encode(values[i]);
|
|
buffer.set(bytes, offsets[i]);
|
|
}
|
|
arrays[name] = new arrow.StringArray(
|
|
new arrow.DataView(new arrow.Buffer(buffer), 0, offsets[values.length]),
|
|
new arrow.Buffer(offsets.buffer, 0, offsets.length * 4),
|
|
0,
|
|
data.length
|
|
);
|
|
}
|
|
}
|
|
|
|
const recordBatch = arrow.RecordBatch.fromArrays(schema, arrays, data.length);
|
|
|
|
// Write to IPC format
|
|
const buffer = arrow.tableFromBatches([recordBatch]).toBuffer();
|
|
return new Uint8Array(buffer);
|
|
} else if (payload_type === 'image') {
|
|
if (data instanceof Uint8Array || data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
|
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
} else {
|
|
throw new Error('Image data must be Uint8Array or ArrayBuffer');
|
|
}
|
|
} else if (payload_type === 'audio') {
|
|
if (data instanceof Uint8Array || data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
|
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
} else {
|
|
throw new Error('Audio data must be Uint8Array or ArrayBuffer');
|
|
}
|
|
} else if (payload_type === 'video') {
|
|
if (data instanceof Uint8Array || data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
|
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
} else {
|
|
throw new Error('Video data must be Uint8Array or ArrayBuffer');
|
|
}
|
|
} else if (payload_type === 'binary') {
|
|
if (data instanceof Uint8Array || data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
|
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
} else {
|
|
throw new Error('Binary data must be Uint8Array or ArrayBuffer');
|
|
}
|
|
} else {
|
|
throw new Error(`Unknown payload_type: ${payload_type}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deserialize bytes to data based on type
|
|
* @param {Uint8Array} data - Serialized data as bytes
|
|
* @param {string} payload_type - Data type
|
|
* @param {string} correlation_id - Correlation ID for logging
|
|
* @returns {Promise<any>} Deserialized data
|
|
*/
|
|
async function deserializeData(data, payload_type, correlation_id) {
|
|
if (payload_type === 'text') {
|
|
return new TextDecoder().decode(data);
|
|
} else if (payload_type === 'dictionary') {
|
|
const jsonStr = new TextDecoder().decode(data);
|
|
return JSON.parse(jsonStr);
|
|
} else if (payload_type === 'table') {
|
|
// Use Apache Arrow for table deserialization
|
|
if (!arrow) {
|
|
throw new Error('apache-arrow not available. Install with: npm install apache-arrow');
|
|
}
|
|
|
|
// Read Arrow IPC format
|
|
const buffer = arrow.Buffer.wrap(data.buffer, data.byteOffset, data.byteLength);
|
|
const table = arrow.tableFromRawBytes(buffer);
|
|
|
|
// Convert to array of objects for consistency with the API
|
|
const result = [];
|
|
const numRows = table.numRows;
|
|
|
|
for (let i = 0; i < numRows; i++) {
|
|
const row = {};
|
|
for (const colName of table.columnNames) {
|
|
const column = table.getColumn(colName);
|
|
row[colName] = column.get(i);
|
|
}
|
|
result.push(row);
|
|
}
|
|
|
|
return result;
|
|
} else if (payload_type === 'image') {
|
|
return data;
|
|
} else if (payload_type === 'audio') {
|
|
return data;
|
|
} else if (payload_type === 'video') {
|
|
return data;
|
|
} else if (payload_type === 'binary') {
|
|
return data;
|
|
} else {
|
|
throw new Error(`Unknown payload_type: ${payload_type}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch data from URL with exponential backoff
|
|
* @param {string} url - URL to fetch from
|
|
* @param {number} max_retries - Maximum retry attempts
|
|
* @param {number} base_delay - Initial delay in milliseconds
|
|
* @param {number} max_delay - Maximum delay in milliseconds
|
|
* @param {string} correlation_id - Correlation ID for logging
|
|
* @returns {Promise<Uint8Array>} Fetched data as bytes
|
|
*/
|
|
async function fetchWithBackoff(url, max_retries, base_delay, max_delay, correlation_id) {
|
|
let delay = base_delay;
|
|
|
|
for (let attempt = 1; attempt <= max_retries; attempt++) {
|
|
try {
|
|
const response = await fetch(url);
|
|
|
|
if (response.status === 200) {
|
|
logTrace(correlation_id, `Successfully fetched data from ${url} on attempt ${attempt}`);
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
return new Uint8Array(arrayBuffer);
|
|
} else {
|
|
throw new Error(`Failed to fetch: ${response.status}`);
|
|
}
|
|
} catch (e) {
|
|
logTrace(correlation_id, `Attempt ${attempt} failed: ${e.constructor.name}`);
|
|
|
|
if (attempt < max_retries) {
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
delay = Math.min(delay * 2, max_delay);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(`Failed to fetch data after ${max_retries} attempts`);
|
|
}
|
|
|
|
/**
|
|
* Upload a single file to a plik server using one-shot mode
|
|
* @param {string} file_server_url - Base URL of the plik server
|
|
* @param {string} dataname - Name of the file being uploaded
|
|
* @param {Uint8Array} data - Raw byte data of the file content
|
|
* @returns {Promise<{ status, uploadid, fileid, url }>} Upload result
|
|
*/
|
|
async function plikOneshotUpload(file_server_url, dataname, data) {
|
|
// Get upload id
|
|
const url_getUploadID = `${file_server_url}/upload`;
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
const body = JSON.stringify({ OneShot: true });
|
|
|
|
const http_response = await fetch(url_getUploadID, {
|
|
method: 'POST',
|
|
headers,
|
|
body
|
|
});
|
|
|
|
const response_json = await http_response.json();
|
|
const uploadid = response_json.id;
|
|
const uploadtoken = response_json.uploadToken;
|
|
|
|
// Upload file
|
|
const url_upload = `${file_server_url}/file/${uploadid}`;
|
|
const form = new FormData();
|
|
const blob = new Blob([data]);
|
|
form.append('file', blob, dataname);
|
|
|
|
const upload_headers = {
|
|
'X-UploadToken': uploadtoken
|
|
};
|
|
|
|
const upload_response = await fetch(url_upload, {
|
|
method: 'POST',
|
|
headers: upload_headers,
|
|
body: form
|
|
});
|
|
|
|
const upload_json = await upload_response.json();
|
|
const fileid = upload_json.id;
|
|
|
|
const url = `${file_server_url}/file/${uploadid}/${fileid}/${dataname}`;
|
|
|
|
return {
|
|
status: upload_response.status,
|
|
uploadid,
|
|
fileid,
|
|
url
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Publish message to NATS
|
|
* @param {string|object} broker_url_or_conn - NATS server URL or pre-existing connection
|
|
* @param {string} subject - NATS subject to publish to
|
|
* @param {string} message - JSON message to publish
|
|
* @param {string} correlation_id - Correlation ID for tracing and logging
|
|
*/
|
|
async function publishMessage(broker_url_or_conn, subject, message, correlation_id) {
|
|
if (broker_url_or_conn instanceof Object && broker_url_or_conn.publish) {
|
|
// Pre-existing connection
|
|
try {
|
|
await broker_url_or_conn.publish(subject, message);
|
|
logTrace(correlation_id, `Message published to ${subject}`);
|
|
} finally {
|
|
// Note: In a real implementation, you might want to drain/close the connection
|
|
}
|
|
} else {
|
|
// URL-based - create new connection
|
|
if (!nats) {
|
|
throw new Error('nats package not available. Install with: npm install nats');
|
|
}
|
|
|
|
const conn = await nats.connect(broker_url_or_conn);
|
|
try {
|
|
await conn.publish(subject, message);
|
|
logTrace(correlation_id, `Message published to ${subject}`);
|
|
} finally {
|
|
conn.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build message envelope from payloads and metadata
|
|
* @param {string} subject - NATS subject
|
|
* @param {Array} payloads - Array of payload objects
|
|
* @param {Object} options - Envelope options
|
|
* @returns {Object} Message envelope
|
|
*/
|
|
function buildEnvelope(subject, payloads, options) {
|
|
return {
|
|
correlation_id: options.correlation_id || generateUUID(),
|
|
msg_id: options.msg_id || generateUUID(),
|
|
timestamp: new Date().toISOString(),
|
|
send_to: subject,
|
|
msg_purpose: options.msg_purpose || '',
|
|
sender_name: options.sender_name || '',
|
|
sender_id: options.sender_id || generateUUID(),
|
|
receiver_name: options.receiver_name || '',
|
|
receiver_id: options.receiver_id || '',
|
|
reply_to: options.reply_to || '',
|
|
reply_to_msg_id: options.reply_to_msg_id || '',
|
|
broker_url: options.broker_url || DEFAULT_BROKER_URL,
|
|
metadata: options.metadata || {},
|
|
payloads
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert data to base64 string
|
|
* @param {Uint8Array} buffer - Data buffer
|
|
* @returns {string} Base64 encoded string
|
|
*/
|
|
function bufferToBase64(buffer) {
|
|
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(buffer)) {
|
|
return buffer.toString('base64');
|
|
}
|
|
|
|
// For browser/Node Uint8Array
|
|
let binary = '';
|
|
const bytes = new Uint8Array(buffer);
|
|
const len = bytes.byteLength;
|
|
for (let i = 0; i < len; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
/**
|
|
* Convert base64 string to Uint8Array
|
|
* @param {string} base64 - Base64 encoded string
|
|
* @returns {Uint8Array} Decoded bytes
|
|
*/
|
|
function base64ToBuffer(base64) {
|
|
const binary = atob(base64);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) {
|
|
bytes[i] = binary.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* smartsend - Send data either directly via NATS or via a fileserver URL, depending on payload size
|
|
*
|
|
* This function intelligently routes data delivery based on payload size relative to a threshold.
|
|
* If the serialized payload is smaller than size_threshold, it encodes the data as Base64 and publishes directly over NATS.
|
|
* Otherwise, it uploads the data to a fileserver and publishes only the download URL over NATS.
|
|
*
|
|
* @param {string} subject - NATS subject to publish the message to
|
|
* @param {Array} data - Array of [dataname, data, type] tuples to send
|
|
* @param {Object} options - Optional configuration
|
|
* @param {string} [options.broker_url=DEFAULT_BROKER_URL] - URL of the NATS server
|
|
* @param {string} [options.fileserver_url=DEFAULT_FILESERVER_URL] - URL of the HTTP file server for large payloads
|
|
* @param {Function} [options.fileserver_upload_handler=plikOneshotUpload] - Function to handle fileserver uploads
|
|
* @param {number} [options.size_threshold=DEFAULT_SIZE_THRESHOLD] - Threshold in bytes separating direct vs link transport
|
|
* @param {string} [options.correlation_id] - Correlation ID for tracing (auto-generated if not provided)
|
|
* @param {string} [options.msg_purpose="chat"] - Purpose of the message
|
|
* @param {string} [options.sender_name="NATSBridge"] - Name of the sender
|
|
* @param {string} [options.receiver_name=""] - Name of the receiver
|
|
* @param {string} [options.receiver_id=""] - UUID of the receiver
|
|
* @param {string} [options.reply_to=""] - Topic to reply to
|
|
* @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying to
|
|
* @param {boolean} [options.is_publish=true] - Whether to automatically publish the message to NATS
|
|
* @param {object} [options.nats_connection] - Pre-existing NATS connection
|
|
* @param {string} [options.msg_id] - Message ID (auto-generated if not provided)
|
|
* @param {string} [options.sender_id] - Sender ID (auto-generated if not provided)
|
|
* @returns {Promise<[Object, string]>} Promise resolving to [envelope, env_json_str]
|
|
*/
|
|
async function smartsend(subject, data, options = {}) {
|
|
const {
|
|
broker_url = DEFAULT_BROKER_URL,
|
|
fileserver_url = DEFAULT_FILESERVER_URL,
|
|
fileserver_upload_handler = plikOneshotUpload,
|
|
size_threshold = DEFAULT_SIZE_THRESHOLD,
|
|
correlation_id = generateUUID(),
|
|
msg_purpose = 'chat',
|
|
sender_name = 'NATSBridge',
|
|
receiver_name = '',
|
|
receiver_id = '',
|
|
reply_to = '',
|
|
reply_to_msg_id = '',
|
|
is_publish = true,
|
|
nats_connection = null,
|
|
msg_id = generateUUID(),
|
|
sender_id = generateUUID()
|
|
} = options;
|
|
|
|
logTrace(correlation_id, `Starting smartsend for subject: ${subject}`);
|
|
|
|
// Process each payload in the list
|
|
const payloads = [];
|
|
for (const [dataname, payloadData, payloadType] of data) {
|
|
const payloadBytes = await serializeData(payloadData, payloadType);
|
|
const payloadSize = payloadBytes.byteLength;
|
|
|
|
logTrace(correlation_id, `Serialized payload '${dataname}' (payload_type: ${payloadType}) size: ${payloadSize} bytes`);
|
|
|
|
// Decision: Direct vs Link
|
|
if (payloadSize < size_threshold) {
|
|
// Direct path - Base64 encode and send via NATS
|
|
const payloadB64 = bufferToBase64(payloadBytes);
|
|
logTrace(correlation_id, `Using direct transport for ${payloadSize} bytes`);
|
|
|
|
payloads.push({
|
|
id: generateUUID(),
|
|
dataname,
|
|
payload_type: payloadType,
|
|
transport: 'direct',
|
|
encoding: 'base64',
|
|
size: payloadSize,
|
|
data: payloadB64,
|
|
metadata: { payload_bytes: payloadSize }
|
|
});
|
|
} else {
|
|
// Link path - Upload to HTTP server, send URL via NATS
|
|
logTrace(correlation_id, 'Using link transport, uploading to fileserver');
|
|
|
|
const response = await fileserver_upload_handler(fileserver_url, dataname, payloadBytes);
|
|
|
|
if (response.status !== 200) {
|
|
throw new Error(`Failed to upload data to fileserver: ${response.status}`);
|
|
}
|
|
|
|
logTrace(correlation_id, `Uploaded to URL: ${response.url}`);
|
|
|
|
payloads.push({
|
|
id: generateUUID(),
|
|
dataname,
|
|
payload_type: payloadType,
|
|
transport: 'link',
|
|
encoding: 'none',
|
|
size: payloadSize,
|
|
data: response.url,
|
|
metadata: {}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Build envelope
|
|
const env = buildEnvelope(subject, payloads, {
|
|
correlation_id,
|
|
msg_id,
|
|
msg_purpose,
|
|
sender_name,
|
|
sender_id,
|
|
receiver_name,
|
|
receiver_id,
|
|
reply_to,
|
|
reply_to_msg_id,
|
|
broker_url
|
|
});
|
|
|
|
const env_json_str = JSON.stringify(env);
|
|
|
|
if (is_publish) {
|
|
if (nats_connection) {
|
|
await publishMessage(nats_connection, subject, env_json_str, correlation_id);
|
|
} else {
|
|
await publishMessage(broker_url, subject, env_json_str, correlation_id);
|
|
}
|
|
}
|
|
|
|
return [env, env_json_str];
|
|
}
|
|
|
|
/**
|
|
* smartreceive - Receive and process messages from NATS
|
|
*
|
|
* This function processes incoming NATS messages, handling both direct transport
|
|
* (base64 decoded payloads) and link transport (URL-based payloads).
|
|
* It deserializes the data based on the transport type and returns the result.
|
|
*
|
|
* @param {Object} msg - NATS message object
|
|
* @param {Object} options - Optional configuration
|
|
* @param {Function} [options.fileserver_download_handler=fetchWithBackoff] - Function to handle downloading data from file server URLs
|
|
* @param {number} [options.max_retries=5] - Maximum retry attempts for fetching URL
|
|
* @param {number} [options.base_delay=100] - Initial delay for exponential backoff in ms
|
|
* @param {number} [options.max_delay=5000] - Maximum delay for exponential backoff in ms
|
|
* @returns {Promise<Object>} Promise resolving to envelope object with deserialized payloads
|
|
*/
|
|
async function smartreceive(msg, options = {}) {
|
|
const {
|
|
fileserver_download_handler = fetchWithBackoff,
|
|
max_retries = 5,
|
|
base_delay = 100,
|
|
max_delay = 5000
|
|
} = options;
|
|
|
|
// Parse the JSON envelope
|
|
const payload = typeof msg.payload === 'string' ? msg.payload : new TextDecoder().decode(msg.payload);
|
|
const env_json_obj = JSON.parse(payload);
|
|
|
|
logTrace(env_json_obj.correlation_id, 'Processing received message');
|
|
|
|
// Process all payloads in the envelope
|
|
const payloads_list = [];
|
|
|
|
for (const payload of env_json_obj.payloads) {
|
|
const transport = payload.transport;
|
|
const dataname = payload.dataname;
|
|
|
|
if (transport === 'direct') {
|
|
logTrace(env_json_obj.correlation_id, `Direct transport - decoding payload '${dataname}'`);
|
|
|
|
// Extract base64 payload from the payload
|
|
const payload_b64 = payload.data;
|
|
|
|
// Decode Base64 payload
|
|
const payload_bytes = base64ToBuffer(payload_b64);
|
|
|
|
// Deserialize based on type
|
|
const data_type = payload.payload_type;
|
|
const data = await deserializeData(payload_bytes, data_type, env_json_obj.correlation_id);
|
|
|
|
payloads_list.push([dataname, data, data_type]);
|
|
} else if (transport === 'link') {
|
|
// Extract download URL from the payload
|
|
const url = payload.data;
|
|
logTrace(env_json_obj.correlation_id, `Link transport - fetching '${dataname}' from URL: ${url}`);
|
|
|
|
// Fetch with exponential backoff using the download handler
|
|
const downloaded_data = await fileserver_download_handler(
|
|
url,
|
|
max_retries,
|
|
base_delay,
|
|
max_delay,
|
|
env_json_obj.correlation_id
|
|
);
|
|
|
|
// Deserialize based on type
|
|
const data_type = payload.payload_type;
|
|
const data = await deserializeData(downloaded_data, data_type, env_json_obj.correlation_id);
|
|
|
|
payloads_list.push([dataname, data, data_type]);
|
|
} else {
|
|
throw new Error(`Unknown transport type for payload '${dataname}': ${transport}`);
|
|
}
|
|
}
|
|
|
|
env_json_obj.payloads = payloads_list;
|
|
return env_json_obj;
|
|
}
|
|
|
|
/**
|
|
* NATS Client wrapper for managing connections
|
|
*/
|
|
class NATSClient {
|
|
constructor(url) {
|
|
this.url = url;
|
|
this.connection = null;
|
|
}
|
|
|
|
async connect() {
|
|
if (!nats) {
|
|
throw new Error('nats package not available. Install with: npm install nats');
|
|
}
|
|
this.connection = await nats.connect({ servers: this.url });
|
|
return this.connection;
|
|
}
|
|
|
|
async publish(subject, message) {
|
|
if (!this.connection) {
|
|
await this.connect();
|
|
}
|
|
await this.connection.publish(subject, message);
|
|
}
|
|
|
|
async close() {
|
|
if (this.connection) {
|
|
this.connection.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export for Node.js
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
NATSClient,
|
|
smartsend,
|
|
smartreceive,
|
|
plikOneshotUpload,
|
|
fetchWithBackoff,
|
|
serializeData,
|
|
deserializeData,
|
|
publishMessage,
|
|
logTrace,
|
|
generateUUID,
|
|
DEFAULT_SIZE_THRESHOLD,
|
|
DEFAULT_BROKER_URL,
|
|
DEFAULT_FILESERVER_URL
|
|
};
|
|
}
|
|
|
|
// Export for browser (global scope)
|
|
if (typeof window !== 'undefined') {
|
|
window.NATSBridge = {
|
|
smartsend,
|
|
smartreceive,
|
|
plikOneshotUpload,
|
|
fetchWithBackoff,
|
|
serializeData,
|
|
deserializeData,
|
|
publishMessage,
|
|
logTrace,
|
|
generateUUID,
|
|
NATSClient,
|
|
DEFAULT_SIZE_THRESHOLD,
|
|
DEFAULT_BROKER_URL,
|
|
DEFAULT_FILESERVER_URL
|
|
};
|
|
} |