Fix live updates: debounce DB writes, broadcast before saving, include smeltable/craftable in updates
This commit is contained in:
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
Reference in New Issue
Block a user