beacon-buffer
Version:
A lightweight, configurable JavaScript library for buffering and sending data using the Beacon API
249 lines (248 loc) • 8.43 kB
JavaScript
/**
* Beacon Buffer - A generic buffer library for sending data using the Beacon API
* Functional API design using TypeScript
*/
const DEFAULT_SEND_INTERVAL = 20000;
const DEFAULT_BUFFER_KEY = 'beaconBuffer';
const DEFAULT_DATA_KEY = 'logs';
const DEFAULT_SEND_TIMEOUT = 30000;
const DEFAULT_MAX_BUFFER_SIZE = 50 * 1024; // 50KB
const CONTENT_TYPE_JSON = 'application/json; charset=UTF-8';
class BeaconBuffer {
constructor(config) {
this.sendIntervalId = null;
this.isRunning = false;
this.isSending = false;
this.sendingData = null;
this.sendTimeoutId = null;
this.validateConfig(config);
this.settings = this.buildSettings(config);
this.initializeEventHandlers();
if (this.settings.autoStart) {
this.start();
}
}
// Configuration and initialization
validateConfig(config) {
if (!config || !config.endpointUrl) {
throw new Error('endpointUrl is required in configuration');
}
}
buildSettings(config) {
return {
endpointUrl: config.endpointUrl,
sendInterval: config.sendInterval || DEFAULT_SEND_INTERVAL,
headers: config.headers || {},
bufferKey: config.bufferKey || DEFAULT_BUFFER_KEY,
dataKey: config.dataKey || DEFAULT_DATA_KEY,
autoStart: config.autoStart || false,
enableSendLock: config.enableSendLock !== false, // Default true
sendTimeout: config.sendTimeout || DEFAULT_SEND_TIMEOUT,
retryOnFailure: config.retryOnFailure || false,
maxBufferSize: config.maxBufferSize || DEFAULT_MAX_BUFFER_SIZE,
enableAutoSend: config.enableAutoSend !== false // Default true
};
}
initializeEventHandlers() {
this.boundSendNow = this.sendNow.bind(this);
this.visibilityHandler = () => {
if (document.visibilityState === 'hidden') {
this.sendNow();
}
};
}
// Storage operations
saveBuffer(data) {
try {
localStorage.setItem(this.settings.bufferKey, JSON.stringify(data));
}
catch (error) {
console.error('Failed to save buffer to localStorage:', error);
}
}
calculateCurrentBufferSize() {
const buffer = this.getBuffer();
if (buffer.length === 0)
return 0;
const dataToSend = this.prepareDataForSending(buffer);
const jsonString = JSON.stringify(dataToSend);
return new Blob([jsonString]).size;
}
// Public buffer operations
addLog(logData) {
if (!logData)
return;
const buffer = this.getBuffer();
buffer.push({ ...logData, timestamp: new Date().toISOString() });
this.saveBuffer(buffer);
// Check buffer size and auto-send if enabled and over threshold
if (this.settings.enableAutoSend && this.isRunning && !this.isSending) {
const currentSize = this.calculateCurrentBufferSize();
if (currentSize >= this.settings.maxBufferSize) {
this.sendNow();
}
}
}
getBuffer() {
try {
const bufferedData = localStorage.getItem(this.settings.bufferKey);
return bufferedData ? JSON.parse(bufferedData) : [];
}
catch (error) {
console.error('Failed to get buffer from localStorage:', error);
return [];
}
}
clearBuffer() {
try {
localStorage.removeItem(this.settings.bufferKey);
}
catch (error) {
console.error('Failed to clear buffer from localStorage:', error);
}
}
// Data sending operations
sendNow() {
// Check if lock is enabled and already sending
if (this.settings.enableSendLock && this.isSending) {
return false;
}
const buffer = this.getBuffer();
if (buffer.length === 0) {
return false;
}
// Acquire lock if enabled
if (this.settings.enableSendLock) {
this.isSending = true;
this.startSendTimeout();
}
try {
// Copy buffer for atomic sending
this.sendingData = [...buffer];
const dataToSend = this.prepareDataForSending(this.sendingData);
const blob = this.createJsonBlob(dataToSend);
const success = navigator.sendBeacon(this.settings.endpointUrl, blob);
if (success) {
// Remove only sent data from buffer
this.removeSentDataFromBuffer();
this.clearSendTimeout();
return true;
}
else {
console.error('Failed to send data with sendBeacon');
// Retry if configured
if (this.settings.retryOnFailure) {
// Release lock temporarily for retry
if (this.settings.enableSendLock) {
this.isSending = false;
this.clearSendTimeout();
}
// Retry once
return this.sendNow();
}
// Data remains in buffer on failure
this.clearSendTimeout();
return false;
}
}
finally {
// Always release lock if enabled
if (this.settings.enableSendLock) {
this.sendingData = null;
this.isSending = false;
this.clearSendTimeout();
}
}
}
prepareDataForSending(buffer) {
return {
...this.settings.headers,
[this.settings.dataKey]: buffer
};
}
createJsonBlob(data) {
return new Blob([JSON.stringify(data)], {
type: CONTENT_TYPE_JSON
});
}
removeSentDataFromBuffer() {
if (!this.sendingData)
return;
const currentBuffer = this.getBuffer();
const sentCount = this.sendingData.length;
// Remove sent items from the beginning of the buffer
// This preserves any new items added during sending
const newBuffer = currentBuffer.slice(sentCount);
if (newBuffer.length > 0) {
this.saveBuffer(newBuffer);
}
else {
this.clearBuffer();
}
}
startSendTimeout() {
if (!this.settings.sendTimeout)
return;
this.sendTimeoutId = setTimeout(() => {
console.error(`Send timeout after ${this.settings.sendTimeout}ms`);
// Force release lock
this.isSending = false;
this.sendingData = null;
this.sendTimeoutId = null;
}, this.settings.sendTimeout);
}
clearSendTimeout() {
if (this.sendTimeoutId) {
clearTimeout(this.sendTimeoutId);
this.sendTimeoutId = null;
}
}
// Lifecycle management
start() {
if (this.isRunning) {
return;
}
this.attachEventListeners();
this.startPeriodicSending();
this.sendNow();
this.isRunning = true;
}
stop() {
if (!this.isRunning) {
return;
}
this.removeEventListeners();
this.stopPeriodicSending();
this.isRunning = false;
}
// Event management
attachEventListeners() {
window.addEventListener('beforeunload', this.boundSendNow);
document.addEventListener('visibilitychange', this.visibilityHandler);
}
removeEventListeners() {
window.removeEventListener('beforeunload', this.boundSendNow);
document.removeEventListener('visibilitychange', this.visibilityHandler);
}
// Periodic sending management
startPeriodicSending() {
if (this.sendIntervalId) {
clearInterval(this.sendIntervalId);
}
this.sendIntervalId = setInterval(() => this.sendNow(), this.settings.sendInterval);
}
stopPeriodicSending() {
if (this.sendIntervalId) {
clearInterval(this.sendIntervalId);
this.sendIntervalId = null;
}
}
// Configuration access
getConfig() {
return { ...this.settings };
}
isStarted() {
return this.isRunning;
}
}
export default BeaconBuffer;