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,
smeltingPaused: data.smeltingPaused !== undefined ? data.smeltingPaused : get().smeltingPaused,
disabledRecipes: data.disabledRecipes || get().disabledRecipes,
smeltable: data.smeltable || get().smeltable,
craftable: data.craftable || get().craftable,
craftTurtleOk: data.craftTurtleOk !== undefined ? data.craftTurtleOk : get().craftTurtleOk,
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 now = Date.now();
// Items
const _saveFullStateTransaction = db.transaction((state, now) => {
// Items — inline loop instead of calling saveItemsBatch to avoid nested transaction
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.)
@@ -349,14 +355,33 @@ export const saveFullState = db.transaction((state) => {
upsertState.run({ key: 'inventoryMeta', value: JSON.stringify(meta), updatedAt: now });
}
// Furnaces
// Furnaces — inline loop
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)) {
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
@@ -381,10 +406,56 @@ export const saveFullState = db.transaction((state) => {
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
*/
export function closeDb() {
flushPendingSave();
db.close();
}

View File

@@ -5,7 +5,7 @@ import { createServer } from 'http';
import {
loadFullState, saveFullState, recordItemHistory,
saveItems, saveFurnaces, saveAlerts, saveState,
getHistory, closeDb,
getHistory, closeDb, flushPendingSave,
} from './db.js';
const app = express();
@@ -378,27 +378,7 @@ function updateStateFromBridge(data) {
lastUpdate = Date.now();
// Persist to SQLite
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
// Broadcast to all web clients FIRST (non-blocking, instant)
broadcastToClients({
type: 'state_update',
inventory: inventoryState,
@@ -406,9 +386,28 @@ function updateStateFromBridge(data) {
alerts: alertsState,
smeltingPaused,
disabledRecipes,
smeltable: smeltableRecipes,
craftable: craftableRecipes,
craftTurtleOk,
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 ==========