@yoyo-org/progressive-json
Version:
Stream and render JSON data as it arrives - perfect for AI responses, large datasets, and real-time updates
104 lines (103 loc) • 3.58 kB
JavaScript
/**
* Batch processor for efficient React updates
* Prevents excessive re-renders by intelligently batching stream chunks
*/
export class BatchProcessor {
constructor(onBatchReady, options = {}) {
var _a, _b, _c, _d;
this.onBatchReady = onBatchReady;
this.pendingChunks = [];
this.batchTimer = null;
this.lastUpdateTime = 0;
this.options = {
maxBatchTime: (_a = options.maxBatchTime) !== null && _a !== void 0 ? _a : 16, // ~1 frame at 60fps
maxBatchSize: (_b = options.maxBatchSize) !== null && _b !== void 0 ? _b : 10,
minUpdateInterval: (_c = options.minUpdateInterval) !== null && _c !== void 0 ? _c : 8, // ~120fps max
smartBatching: (_d = options.smartBatching) !== null && _d !== void 0 ? _d : true
};
}
addChunk(chunk) {
this.pendingChunks.push(chunk);
const now = Date.now();
const timeSinceLastUpdate = now - this.lastUpdateTime;
// Check if we should flush immediately
const shouldFlushNow = this.pendingChunks.length >= this.options.maxBatchSize ||
(timeSinceLastUpdate >= this.options.minUpdateInterval && this.isAtBoundary());
if (shouldFlushNow) {
this.flush();
}
else if (!this.batchTimer) {
// Schedule a flush
const timeToWait = Math.max(this.options.maxBatchTime, this.options.minUpdateInterval - timeSinceLastUpdate);
this.batchTimer = setTimeout(() => this.flush(), timeToWait);
}
}
isAtBoundary() {
if (!this.options.smartBatching)
return true;
// Check if we're at a meaningful JSON boundary
const combined = this.pendingChunks.join('');
// Simple heuristics for JSON boundaries
const boundaries = [
'},', // End of object
'],', // End of array
'",', // End of string value
'true,', // End of boolean
'false,', // End of boolean
/\d+,/, // End of number
'}\n', // End of line with object
']\n', // End of line with array
];
return boundaries.some(boundary => {
if (typeof boundary === 'string') {
return combined.endsWith(boundary);
}
return boundary.test(combined);
});
}
flush() {
if (this.pendingChunks.length === 0)
return;
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
const chunks = this.pendingChunks;
this.pendingChunks = [];
this.lastUpdateTime = Date.now();
this.onBatchReady(chunks);
}
destroy() {
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
this.flush(); // Flush any remaining chunks
}
}
/**
* React-friendly update scheduler using requestAnimationFrame
*/
export class UpdateScheduler {
constructor(callback) {
this.callback = callback;
this.scheduled = false;
this.rafId = null;
}
schedule() {
if (this.scheduled)
return;
this.scheduled = true;
this.rafId = requestAnimationFrame(() => {
this.scheduled = false;
this.callback();
});
}
cancel() {
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.scheduled = false;
}
}