@@ -190,6 +190,215 @@ interface Client<ClientData> {
190190}
191191```
192192
193+ ## Components
194+
195+ ### ` SyncZone `
196+
197+ A context provider for hierarchical key prefixing and host inheritance. Zones enable component reusability and organized state namespacing.
198+
199+ ``` tsx
200+ import { SyncZone , SyncBox } from ' @robojs/sync'
201+
202+ function Game() {
203+ return (
204+ <SyncZone id = { [' game' , ' room1' ]} >
205+ <SyncBox id = { [' player' ]} initialState = { { x: 0 , y: 0 }} >
206+ <PlayerSprite />
207+ </SyncBox >
208+ { /* key becomes 'game.room1.player' */ }
209+ </SyncZone >
210+ )
211+ }
212+ ```
213+
214+ ** Nested zones** accumulate prefixes:
215+
216+ ``` tsx
217+ <SyncZone id = { [' game' ]} >
218+ <SyncZone id = { [' board' ]} >
219+ <SyncBox id = { [' piece' ]} />
220+ { /* key: 'game.board.piece' */ }
221+ </SyncZone >
222+ </SyncZone >
223+ ```
224+
225+ ** Host rules** control host determination:
226+
227+ ``` tsx
228+ // First subscriber becomes host (default)
229+ <SyncZone id = { [' match' ]} hostRules = " first" >
230+
231+ // Explicit host assignment
232+ <SyncZone id = { [' match' ]} hostRules = " explicit" host = { adminClientId } >
233+ ```
234+
235+ | Prop | Type | Description |
236+ |------|------|-------------|
237+ | `id` | `(string \| null)[]` | Key prefix for this zone |
238+ | `hostRules` | `'first' \| 'explicit'` | Host determination strategy (default: `'first'`) |
239+ | `host` | `string \| null` | Explicit host client ID (when `hostRules='explicit'`) |
240+
241+ ### `SyncBox`
242+
243+ A synced container component that synchronizes arbitrary state across clients. General-purpose—apps define their own logic for what to sync.
244+
245+ **With render props (recommended):**
246+
247+ ```tsx
248+ import { SyncBox } from '@robojs/sync'
249+
250+ function Cursor() {
251+ return (
252+ <SyncBox id = { [' cursor' ]} initialState = { { x: 0.5 , y: 0.5 }} throttle = { 16 } >
253+ { (state , setState , status ) => (
254+ <div style = { { left: ` ${state ?.x * 100 }% ` , top: ` ${state ?.y * 100 }% ` }} >
255+ { status .stale && <span >Reconnecting...</span >}
256+ </div >
257+ )}
258+ </SyncBox >
259+ )
260+ }
261+ ```
262+
263+ **With lockable (exclusive ownership):**
264+
265+ ```tsx
266+ <SyncBox id = { [' ball' ]} initialState = { { x: 0 , y: 0 }} lockable >
267+ { (state , setState , status , context , lock ) => (
268+ <div
269+ onMouseDown = { () => lock ?.lock ()}
270+ onMouseUp = { () => lock ?.unlock ()}
271+ style = { {
272+ cursor: lock ?.isLocked
273+ ? lock .isLockHolder ? ' grabbing' : ' not-allowed'
274+ : ' grab'
275+ }}
276+ />
277+ )}
278+ </SyncBox >
279+ ```
280+
281+ **With interpolation (smooth remote movement):**
282+
283+ ```tsx
284+ <SyncBox
285+ id = { [' cursor' ]}
286+ initialState = { { x: 0.5 , y: 0.5 }}
287+ interpolate = { { x: 0.15 , y: 0.15 }}
288+ >
289+ { (state ) => <Cursor x = { state ?.x } y = { state ?.y } />}
290+ </SyncBox >
291+ ```
292+
293+ **With optimistic updates:**
294+
295+ ```tsx
296+ <SyncBox id = { [' counter' ]} initialState = { { count: 0 }} >
297+ { (state , setState ) => (
298+ <button onClick = { () => setState ({ count: (state ?.count ?? 0 ) + 1 }, { optimistic: true })} >
299+ { state ?.count }
300+ </button >
301+ )}
302+ </SyncBox >
303+ ```
304+
305+ **No wrapper element:**
306+
307+ ```tsx
308+ <SyncBox as = { null } id = { [' data' ]} initialState = { { value: ' ' }} >
309+ { (state , setState ) => <input value = { state ?.value } onChange = { e => setState ({ value: e .target .value })} />}
310+ </SyncBox >
311+ ```
312+
313+ | Prop | Type | Description |
314+ |------|------|-------------|
315+ | `id` | `(string \| null)[]` | Key suffix (combined with zone prefix) |
316+ | `initialState` | `T` | Initial state value |
317+ | `children` | `ReactNode \| (state, setState, status, context, lock?) => ReactNode` | Children or render function |
318+ | `throttle` | `number \| { [field ]: number } ` | Throttle setState calls (ms), global or per-field |
319+ | `lockable` | `boolean` | Enable exclusive ownership mode |
320+ | `interpolate` | `{ [field ]: number } ` | Lerp factor per field for smooth remote updates (0-1) |
321+ | `as` | `ElementType \| null` | Wrapper element (default: `'div'`, `null` for none) |
322+ | `onStateChange` | `(state, prev) => void` | Called when synced state changes |
323+ | `onSyncStatusChange` | `(status) => void` | Called when sync status changes |
324+ | `onConflict` | `(local, remote) => T` | Resolve conflicts between optimistic and remote state |
325+ | `style` | `CSSProperties` | Styles for wrapper element |
326+ | `className` | `string` | Class name for wrapper element |
327+
328+ **Render function signature:**
329+
330+ ```ts
331+ (state, setState, status, context, lock?) => ReactNode
332+
333+ // setState supports options
334+ setState({ x : 1 } , { optimistic : true , throttle : 16 } )
335+ ```
336+
337+ **Lock context** (when `lockable` is enabled):
338+
339+ ```ts
340+ interface LockContext {
341+ isLocked : boolean // Anyone holds the lock
342+ lockedBy : string | null // Lock holder's client ID
343+ isLockHolder : boolean // Current client holds the lock
344+ lock : () => void // Acquire lock (auto-assigns to you)
345+ unlock : () => void // Release lock
346+ }
347+ ```
348+
349+ **Imperative handle** (`SyncBoxHandle<T >`):
350+
351+ ```ts
352+ interface SyncBoxHandle<T > {
353+ getState : () => T | undefined
354+ setState : (value , options ? ) => void
355+ getSyncStatus : () => SyncStatus
356+ lock ?: LockContext // When lockable=true
357+ }
358+
359+ interface SyncStatus {
360+ synced : boolean // Initial state received
361+ syncing : boolean // Update in progress
362+ stale : boolean // May be outdated
363+ lastSyncedAt ?: number // Timestamp
364+ }
365+ ```
366+
367+ ### `useZoneContext`
368+
369+ Access the current zone's context:
370+
371+ ```tsx
372+ import { useZoneContext } from '@robojs/sync'
373+
374+ function GameStatus() {
375+ const zone = useZoneContext ()
376+ if (! zone ) return null
377+
378+ return (
379+ <div >
380+ { zone .isHost ? ' You are the host' : ` Host: ${zone .hostId } ` }
381+ Players: { zone .clients .length }
382+ </div >
383+ )
384+ }
385+ ```
386+
387+ ### `useZoneKey`
388+
389+ Compute the full key by combining zone prefix with a local id:
390+
391+ ```tsx
392+ import { useZoneKey , useSyncState } from '@robojs/sync'
393+
394+ function PlayerMarker({ playerId } ) {
395+ const fullKey = useZoneKey ([' player' , playerId ])
396+ // In <SyncZone id={['game']}>, fullKey = ['game', 'player', playerId]
397+ const [position , setPosition ] = useSyncState ({ x: 0 , y: 0 }, fullKey )
398+ // ...
399+ }
400+ ```
401+
193402## Server Access
194403
195404For advanced use cases, access the WebSocket server directly:
@@ -201,6 +410,47 @@ SyncServer.start() // Manually start (usually automatic)
201410const wss = SyncServer.getSocketServer() // Get underlying WebSocketServer
202411```
203412
413+ ### Server-Side Zone API
414+
415+ Control sync state from the server for game logic, admin actions, or validation:
416+
417+ ```ts
418+ import { SyncServer } from '@robojs/sync/server.js'
419+
420+ // Get a zone handle
421+ const zone = SyncServer.getZone(['game', 'room1'])
422+
423+ // Read state
424+ const state = zone.getState()
425+
426+ // Update state (broadcasts to all subscribers)
427+ zone.setState({ phase : ' playing' , round : 2 } )
428+
429+ // Override host (e.g., for admin actions)
430+ zone.setHost(newHostId)
431+ zone.setHost(null) // Clear host
432+
433+ // Get current host and clients
434+ const hostId = zone.getHost()
435+ const clients = zone.getClients()
436+
437+ // Send ephemeral messages
438+ zone.broadcast({ event : ' countdown' , seconds : 5 } )
439+ zone.send(clientId, { message : ' private' } )
440+ ```
441+
442+ Server-initiated messages include `fromClientId: '__server__'` so clients can distinguish them from player messages.
443+
444+ | Method | Description |
445+ |--------|-------------|
446+ | `getState()` | Get current state for the zone key |
447+ | `setState(data)` | Set state and broadcast to all subscribers |
448+ | `setHost(clientId \| null)` | Override or clear the host |
449+ | `getHost()` | Get current host client ID |
450+ | `getClients()` | Get all subscribed clients |
451+ | `broadcast(payload)` | Send to all subscribers |
452+ | `send(clientId, payload)` | Send to specific client |
453+
204454## Need more power? ⚡
205455
206456For complex multiplayer games, check out [**Colyseus**](https://colyseus.io/)—a powerful multiplayer game server that pairs perfectly with Robo.js.
0 commit comments