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
|
now
|
||||||
)
|
)
|
||||||
|
|
||||||
# Insert into database via DataCache
|
# Insert into database/cache via DataCache
|
||||||
self.data_cache.add_row_to_datacache(
|
self.data_cache.insert_row_into_datacache(
|
||||||
cache_name='formations',
|
cache_name='formations',
|
||||||
row_data=dict(zip(columns, values)),
|
columns=columns,
|
||||||
tbl_key=tbl_key,
|
values=values
|
||||||
persist=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create Formation object and add to memory cache
|
# Create Formation object and add to memory cache
|
||||||
|
|
@ -295,12 +294,14 @@ class Formations:
|
||||||
|
|
||||||
formation.updated_at = now
|
formation.updated_at = now
|
||||||
|
|
||||||
# Update in database
|
# Update in database/cache
|
||||||
self.data_cache.update_row_in_datacache(
|
self.data_cache.modify_datacache_item(
|
||||||
cache_name='formations',
|
cache_name='formations',
|
||||||
tbl_key=tbl_key,
|
filter_vals=[('tbl_key', tbl_key)],
|
||||||
updates=update_data,
|
field_names=tuple(update_data.keys()),
|
||||||
persist=True
|
new_values=tuple(update_data.values()),
|
||||||
|
key=tbl_key,
|
||||||
|
overwrite='tbl_key'
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Updated formation '{formation.name}' (tbl_key: {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:
|
if formation.user_id != user_id:
|
||||||
return {"success": False, "message": "Not authorized to delete this formation"}
|
return {"success": False, "message": "Not authorized to delete this formation"}
|
||||||
|
|
||||||
# Remove from database
|
# Remove from database/cache
|
||||||
self.data_cache.delete_row_from_datacache(
|
self.data_cache.remove_row_from_datacache(
|
||||||
cache_name='formations',
|
cache_name='formations',
|
||||||
tbl_key=tbl_key,
|
filter_vals=[('tbl_key', tbl_key)]
|
||||||
persist=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove from memory cache
|
# Remove from memory cache
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ class Comms {
|
||||||
|
|
||||||
// Initialize the message queue
|
// Initialize the message queue
|
||||||
this.messageQueue = [];
|
this.messageQueue = [];
|
||||||
|
this.maxQueueSize = 250;
|
||||||
|
|
||||||
// Save the userName
|
// Save the userName
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
|
|
@ -62,8 +63,9 @@ class Comms {
|
||||||
query: { 'user_name': this.userName },
|
query: { 'user_name': this.userName },
|
||||||
transports: ['websocket'], // Optional: Force WebSocket transport
|
transports: ['websocket'], // Optional: Force WebSocket transport
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
reconnectionAttempts: 5, // Optional: Number of reconnection attempts
|
reconnection: true,
|
||||||
reconnectionDelay: 1000 // Optional: Delay between reconnections
|
reconnectionAttempts: Infinity,
|
||||||
|
reconnectionDelay: 1000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle connection events
|
// Handle connection events
|
||||||
|
|
@ -118,8 +120,24 @@ class Comms {
|
||||||
* Flushes the message queue by sending all queued messages.
|
* Flushes the message queue by sending all queued messages.
|
||||||
*/
|
*/
|
||||||
_flushMessageQueue() {
|
_flushMessageQueue() {
|
||||||
while (this.messageQueue.length > 0) {
|
// Prioritize control/CRUD messages first, keep only the latest candle_data.
|
||||||
const { messageType, data } = this.messageQueue.shift();
|
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', {
|
this.socket.emit('message', {
|
||||||
message_type: messageType,
|
message_type: messageType,
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -128,6 +146,29 @@ class Comms {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(`Comms: Sent queued message-> ${JSON.stringify({ messageType, data })}`);
|
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
|
// Not an error; message will be queued
|
||||||
console.warn('Socket.IO connection is not open. Queuing message.');
|
console.warn('Socket.IO connection is not open. Queuing message.');
|
||||||
// Queue the message to be sent once connected
|
// 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)`);
|
console.warn(`Comms: Queued message-> ${JSON.stringify({ messageType, data })} (Connection not open)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ class FormationOverlay {
|
||||||
this._loopRunning = false;
|
this._loopRunning = false;
|
||||||
this._animationFrameId = null;
|
this._animationFrameId = null;
|
||||||
this._lastTimeRange = null;
|
this._lastTimeRange = null;
|
||||||
this._lastPriceRange = null;
|
|
||||||
|
|
||||||
// Callback for saving formations
|
// Callback for saving formations
|
||||||
this.onSaveCallback = null;
|
this.onSaveCallback = null;
|
||||||
|
|
@ -116,9 +115,12 @@ class FormationOverlay {
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
if (!this._loopRunning) return;
|
if (!this._loopRunning) return;
|
||||||
|
|
||||||
// Check if chart view has changed
|
// Skip work when there's nothing to redraw.
|
||||||
if (this._hasViewChanged()) {
|
if (this.renderedFormations.size > 0 || this.drawingMode) {
|
||||||
this._updateAllFormations();
|
// Check if chart view has changed
|
||||||
|
if (this._hasViewChanged()) {
|
||||||
|
this._updateAllFormations();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._animationFrameId = requestAnimationFrame(sync);
|
this._animationFrameId = requestAnimationFrame(sync);
|
||||||
|
|
@ -138,15 +140,20 @@ class FormationOverlay {
|
||||||
try {
|
try {
|
||||||
const timeScale = this.chart.timeScale();
|
const timeScale = this.chart.timeScale();
|
||||||
const timeRange = timeScale.getVisibleLogicalRange();
|
const timeRange = timeScale.getVisibleLogicalRange();
|
||||||
// In v5, use barsInLogicalRange for visible bars info
|
if (!timeRange) return false;
|
||||||
const visibleBars = this.candleSeries ? this.candleSeries.barsInLogicalRange(timeRange) : null;
|
|
||||||
|
|
||||||
const timeChanged = JSON.stringify(timeRange) !== JSON.stringify(this._lastTimeRange);
|
// Store only stable primitives for quick equality checks.
|
||||||
const barsChanged = JSON.stringify(visibleBars) !== JSON.stringify(this._lastPriceRange);
|
const nextRange = {
|
||||||
|
from: Number(timeRange.from),
|
||||||
|
to: Number(timeRange.to)
|
||||||
|
};
|
||||||
|
|
||||||
if (timeChanged || barsChanged) {
|
const changed = !this._lastTimeRange
|
||||||
this._lastTimeRange = timeRange;
|
|| nextRange.from !== this._lastTimeRange.from
|
||||||
this._lastPriceRange = visibleBars;
|
|| nextRange.to !== this._lastTimeRange.to;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this._lastTimeRange = nextRange;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -678,10 +685,10 @@ class FormationOverlay {
|
||||||
* Update all rendered formations (called when chart view changes).
|
* Update all rendered formations (called when chart view changes).
|
||||||
*/
|
*/
|
||||||
_updateAllFormations() {
|
_updateAllFormations() {
|
||||||
for (const [tblKey, data] of this.renderedFormations) {
|
// IMPORTANT: iterate over a snapshot, because renderFormation mutates renderedFormations.
|
||||||
// Re-render the formation
|
// Mutating a Map while iterating it can lead to effectively infinite loops.
|
||||||
this.renderFormation(data.formation);
|
const formationsSnapshot = Array.from(this.renderedFormations.values()).map(item => item.formation);
|
||||||
}
|
formationsSnapshot.forEach(formation => this.renderFormation(formation));
|
||||||
|
|
||||||
// Update temp elements if drawing
|
// Update temp elements if drawing
|
||||||
if (this.drawingMode && this.currentPoints.length > 0) {
|
if (this.drawingMode && this.currentPoints.length > 0) {
|
||||||
|
|
@ -714,7 +721,9 @@ class FormationOverlay {
|
||||||
* Clear all formations from the overlay.
|
* Clear all formations from the overlay.
|
||||||
*/
|
*/
|
||||||
clearAllFormations() {
|
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);
|
this.removeFormation(tblKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,9 @@ class Formations {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.comms) {
|
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 });
|
this.comms.sendToApp('delete_formation', { tbl_key: tblKey });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue