@martini-kit/devtools
v0.2.0
Published
Development tools for martini-kit - state inspector, action history, network monitor
Maintainers
Readme
@martini-kit/devtools
Development tools for martini-kit multiplayer SDK - debug your multiplayer games with ease.
Features
- Real-time State Inspection - Diff-based snapshots with automatic throttling
- Action History Tracking - Aggregated timelines that avoid spammy actions
- Statistics & Metrics - Track action frequency and state changes
- Event Listeners - React to state changes and actions programmatically
- Memory-Efficient - Configurable limits for history size
Installation
pnpm add -D @martini-kit/devtoolsQuick Start
import { StateInspector } from '@martini-kit/devtools';
import { GameRuntime } from '@martini-kit/core';
// Create your game runtime
const runtime = new GameRuntime(myGame, transport, config);
// Attach the inspector
const inspector = new StateInspector();
inspector.attach(runtime);
// View state snapshots
console.log(inspector.getSnapshots());
// View action history
console.log(inspector.getActionHistory());
// View statistics
console.log(inspector.getStats());
// Listen for changes
inspector.onStateChange((snapshot) => {
console.log('State changed:', snapshot.state);
});
inspector.onAction((action) => {
console.log('Action submitted:', action.actionName, action.input);
});
// Clean up when done
inspector.detach();API Reference
Constructor
new StateInspector(options?: StateInspectorOptions)Options:
maxSnapshots(default: 100) - Maximum number of state snapshots to keepmaxActions(default: 1000) - Maximum number of actions to keep in historysnapshotIntervalMs(default: 250) - Minimum time between automatic snapshotsactionAggregationWindowMs(default: 200) - Time window for grouping identical actionsignoreActions(default: []) - Array of action names to drop entirely (e.g.['tick'])
Methods
attach(runtime: GameRuntime): void
Attach the inspector to a GameRuntime instance. Only one runtime can be attached at a time.
inspector.attach(runtime);detach(): void
Detach the inspector from the current runtime and stop tracking.
inspector.detach();isAttached(): boolean
Check if the inspector is currently attached to a runtime.
if (inspector.isAttached()) {
console.log('Inspector is active');
}getRuntime(): GameRuntime | null
Get the currently attached runtime, or null if not attached.
const runtime = inspector.getRuntime();getSnapshots(): StateSnapshot[]
Get all captured state snapshots.
const snapshots = inspector.getSnapshots();
snapshots.forEach(snapshot => {
console.log(snapshot.timestamp, snapshot.state);
});Returns an array of:
interface StateSnapshot {
id: number; // Incrementing identifier
timestamp: number; // Unix timestamp in milliseconds
state?: any; // Deep clone of game state (only stored for baseline snapshots)
diff?: Patch[]; // Diffs relative to previous snapshot
lastActionId?: number;// Action id responsible for this snapshot
}getActionHistory(): ActionRecord[]
Get all tracked actions.
const history = inspector.getActionHistory();
history.forEach(action => {
console.log(action.actionName, action.input);
});Returns an array of:
interface ActionRecord {
id: number; // Incrementing identifier
timestamp: number; // Unix timestamp in milliseconds
actionName: string; // Name of the action
input: any; // Action payload
playerId?: string; // Who submitted the action
targetId?: string; // Who was targeted
count?: number; // Number of aggregated occurrences
duration?: number; // Duration of aggregated burst in ms
snapshotId?: number; // Linked snapshot id
excludedActionsTotal?: number; // Total ignored actions so far
}getStats(): InspectorStats
Get statistics about tracked actions and state changes.
const stats = inspector.getStats();
console.log(`Total actions: ${stats.totalActions}`);
console.log(`Total state changes: ${stats.totalStateChanges}`);
console.log(`Increment action called ${stats.actionsByName.increment} times`);Returns:
interface InspectorStats {
totalActions: number; // Total actions submitted
totalStateChanges: number; // Total state updates
actionsByName: Record<string, number>; // Action frequency map
excludedActions: number; // Actions filtered via ignoreActions
}onStateChange(listener: (snapshot: StateSnapshot) => void): () => void
Listen for state changes. Returns an unsubscribe function.
const unsubscribe = inspector.onStateChange((snapshot) => {
console.log('New state:', snapshot.state);
});
// Later: stop listening
unsubscribe();onAction(listener: (action: ActionRecord) => void): () => void
Listen for actions. Returns an unsubscribe function.
const unsubscribe = inspector.onAction((action) => {
console.log(`${action.actionName} submitted by ${action.playerId}`);
});
// Later: stop listening
unsubscribe();clear(): void
Clear all snapshots, action history, and statistics.
inspector.clear();Use Cases
Debug State Issues
const inspector = new StateInspector();
inspector.attach(runtime);
// Submit some actions
runtime.submitAction('move', { x: 100, y: 200 });
runtime.submitAction('attack', { targetId: 'enemy1' });
// View state evolution
const snapshots = inspector.getSnapshots();
console.log('Initial state:', snapshots[0].state);
console.log('Final state:', snapshots[snapshots.length - 1].state);Track Performance
const inspector = new StateInspector();
inspector.attach(runtime);
let actionCount = 0;
inspector.onAction(() => {
actionCount++;
if (actionCount % 100 === 0) {
const stats = inspector.getStats();
console.log(`Actions per second: ${actionCount / ((Date.now() - startTime) / 1000)}`);
}
});Build Custom DevTools UI
const inspector = new StateInspector({ maxSnapshots: 50 });
inspector.attach(runtime);
// React component example
function DevToolsPanel() {
const [snapshots, setSnapshots] = useState([]);
const [actions, setActions] = useState([]);
useEffect(() => {
const unsubState = inspector.onStateChange((snapshot) => {
setSnapshots(prev => [...prev, snapshot]);
});
const unsubAction = inspector.onAction((action) => {
setActions(prev => [...prev, action]);
});
return () => {
unsubState();
unsubAction();
};
}, []);
return (
<div>
<StateViewer snapshots={snapshots} />
<ActionHistory actions={actions} />
<Statistics stats={inspector.getStats()} />
</div>
);
}Monitor Multiplayer Sync
const inspector = new StateInspector();
inspector.attach(runtime);
inspector.onAction((action) => {
console.log(`[${action.timestamp}] ${action.playerId} -> ${action.actionName}`);
});
inspector.onStateChange((snapshot) => {
// Check for state anomalies
if (snapshot.state.players[myId]?.health < 0) {
console.warn('Invalid state detected:', snapshot);
}
});Testing with StateInspector
import { describe, it, expect } from 'vitest';
import { StateInspector } from '@martini-kit/devtools';
describe('MyGame', () => {
it('should increment counter on action', () => {
const inspector = new StateInspector();
inspector.attach(runtime);
const initialState = inspector.getSnapshots()[0].state;
expect(initialState.count).toBe(0);
runtime.submitAction('increment', {});
const finalSnapshots = inspector.getSnapshots();
const finalState = finalSnapshots[finalSnapshots.length - 1].state;
expect(finalState.count).toBe(1);
inspector.detach();
});
});Best Practices
- Use in Development Only - Don't ship StateInspector to production (it's a devDependency)
- Set Limits - Configure
maxSnapshotsandmaxActionsto prevent memory issues - Detach When Done - Always call
detach()to clean up listeners - Deep Clone Awareness - Snapshots are deep clones, so large states may impact performance
Next Steps
- See martini-kit SDK Documentation
- Check out the demos for working examples
- Read about Best Practices
License
MIT
