Gathering detailed insights and metrics for @hpkv/zustand-multiplayer
Gathering detailed insights and metrics for @hpkv/zustand-multiplayer
Gathering detailed insights and metrics for @hpkv/zustand-multiplayer
Gathering detailed insights and metrics for @hpkv/zustand-multiplayer
A real-time synchronization middleware for Zustand that uses HPKV's WebSocket API for storage and real-time updates across clients.
npm install @hpkv/zustand-multiplayer
Typescript
Module System
Node Version
NPM Version
TypeScript (98.83%)
JavaScript (1.17%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
2 Stars
46 Commits
1 Watchers
6 Branches
1 Contributors
Updated on Jul 14, 2025
Latest Version
0.4.1
Package Id
@hpkv/zustand-multiplayer@0.4.1
Unpacked Size
192.97 kB
Size
42.92 kB
File Count
50
NPM Version
10.8.2
Node Version
20.19.3
Published on
Jul 14, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
2
1
30
Real-time state synchronization for Zustand stores. Build collaborative applications with automatic state sharing across multiple clients. The multiplayer
middleware brings state persistence and real-time synchronization to Zustand stores, making it easy to build multiplayer applications.
Zustand Multiplayer is a powerful middleware that transforms any Zustand store into a real-time collaborative state management system. It provides:
1npm install @hpkv/zustand-multiplayer zustand
1// store.js 2import { create } from 'zustand'; 3import { multiplayer } from '@hpkv/zustand-multiplayer'; 4 5export const usePollStore = create( 6 multiplayer( 7 (set) => ({ 8 votes: {}, 9 vote: (option) => set((state) => { 10 if (!state.votes[option]) { 11 state.votes[option] = 0; 12 } 13 state.votes[option] = state.votes[option] + 1; 14 }), 15 }), 16 { 17 namespace: 'live-poll', 18 apiBaseUrl: 'YOUR_HPKV_BASE_URL', 19 tokenGenerationUrl: 'http://localhost:3000/api/generate-token', 20 }) 21);
1// Your backend API endpoint 2import { TokenHelper } from '@hpkv/zustand-multiplayer'; 3import http from 'node:http'; 4 5const tokenHelper = new TokenHelper( 6 'Your_HPKV_API-Key', 7 'Your_HPKV_API_Base_URL' 8); 9 10const server = http.createServer(async (req, res) => { 11 if (req.method === 'POST' && req.url === '/api/generate-token') { 12 let body = ''; 13 req.on('data', chunk => body += chunk); 14 req.on('end', async () => { 15 const requestBody = JSON.parse(body); 16 // Use TokenHelper to generate token 17 const response = await tokenHelper.processTokenRequest(requestBody); 18 res.writeHead(200, { 'Content-Type': 'application/json' }); 19 res.end(JSON.stringify(response)); 20 }); 21 } else { 22 res.writeHead(404); 23 res.end(); 24 } 25}); 26 27server.listen(3000, () => console.log('Token server running on port 3000'));
📖 See Token API Guide for more details on the details and implementations in Express, Next.js, Fastify, and other frameworks.
1// App.js 2import { usePollStore } from './store'; 3 4function App() { 5 const { votes, vote } = usePollStore(); 6 7 return ( 8 <div> 9 <h1>What's your favorite food? 🍕</h1> 10 <button onClick={() => vote('pizza')}>Pizza ({votes[pizza] ?? 0})</button> 11 <button onClick={() => vote('burger')}>Burger ({votes[burger] ?? 0})</button> 12 <button onClick={() => vote('tacos')}>Tacos ({votes[tacos] ?? 0})</button> 13 <p>👆 Vote and watch results update live across all devices!</p> 14 </div> 15 ); 16}
🎉 That's it! Creating your online voting application just takes some simple steps!
Every store created with the multiplayer middleware provides a multiplayer
object with state and methods for managing the connection and synchronization:
1// You can access the multiplayer state to use multiplayer API and states 2const multiplayer = usePollStore((state) => state.multiplayer); 3 4// Connection state (reactive) 5console.log(multiplayer.connectionState); // 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'RECONNECTING' 6 7// Manual control methods 8multiplayer.hydrate(); // Refresh from server 9multiplayer.clearStorage(); // Clear local data 10multiplayer.connect(); // Establish connection 11multiplayer.disconnect(); // Close connection 12multiplayer.destroy(); // Cleanup resources 13 14// Status and metrics 15const status = multiplayer.getConnectionStatus(); 16const metrics = multiplayer.getMetrics();
Available State:
connectionState
- Reactive connection state (CONNECTED
, DISCONNECTED
, CONNECTING
, RECONNECTING
)hasHydrated
- Whether the store has loaded initial state from serverAvailable Methods:
hydrate()
- Manually sync with server stateclearStorage()
- Clear all local stored dataconnect()
- Establish connectiondisconnect()
- Close connectiongetConnectionStatus()
- Get detailed connection statisticsgetMetrics()
- Get performance statistics (sync times, operation counts)destroy()
- Destroy the store and cleanup resourcesBy default, every change to your state is synced with all connected clients. However, you have control over which parts of your state are shared and which remain local. The multiplayer middleware lets you specify exactly which state fields should be broadcast to others and which updates your store should listen for from other clients.
This can be managed using publishUpdatesFor
and subscribeToUpdatesFor
options.
Why use selective sync?
1export const useAppStore = create( 2 multiplayer( 3 (set) => ({ 4 // Shared across all users 5 sharedSettings: { teamSettings: {}, defaultLanguage: 'en' }, 6 // Local to each user 7 userPreferences: { theme: '' }, 8 // Actions... 9 }), 10 { 11 namespace: 'my-app', 12 // Only sync changes to shared settings with remote 13 publishUpdatesFor: () => ['sharedSettings'], 14 // Only receive updates for shared settings from other clients 15 subscribeToUpdatesFor: () => ['sharedSettings'], 16 }) 17);
Result:
sharedSettings
- synchronized across all users - will be persisted/synceduserPreferences
- local to each user - Won't be persisted/syncedMultiplayer uses a granular storage scheme that defines how nested state is stored and synchronized. Instead of treating entire objects as single units, it breaks down nested structures into individual storage keys, avoiding unnecessary conflicts for collaborative editing.
multiplayer
allows immer style state updates to conveniently changing only part of the state which is intended to be changed.
1 multiplayer( 2 (set) => ({ 3 todos: {}, 4 addTodo: (id, text) => set((state) => { 5 state.todos[id] = {id, text, completed:false} 6 }), 7 toggleTodo:(id) => set((state) => { 8 state.todos[id].completed = !state.todos[id].completed 9 }), 10 }), 11 { 12 //options... 13 })
For this example state value
{
"user": {
"profile": {
"name": "John",
"email": "john@example.com"
},
"preferences": {
"theme": "dark"
}
},
"todos": {
"1": {
"id": "1",
"text": "Buy milk"
},
"2": {
"id": "2",
"text": "Walk dog"
}
}
}
Each state part will be stored in a sepearate key in databse:
namespace:user:profile:name -> "John"
namespace:user:profile:email -> "john@example.com"
namespace:user:preferences:theme -> "dark"
namespace:todos:1:id -> "1"
namespace:todos:1:text -> "Buy milk"
namespace:todos:1:completed -> false
namespace:todos:2:id -> "2"
namespace:todos:2:text -> "Walk dog"
namespace:todos:2:completed -> true
Each nested property gets its own storage key, allowing the middleware to:
1export const useAppStore = create( 2 multiplayer( 3 (set) => ({ 4 // Nested object structure 5 user: { 6 profile: { name: '', email: '' }, 7 preferences: { theme: 'light', notifications: true } 8 }, 9 // Record structure (key-value pairs) 10 todos: {}, 11 12 updateUserEmail: (email) => set((state) => { 13 state.user.profile.email = email 14 }), 15 updatePreferences: (prefs) => set((state) => { 16 state.user.preferences = { ...state.user.preferences, ...prefs }; 17 }), 18 // Actions for Record structure 19 addTodo: (text) => set((state) => { 20 const id = Date.now().toString(); 21 state.todos[id] = { id, text, completed: false }; 22 }), 23 updateTodo: (id, updates) => set((state) => { 24 if (state.todos[id]) { 25 state.todos[id] = { ...state.todos[id], ...updates }; 26 } 27 }), 28 }), 29 { 30 namespace: 'collaborative-app', 31 // options... 32 }) 33);
📝 Records vs Arrays for Collections: multiplayer treats records as objects so each record entry will be stored in a separate key-value entry in the database, however arrays are treated as primitive types and array members will not be stored in separate key-value entries. Therefore when dealing with collection of objects that are going to be updated concurrently by multiple users, best is to use records instead of arrays.
When a client goes offline and comes back online, it may have missed updates from other clients. The conflict resolution system handles reconciling local pending changes with the current server state:
1const useSharedContentStore = create( 2 multiplayer( 3 (set) => ({ 4 content: '', 5 setContent: (content) => set((state) => state.content = state.content + content), 6 }), 7 { 8 namespace: 'shared-document', 9 onConflict: (conflicts) => { 10 const contentConflict = conflicts.find(c => c.field === 'content'); 11 if (contentConflict) { 12 const localChange = contentConflict.pendingValue; 13 const remoteContent = contentConflict.remoteValue; 14 return { 15 strategy: 'merge', 16 mergedValues: { 17 content: mergeDocumentContent(localChange, remoteContent), 18 } 19 }; 20 } 21 // For other fields, prefer remote (server) version 22 return { strategy: 'keep-remote' }; 23 }, 24 // rest of the options... 25 } 26 ) 27); 28 29function mergeDocumentContent(localContent, remoteContent) { 30 // Your merge logic here 31}
When conflicts occur:
onConflict
handler decides how to mergeAvailable strategies:
keep-remote
: Use the server state (default - safe choice)keep-local
: Use your local changes (may overwrite others' work)merge
: Custom merge with mergedValues
(merge the changes with the existing ones)The middleware provides comprehensive monitoring and debugging capabilities:
1// Get connection status and performance metrics 2const multiplayer = useMyStore((state) => state.multiplayer); 3 4// Connection status 5const status = multiplayer.getConnectionStatus(); 6console.log('Connected:', status?.isConnected); 7console.log('Reconnect attempts:', status?.reconnectAttempts); 8console.log('Pending messages:', status?.messagesPending); 9 10// Performance metrics 11const metrics = multiplayer.getMetrics(); 12console.log('State changes processed:', metrics.stateChangesProcessed); 13console.log('Average sync time:', metrics.averageSyncTime); 14console.log('Average hydration time:', metrics.averageHydrationTime);
Multiplayer will automatically connect and hydrate the state from database, However you can also take control when needed:
1// components/AdminControls.js 2function AdminControls() { 3 const { multiplayer } = useMyStore(); 4 5 return ( 6 <div> 7 <button onClick={() => multiplayer.hydrate()}> 8 Refresh from Server 9 </button> 10 <button onClick={() => multiplayer.clearStorage()}> 11 Clear All Data 12 </button> 13 <button onClick={() => multiplayer.disconnect()}> 14 Disconnect 15 </button> 16 <button onClick={() => multiplayer.connect()}> 17 Reconnect 18 </button> 19 </div> 20 ); 21}
Track connection health using reactive state:
1import { useMyStore } from './store'; 2import { ConnectionState } from '@hpkv/websocket-client'; 3function ConnectionMonitor() { 4 const {connectionState} = useMyStore((state) => state.multiplayer); 5 return ( 6 <div> 7 <p>Connected: {connectionState === ConnectionState.CONNECTED ? 'Yes' : 'No'}</p> 8 </div> 9 ); 10}
Always use WithMultiplayer<T>
wrapper for proper typing:
1import { create } from 'zustand'; 2import { multiplayer, WithMultiplayer } from '@hpkv/zustand-multiplayer'; 3 4 5interface TodoState { 6 todos: Record<string, {id: string, text: string, completed: boolean}>; 7 addTodo: (text: string) => void; 8 toggleTodo: (id: string) => void; 9 } 10 11 export const useTodoStore = create<WithMultiplayer<TodoState>>()( 12 multiplayer((set) => ({ 13 todos: {}, 14 addTodo: (text: string) => set((state: TodoState) => { 15 const id = Date.now().toString(); 16 state.todos[id] = {id, text, completed: false}; 17 }), 18 toggleTodo: (id: string) => set((state: TodoState) => { 19 state.todos[id].completed = !state.todos[id].completed; 20 }), 21 }), 22 { 23 namespace: 'todos', 24 apiBaseUrl: 'YOUR_HPKV_BASE_URL', 25 tokenGenerationUrl: 'http://localhost:3000/api/generate-token', 26 }) 27 );
1import { createStore } from 'zustand/vanilla'; 2import { multiplayer } from '@hpkv/zustand-multiplayer'; 3 4// Create store without React hooks 5const gameStore = createStore( 6 multiplayer( 7 (set) => ({ 8 players: {}, 9 gameState: 'waiting', 10 addPlayer: (id, name) => set((state) => { 11 state.players[id] = { name, score: 0 }; 12 }), 13 updateScore: (playerId, score) => set((state) => { 14 state.players[playerId].score = score; 15 }), 16 startGame: () => set((state) => { 17 state.gameState = 'playing'; 18 }), 19 }), 20 { 21 namespace: 'multiplayer-game', 22 apiBaseUrl: 'Your_HPKV_API_Base_URL', 23 apiKey: 'Your_HPKV_API-Key', // Server-side only 24 } 25 ) 26); 27 28gameStore.getState().addPlayer('player1', 'Alice'); 29gameStore.getState().addPlayer('player2', 'Bob'); 30gameStore.getState().startGame(); 31 32// Subscribe to changes 33gameStore.subscribe((state) => { 34 console.log('Game state updated:', state); 35 updateGameUI(state); 36});
Server-side stores can use your API key directly for authentication (no token generation endpoint needed). When client and server stores share the same namespace, they automatically synchronize state in real-time:
1// server-store.js 2import { createStore } from 'zustand/vanilla'; 3import { multiplayer } from '@hpkv/zustand-multiplayer'; 4 5const serverStore = createStore( 6 multiplayer( 7 (set) => ({ 8 notifications: {}, 9 addNotification: (message) => set((state) => { 10 state.notifications[message.id] = message; 11 }), 12 }), 13 { 14 namespace: 'live-notiictions', 15 apiBaseUrl: 'Your_HPKV_API_Base_URL', 16 apiKey: 'Your_HPKV_API-Key', 17 }) 18);
1// client-store.js 2export const useAppStore = create()( 3 multiplayer( 4 (set) => ({ 5 notifications: {}, 6 }), 7 { 8 namespace: 'live-notifications', // Same namespace = shared state 9 apiBaseUrl: 'Your_HPKV_API_Base_URL', 10 tokenGenerationUrl: '/api/generate-token', 11 }) 12);
Each store has a unique namespace
that:
namespace:
)tokenGenerationUrl
pointing to your secure backend endpointapiKey
directly (never expose in client code)All published state changes are automatically:
1{ 2 namespace: 'my-app', // Required: unique identifier 3 apiBaseUrl: 'hpkv-api-base-url', // Required: your HPKV base URL 4 tokenGenerationUrl: '/api/token', // Required for client-side 5 apiKey: 'your-api-key', // Required for server-side 6}
1{ 2 // Selective sync 3 publishUpdatesFor: () => ['field1', 'field2'], 4 subscribeToUpdatesFor: () => ['field1', 'field3'], 5 6 // Lifecycle hooks 7 onHydrate: (state) => console.log('Hydrated:', state), 8 onConflict: (conflicts) => ({ strategy: 'keep-remote' }), 9 10 // Performance & debugging 11 logLevel: LogLevel.INFO, 12 profiling: true, 13 retryConfig: { 14 maxRetries: 5, 15 baseDelay: 1000, 16 maxDelay: 30000, 17 backoffFactor: 2, 18 }, 19 20 // Websocket connection tuning 21 clientConfig: { 22 maxReconnectAttempts: 10, 23 throttling: { enabled: true, rateLimit: 10 } 24 } 25}
Check out the examples/
directory for complete working applications:
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
MIT - see LICENSE for details.
Need help? Check our documentation or open an issue.
No vulnerabilities found.
No security vulnerabilities found.