Skip to content

Commit

Permalink
Added simple caching and sending data in interval mechanism, touch #456.
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulDalek committed Jun 18, 2024
1 parent f1262ec commit e044382
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 49 deletions.
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,6 @@ WEB_SOCKET_REJECT_UNAUTHORIZED = false
WEB_SOCKET_PING_TIMEOUT = 16000
WEB_SOCKET_RECONNECT_INTERVAL = 3000
WEB_SOCKET_RECONNECT_ATTEMPTS = 3
WEB_SOCKET_MESSAGE_INTERVAL = 3600000
WEB_SOCKET_URL =
WEB_SOCKET_SECRET =
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ These variables are set in your environment and take precedence over options fro
- `WEB_SOCKET_PING_TIMEOUT`: The timeout, in milliseconds, for the heartbeat mechanism between the client and server (defaults to `16000`).
- `WEB_SOCKET_RECONNECT_INTERVAL`: The interval, in milliseconds, for the reconnect attempt (defaults to `3000`).
- `WEB_SOCKET_RECONNECT_ATTEMPTS`: The number of reconnect attempts before returning a connection error (defaults to `3`).
- `WEB_SOCKET_MESSAGE_INTERVAL`: The interval, in milliseconds, for auto sending the data through a WebSocket connection (defaults to `3600000`).
- `WEB_SOCKET_URL`: The URL of the WebSocket server (defaults to ``).
- `WEB_SOCKET_SECRET`: The secret used to create a JSON Web Token sent to the WebSocket server (defaults to ``).

Expand Down Expand Up @@ -465,6 +466,7 @@ _Available options:_
- `--wsPingTimeout`: The timeout, in milliseconds, for the heartbeat mechanism between the client and server (defaults to `16000`).
- `--wsReconnectInterval`: The interval, in milliseconds, for the reconnect attempt (defaults to `3000`).
- `--wsReconnectAttempts`: The number of reconnect attempts before returning a connection error (defaults to `3`).
- `--wsMessageInterval`: The interval, in milliseconds, for auto sending the data through a WebSocket connection (defaults to `3600000`).
- `--wsUrl`: The URL of the WebSocket server (defaults to ``).
- `--wsSecret`: The secret used to create a JSON Web Token sent to the WebSocket server (defaults to ``).

Expand Down
4 changes: 2 additions & 2 deletions dist/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.esm.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.esm.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/envs.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export const Config = z.object({
WEB_SOCKET_PING_TIMEOUT: v.nonNegativeNum(),
WEB_SOCKET_RECONNECT_INTERVAL: v.nonNegativeNum(),
WEB_SOCKET_RECONNECT_ATTEMPTS: v.nonNegativeNum(),
WEB_SOCKET_MESSAGE_INTERVAL: v.nonNegativeNum(),
WEB_SOCKET_URL: v.string(),
WEB_SOCKET_SECRET: v.string()
});
Expand Down
14 changes: 14 additions & 0 deletions lib/schemas/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,14 @@ export const defaultConfig = {
description:
'The number of reconnect attempts before returning a connection error.'
},
messageInterval: {
value: 3,
type: 'number',
envLink: 'WEB_SOCKET_MESSAGE_INTERVAL',
cliName: 'wsMessageInterval',
description:
'The interval, in milliseconds, for auto sending the data through a WebSocket connection.'
},
url: {
value: false,
type: 'string',
Expand Down Expand Up @@ -1198,6 +1206,12 @@ export const promptsConfig = {
message: 'The number of reconnect attempts',
initial: defaultConfig.webSocket.reconnectAttempts.value
},
{
type: 'number',
name: 'messageInterval',
message: 'Interval for auto sending the data',
initial: defaultConfig.webSocket.messageInterval.value
},
{
type: 'text',
name: 'url',
Expand Down
22 changes: 22 additions & 0 deletions lib/schemas/telemetry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"boost": null,
"chart": {
"type": null,
"options3d": {
"enabled": null
}
},
"colors": null,
"legend": {
"enabled": null
},
"series": {
"type": null
},
"xAxis": {
"type": null
},
"yAxis": {
"type": null
}
}
16 changes: 4 additions & 12 deletions lib/server/routes/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ See LICENSE file in root for details.
*******************************************************************************/

import { v4 as uuid } from 'uuid';
import { WebSocket } from 'ws';

import { getAllowCodeExecution, startExport } from '../../chart.js';
import { getOptions, mergeConfigOptions } from '../../config.js';
import { log } from '../../logger.js';
import { getClients as getWebSocketClient } from '../web_socket.js';
import { prepareTelemetry } from '../../telemetry.js';
import {
fixType,
isCorrectJSON,
Expand Down Expand Up @@ -101,10 +100,6 @@ const exportHandler = async (request, response, next) => {

// Get the current server's general options
const defaultOptions = getOptions();

// Get the first WebSocket client
const webSocketClient = getWebSocketClient().next().value;

const body = request.body;
const id = ++requestsCounter;

Expand Down Expand Up @@ -213,12 +208,6 @@ const exportHandler = async (request, response, next) => {
);
}

// If the client is found, send data through WebSocket
if (webSocketClient && webSocketClient.readyState === WebSocket.OPEN) {
// Already prepared options but before the export process
webSocketClient.send(JSON.stringify(options));
}

// Start the export process
await startExport(options, (error, info) => {
// Remove the close event from the socket
Expand Down Expand Up @@ -259,6 +248,9 @@ const exportHandler = async (request, response, next) => {
// The after request callbacks
doCallbacks(afterRequest, request, response, { id, body: info.result });

// Prepare and send the options through the WebSocket
prepareTelemetry(options.export.options, options.payload.requestId);

if (info.result) {
// If only base64 is required, return it
if (body.b64) {
Expand Down
38 changes: 35 additions & 3 deletions lib/server/web_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ import WebSocket from 'ws';

import { getOptions } from '../config.js';
import { log, logWithStack } from '../logger.js';
import { telemetryData } from '../telemetry.js';
import { addTimer } from '../timers.js';

// WebSocket clients map
const webSocketClients = new Map();

// WebSocket options
let webSocketOptions;

// WebSocket clients map
const webSocketClients = new Map();
// WebSocket message sending interval
let messageInterval = null;

/**
* Init WebSocket client and connection options.
Expand Down Expand Up @@ -63,9 +67,37 @@ export function init(address) {

// Start the WebSocket connection
connect(webSocketOptions.url, connectionOptions, clientOptions);

// Start the WebSocket message sending interval
sendingMessageInterval(webSocketOptions);
}
}

function sendingMessageInterval(webSocketOptions) {
// Set the sending message interval
messageInterval = setInterval(() => {
try {
// Get the first WebSocket client
const webSocketClient = getClients().next().value;
// If the client is found, open and there is data to send
if (
webSocketClient &&
webSocketClient.readyState === WebSocket.OPEN &&
Object.keys(telemetryData).length > 1 &&
telemetryData.numberOfRequests > 0
) {
// Send through the WebSocket
webSocketClient.send(JSON.stringify(telemetryData));
}
} catch (error) {
logWithStack(1, `[websocket] Could not send data through WebSocket.`);
}
}, webSocketOptions.messageInterval);

// Register interval for the later clearing
addTimer(messageInterval);
}

/**
* Creates WebSocket client and connects to WebSocket server on a provided url.
*
Expand Down Expand Up @@ -186,7 +218,7 @@ function reconnect(webSocketUrl, connectionOptions, clientOptions) {
}
}, webSocketOptions.reconnectInterval);

// Register timeout for the later clearing
// Register interval for the later clearing
addTimer(clientOptions.reconnectInterval);
}

Expand Down
93 changes: 93 additions & 0 deletions lib/telemetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*******************************************************************************
Highcharts Export Server
Copyright (c) 2016-2023, Highsoft
Licenced under the MIT licence.
Additionally a valid Highcharts license is required for use.
See LICENSE file in root for details.
*******************************************************************************/

import { readFileSync } from 'fs';
import { join } from 'path';

import { __dirname } from './utils.js';

// Get the telemetry template
const telemetryTemplate = JSON.parse(
readFileSync(join(__dirname, 'lib', 'schemas', 'telemetry.json'))
);

// The object with telemetry data collected
export const telemetryData = {
numberOfRequests: 0
};

// Possible properties in an array
const optionsInArray = ['series', 'xAxis', 'yAxis', 'zAxis'];

// Recursive function for getting only the required options
function filterData(template, options) {
const filteredObject = {};

// Cycle through allowed propeties
for (const [templateKey, templateValue] of Object.entries(template)) {
// Check if the section exists
if (options[templateKey] !== undefined) {
// Check if this is the final level of indent in the template
if (templateValue !== null) {
// Check if it is an array
if (Array.isArray(options[templateKey])) {
// And if it contains allowed properties
if (optionsInArray.includes(templateKey)) {
// Create an array
filteredObject[templateKey] = [];
// If so, cycle through all of them
for (const [index, optionsValue] of options[
templateKey
].entries()) {
filteredObject[templateKey][index] = filterData(
templateValue,
optionsValue
);
}
} else {
// Otherwise, get only the first element
filteredObject[templateKey] = filterData(
templateValue,
options[templateKey][0]
);
}
} else {
filteredObject[templateKey] = filterData(
templateValue,
options[templateKey]
);
}
} else {
// Return the option
filteredObject[templateKey] = options[templateKey];
}
}
}

// Return the object
return filteredObject;
}

export function prepareTelemetry(chartOptions, requestId) {
// Save the filtered options under the request's id
telemetryData[requestId] = filterData(telemetryTemplate, chartOptions);

// Increment requests counter
telemetryData.numberOfRequests++;
}

export default {
telemetryData,
prepareTelemetry
};
Loading

0 comments on commit e044382

Please sign in to comment.