Fix formations stability and socket reliability issues
formation_overlay.js: - Fix Map mutation during iteration causing infinite loops - Use array snapshots for _updateAllFormations and clearAllFormations - Add guards to skip unnecessary RAF work when no formations exist - Simplify range change detection with primitive comparison Formations.py: - Fix DataCache method names to match actual API - Use insert_row_into_datacache, modify_datacache_item, remove_row_from_datacache communication.js: - Enable persistent reconnection (Infinity attempts) - Add bounded queue (250 max) to prevent memory growth - Coalesce candle_data messages (keep latest only) - Prioritize control/CRUD messages in queue flush formations.js: - Add user feedback alert when deleting while offline Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5717ac6a81
commit
fceb040ef0
|
|
@ -211,12 +211,11 @@ class Formations:
|
|||
now
|
||||
)
|
||||
|
||||
# Insert into database via DataCache
|
||||
self.data_cache.add_row_to_datacache(
|
||||
# Insert into database/cache via DataCache
|
||||
self.data_cache.insert_row_into_datacache(
|
||||
cache_name='formations',
|
||||
row_data=dict(zip(columns, values)),
|
||||
tbl_key=tbl_key,
|
||||
persist=True
|
||||
columns=columns,
|
||||
values=values
|
||||
)
|
||||
|
||||
# Create Formation object and add to memory cache
|
||||
|
|
@ -295,12 +294,14 @@ class Formations:
|
|||
|
||||
formation.updated_at = now
|
||||
|
||||
# Update in database
|
||||
self.data_cache.update_row_in_datacache(
|
||||
# Update in database/cache
|
||||
self.data_cache.modify_datacache_item(
|
||||
cache_name='formations',
|
||||
tbl_key=tbl_key,
|
||||
updates=update_data,
|
||||
persist=True
|
||||
filter_vals=[('tbl_key', tbl_key)],
|
||||
field_names=tuple(update_data.keys()),
|
||||
new_values=tuple(update_data.values()),
|
||||
key=tbl_key,
|
||||
overwrite='tbl_key'
|
||||
)
|
||||
|
||||
logger.info(f"Updated formation '{formation.name}' (tbl_key: {tbl_key})")
|
||||
|
|
@ -333,11 +334,10 @@ class Formations:
|
|||
if formation.user_id != user_id:
|
||||
return {"success": False, "message": "Not authorized to delete this formation"}
|
||||
|
||||
# Remove from database
|
||||
self.data_cache.delete_row_from_datacache(
|
||||
# Remove from database/cache
|
||||
self.data_cache.remove_row_from_datacache(
|
||||
cache_name='formations',
|
||||
tbl_key=tbl_key,
|
||||
persist=True
|
||||
filter_vals=[('tbl_key', tbl_key)]
|
||||
)
|
||||
|
||||
# Remove from memory cache
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class Comms {
|
|||
|
||||
// Initialize the message queue
|
||||
this.messageQueue = [];
|
||||
this.maxQueueSize = 250;
|
||||
|
||||
// Save the userName
|
||||
this.userName = userName;
|
||||
|
|
@ -62,8 +63,9 @@ class Comms {
|
|||
query: { 'user_name': this.userName },
|
||||
transports: ['websocket'], // Optional: Force WebSocket transport
|
||||
autoConnect: true,
|
||||
reconnectionAttempts: 5, // Optional: Number of reconnection attempts
|
||||
reconnectionDelay: 1000 // Optional: Delay between reconnections
|
||||
reconnection: true,
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 1000
|
||||
});
|
||||
|
||||
// Handle connection events
|
||||
|
|
@ -118,8 +120,24 @@ class Comms {
|
|||
* Flushes the message queue by sending all queued messages.
|
||||
*/
|
||||
_flushMessageQueue() {
|
||||
while (this.messageQueue.length > 0) {
|
||||
const { messageType, data } = this.messageQueue.shift();
|
||||
// Prioritize control/CRUD messages first, keep only the latest candle_data.
|
||||
const queued = this.messageQueue.splice(0);
|
||||
const priority = [];
|
||||
let latestCandleMessage = null;
|
||||
|
||||
queued.forEach((msg) => {
|
||||
if (msg.messageType === 'candle_data') {
|
||||
latestCandleMessage = msg;
|
||||
} else {
|
||||
priority.push(msg);
|
||||
}
|
||||
});
|
||||
|
||||
const messagesToSend = latestCandleMessage
|
||||
? [...priority, latestCandleMessage]
|
||||
: priority;
|
||||
|
||||
messagesToSend.forEach(({ messageType, data }) => {
|
||||
this.socket.emit('message', {
|
||||
message_type: messageType,
|
||||
data: {
|
||||
|
|
@ -128,6 +146,29 @@ class Comms {
|
|||
}
|
||||
});
|
||||
console.log(`Comms: Sent queued message-> ${JSON.stringify({ messageType, data })}`);
|
||||
});
|
||||
}
|
||||
|
||||
_enqueueMessage(messageType, data) {
|
||||
if (messageType === 'candle_data') {
|
||||
// Candle data is high-frequency. Keep only the latest unsent candle update.
|
||||
const existingIndex = this.messageQueue.findIndex(msg => msg.messageType === 'candle_data');
|
||||
if (existingIndex !== -1) {
|
||||
this.messageQueue[existingIndex] = { messageType, data };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.messageQueue.push({ messageType, data });
|
||||
|
||||
// Prevent unbounded queue growth while disconnected.
|
||||
while (this.messageQueue.length > this.maxQueueSize) {
|
||||
const candleIndex = this.messageQueue.findIndex(msg => msg.messageType === 'candle_data');
|
||||
if (candleIndex !== -1) {
|
||||
this.messageQueue.splice(candleIndex, 1);
|
||||
} else {
|
||||
this.messageQueue.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -468,7 +509,7 @@ class Comms {
|
|||
// Not an error; message will be queued
|
||||
console.warn('Socket.IO connection is not open. Queuing message.');
|
||||
// Queue the message to be sent once connected
|
||||
this.messageQueue.push({ messageType, data });
|
||||
this._enqueueMessage(messageType, data);
|
||||
console.warn(`Comms: Queued message-> ${JSON.stringify({ messageType, data })} (Connection not open)`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ class FormationOverlay {
|
|||
this._loopRunning = false;
|
||||
this._animationFrameId = null;
|
||||
this._lastTimeRange = null;
|
||||
this._lastPriceRange = null;
|
||||
|
||||
// Callback for saving formations
|
||||
this.onSaveCallback = null;
|
||||
|
|
@ -116,9 +115,12 @@ class FormationOverlay {
|
|||
const sync = () => {
|
||||
if (!this._loopRunning) return;
|
||||
|
||||
// Check if chart view has changed
|
||||
if (this._hasViewChanged()) {
|
||||
this._updateAllFormations();
|
||||
// Skip work when there's nothing to redraw.
|
||||
if (this.renderedFormations.size > 0 || this.drawingMode) {
|
||||
// Check if chart view has changed
|
||||
if (this._hasViewChanged()) {
|
||||
this._updateAllFormations();
|
||||
}
|
||||
}
|
||||
|
||||
this._animationFrameId = requestAnimationFrame(sync);
|
||||
|
|
@ -138,15 +140,20 @@ class FormationOverlay {
|
|||
try {
|
||||
const timeScale = this.chart.timeScale();
|
||||
const timeRange = timeScale.getVisibleLogicalRange();
|
||||
// In v5, use barsInLogicalRange for visible bars info
|
||||
const visibleBars = this.candleSeries ? this.candleSeries.barsInLogicalRange(timeRange) : null;
|
||||
if (!timeRange) return false;
|
||||
|
||||
const timeChanged = JSON.stringify(timeRange) !== JSON.stringify(this._lastTimeRange);
|
||||
const barsChanged = JSON.stringify(visibleBars) !== JSON.stringify(this._lastPriceRange);
|
||||
// Store only stable primitives for quick equality checks.
|
||||
const nextRange = {
|
||||
from: Number(timeRange.from),
|
||||
to: Number(timeRange.to)
|
||||
};
|
||||
|
||||
if (timeChanged || barsChanged) {
|
||||
this._lastTimeRange = timeRange;
|
||||
this._lastPriceRange = visibleBars;
|
||||
const changed = !this._lastTimeRange
|
||||
|| nextRange.from !== this._lastTimeRange.from
|
||||
|| nextRange.to !== this._lastTimeRange.to;
|
||||
|
||||
if (changed) {
|
||||
this._lastTimeRange = nextRange;
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -678,10 +685,10 @@ class FormationOverlay {
|
|||
* Update all rendered formations (called when chart view changes).
|
||||
*/
|
||||
_updateAllFormations() {
|
||||
for (const [tblKey, data] of this.renderedFormations) {
|
||||
// Re-render the formation
|
||||
this.renderFormation(data.formation);
|
||||
}
|
||||
// IMPORTANT: iterate over a snapshot, because renderFormation mutates renderedFormations.
|
||||
// Mutating a Map while iterating it can lead to effectively infinite loops.
|
||||
const formationsSnapshot = Array.from(this.renderedFormations.values()).map(item => item.formation);
|
||||
formationsSnapshot.forEach(formation => this.renderFormation(formation));
|
||||
|
||||
// Update temp elements if drawing
|
||||
if (this.drawingMode && this.currentPoints.length > 0) {
|
||||
|
|
@ -714,7 +721,9 @@ class FormationOverlay {
|
|||
* Clear all formations from the overlay.
|
||||
*/
|
||||
clearAllFormations() {
|
||||
for (const tblKey of this.renderedFormations.keys()) {
|
||||
// Iterate over a stable key snapshot to avoid mutating the Map during iteration.
|
||||
const keys = Array.from(this.renderedFormations.keys());
|
||||
for (const tblKey of keys) {
|
||||
this.removeFormation(tblKey);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -519,6 +519,9 @@ class Formations {
|
|||
}
|
||||
|
||||
if (this.comms) {
|
||||
if (!this.comms.connectionOpen) {
|
||||
alert('Server connection is offline. Delete will be sent after reconnect.');
|
||||
}
|
||||
this.comms.sendToApp('delete_formation', { tbl_key: tblKey });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue