Fix live updates: debounce DB writes, broadcast before saving, include smeltable/craftable in updates

This commit is contained in:
MayaTheShy
2026-03-21 18:15:50 -04:00
parent aed7d1f735
commit 80338d1973
3 changed files with 104 additions and 32 deletions

View File

@@ -68,6 +68,8 @@ export const useInventoryStore = create((set, get) => ({
alerts: data.alerts || get().alerts, alerts: data.alerts || get().alerts,
smeltingPaused: data.smeltingPaused !== undefined ? data.smeltingPaused : get().smeltingPaused, smeltingPaused: data.smeltingPaused !== undefined ? data.smeltingPaused : get().smeltingPaused,
disabledRecipes: data.disabledRecipes || get().disabledRecipes, disabledRecipes: data.disabledRecipes || get().disabledRecipes,
smeltable: data.smeltable || get().smeltable,
craftable: data.craftable || get().craftable,
craftTurtleOk: data.craftTurtleOk !== undefined ? data.craftTurtleOk : get().craftTurtleOk, craftTurtleOk: data.craftTurtleOk !== undefined ? data.craftTurtleOk : get().craftTurtleOk,
lastUpdate: data.lastUpdate || Date.now(), lastUpdate: data.lastUpdate || Date.now(),
}); });

View File

@@ -333,14 +333,20 @@ export function loadFullState() {
} }
/** /**
* Persist the full state to DB in one transaction * Persist the full state to DB in one transaction.
* Uses a single flat transaction (no nested sub-transactions).
*/ */
export const saveFullState = db.transaction((state) => { const _saveFullStateTransaction = db.transaction((state, now) => {
const now = Date.now(); // Items — inline loop instead of calling saveItemsBatch to avoid nested transaction
// Items
if (state.inventoryState?.itemList?.length) { if (state.inventoryState?.itemList?.length) {
saveItemsBatch(state.inventoryState.itemList, now); for (const item of state.inventoryState.itemList) {
upsertItem.run({
name: item.name || '',
count: item.count || 0,
displayName: item.displayName || item.name || '',
updatedAt: now,
});
}
} }
// Inventory metadata (totals, slot info, etc.) // Inventory metadata (totals, slot info, etc.)
@@ -349,14 +355,33 @@ export const saveFullState = db.transaction((state) => {
upsertState.run({ key: 'inventoryMeta', value: JSON.stringify(meta), updatedAt: now }); upsertState.run({ key: 'inventoryMeta', value: JSON.stringify(meta), updatedAt: now });
} }
// Furnaces // Furnaces — inline loop
if (state.inventoryState?.furnaceStatus && typeof state.inventoryState.furnaceStatus === 'object') { if (state.inventoryState?.furnaceStatus && typeof state.inventoryState.furnaceStatus === 'object') {
saveFurnacesBatch(state.inventoryState.furnaceStatus, now); for (const [name, f] of Object.entries(state.inventoryState.furnaceStatus)) {
upsertFurnace.run({
name,
type: f.type || 'minecraft:furnace',
active: f.active ? 1 : 0,
input: f.input || null,
fuel: f.fuel || null,
output: f.output || null,
updatedAt: now,
});
}
} }
// Alerts // Alerts — inline loop
if (Array.isArray(state.alertsState)) { if (Array.isArray(state.alertsState)) {
saveAlertsBatch(state.alertsState, now); clearAlertTriggered.run();
for (const alert of state.alertsState) {
upsertAlert.run({
item: alert.item || '',
triggered: alert.triggered ? 1 : 0,
current: alert.current || 0,
threshold: alert.threshold || 0,
updatedAt: now,
});
}
} }
// Key-value state // Key-value state
@@ -381,10 +406,56 @@ export const saveFullState = db.transaction((state) => {
upsertState.run({ key: 'lastUpdate', value: JSON.stringify(now), updatedAt: now }); upsertState.run({ key: 'lastUpdate', value: JSON.stringify(now), updatedAt: now });
}); });
/**
* Debounced save — buffers rapid updates and writes to DB at most every SAVE_INTERVAL ms.
* This prevents DB writes from blocking WebSocket broadcasts on every bridge update.
*/
const SAVE_INTERVAL = 2000; // Write at most every 2 seconds
let pendingSaveState = null;
let saveTimer = null;
export function saveFullState(state) {
pendingSaveState = state;
if (!saveTimer) {
saveTimer = setTimeout(() => {
saveTimer = null;
if (pendingSaveState) {
const toSave = pendingSaveState;
pendingSaveState = null;
try {
_saveFullStateTransaction(toSave, Date.now());
} catch (err) {
console.error('❌ DB save error:', err.message);
}
}
}, SAVE_INTERVAL);
}
}
/**
* Flush any pending save immediately (used during shutdown)
*/
export function flushPendingSave() {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
if (pendingSaveState) {
const toSave = pendingSaveState;
pendingSaveState = null;
try {
_saveFullStateTransaction(toSave, Date.now());
} catch (err) {
console.error('❌ DB flush error:', err.message);
}
}
}
/** /**
* Close the database connection gracefully * Close the database connection gracefully
*/ */
export function closeDb() { export function closeDb() {
flushPendingSave();
db.close(); db.close();
} }

View File

@@ -5,7 +5,7 @@ import { createServer } from 'http';
import { import {
loadFullState, saveFullState, recordItemHistory, loadFullState, saveFullState, recordItemHistory,
saveItems, saveFurnaces, saveAlerts, saveState, saveItems, saveFurnaces, saveAlerts, saveState,
getHistory, closeDb, getHistory, closeDb, flushPendingSave,
} from './db.js'; } from './db.js';
const app = express(); const app = express();
@@ -378,27 +378,7 @@ function updateStateFromBridge(data) {
lastUpdate = Date.now(); lastUpdate = Date.now();
// Persist to SQLite // Broadcast to all web clients FIRST (non-blocking, instant)
try {
saveFullState({
inventoryState,
activityState,
alertsState,
smeltingPaused,
disabledRecipes,
smeltableRecipes,
craftableRecipes,
craftTurtleOk,
});
// Record history snapshot (throttled to every 5 min internally)
if (inventoryState.itemList?.length) {
recordItemHistory(inventoryState.itemList);
}
} catch (err) {
console.error('❌ DB save error:', err.message);
}
// Broadcast to all web clients
broadcastToClients({ broadcastToClients({
type: 'state_update', type: 'state_update',
inventory: inventoryState, inventory: inventoryState,
@@ -406,9 +386,28 @@ function updateStateFromBridge(data) {
alerts: alertsState, alerts: alertsState,
smeltingPaused, smeltingPaused,
disabledRecipes, disabledRecipes,
smeltable: smeltableRecipes,
craftable: craftableRecipes,
craftTurtleOk, craftTurtleOk,
lastUpdate, lastUpdate,
}); });
// Persist to SQLite (debounced — won't block the broadcast)
saveFullState({
inventoryState,
activityState,
alertsState,
smeltingPaused,
disabledRecipes,
smeltableRecipes,
craftableRecipes,
craftTurtleOk,
});
// Record history snapshot (throttled to every 5 min internally)
if (inventoryState.itemList?.length) {
recordItemHistory(inventoryState.itemList);
}
} }
// ========== WebSocket Server ========== // ========== WebSocket Server ==========