create-fluxstack 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LLMD/INDEX.md +4 -3
  2. package/LLMD/resources/live-binary-delta.md +507 -0
  3. package/LLMD/resources/live-components.md +1 -0
  4. package/LLMD/resources/live-rooms.md +731 -333
  5. package/app/client/.live-stubs/LivePingPong.js +10 -0
  6. package/app/client/.live-stubs/LiveRoomChat.js +3 -2
  7. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  8. package/app/client/src/App.tsx +15 -14
  9. package/app/client/src/components/AppLayout.tsx +4 -4
  10. package/app/client/src/live/PingPongDemo.tsx +199 -0
  11. package/app/client/src/live/RoomChatDemo.tsx +187 -22
  12. package/app/client/src/live/SharedCounterDemo.tsx +142 -0
  13. package/app/server/live/LivePingPong.ts +61 -0
  14. package/app/server/live/LiveRoomChat.ts +106 -38
  15. package/app/server/live/LiveSharedCounter.ts +73 -0
  16. package/app/server/live/rooms/ChatRoom.ts +68 -0
  17. package/app/server/live/rooms/CounterRoom.ts +51 -0
  18. package/app/server/live/rooms/DirectoryRoom.ts +42 -0
  19. package/app/server/live/rooms/PingRoom.ts +40 -0
  20. package/core/build/live-components-generator.ts +10 -1
  21. package/core/client/index.ts +0 -16
  22. package/core/server/live/auto-generated-components.ts +3 -9
  23. package/core/server/live/index.ts +0 -5
  24. package/core/server/live/websocket-plugin.ts +37 -2
  25. package/core/utils/version.ts +1 -1
  26. package/package.json +100 -99
  27. package/tsconfig.json +4 -1
  28. package/app/client/.live-stubs/LiveChat.js +0 -7
  29. package/app/client/.live-stubs/LiveTodoList.js +0 -9
  30. package/app/client/src/live/ChatDemo.tsx +0 -107
  31. package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
  32. package/app/client/src/live/TodoListDemo.tsx +0 -158
  33. package/app/server/live/LiveChat.ts +0 -78
  34. package/app/server/live/LiveTodoList.ts +0 -110
  35. package/core/client/components/LiveDebugger.tsx +0 -1324
package/LLMD/INDEX.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # FluxStack LLM Documentation
2
2
 
3
- **Version:** 1.12.1 | **Framework:** Bun + Elysia + React + Eden Treaty
3
+ **Version:** 2.0.0 | **Framework:** Bun + Elysia + React + Eden Treaty
4
4
 
5
5
  ## Quick Navigation
6
6
 
@@ -9,7 +9,7 @@
9
9
  **Creating Routes?** → [resources/routes-eden.md](resources/routes-eden.md)
10
10
  **REST API Auth?** → [resources/rest-auth.md](resources/rest-auth.md)
11
11
  **Live Components Auth?** → [resources/live-auth.md](resources/live-auth.md)
12
- **Real-time Rooms?** → [resources/live-rooms.md](resources/live-rooms.md)
12
+ **Real-time Rooms?** → [resources/live-rooms.md](resources/live-rooms.md) (Typed LiveRoom + untyped, password rooms, directory)
13
13
  **Debugging Logs?** → [resources/live-logging.md](resources/live-logging.md)
14
14
  **Config Issues?** → [config/declarative-system.md](config/declarative-system.md)
15
15
  **Plugin Development?** → [resources/plugins-external.md](resources/plugins-external.md)
@@ -34,9 +34,10 @@
34
34
  - [Live Components](resources/live-components.md) - WebSocket components
35
35
  - [REST Auth](resources/rest-auth.md) - Session & Token guards, middleware, rate limiting
36
36
  - [Live Auth](resources/live-auth.md) - Authentication for Live Components
37
- - [Live Rooms](resources/live-rooms.md) - Multi-room real-time communication
37
+ - [Live Rooms](resources/live-rooms.md) - Typed rooms (LiveRoom), password protection, room directory, untyped rooms
38
38
  - [Live Logging](resources/live-logging.md) - Per-component logging control
39
39
  - [Live Upload](resources/live-upload.md) - Chunked upload via Live Components
40
+ - [Live Binary Delta](resources/live-binary-delta.md) - High-frequency binary state sync
40
41
  - [External Plugins](resources/plugins-external.md) - Plugin development
41
42
  - [Routing (React Router v7)](reference/routing.md) - Frontend routing setup
42
43
 
@@ -0,0 +1,507 @@
1
+ # Binary Delta (High-Frequency State Sync)
2
+
3
+ **Version:** 1.14.0 | **Updated:** 2025-03-09
4
+
5
+ ## Overview
6
+
7
+ Binary Delta allows Live Components to send state updates as raw binary frames instead of JSON. This bypasses the JSON batcher and sends directly over the WebSocket, making it ideal for high-frequency updates like game state (positions, rotations, physics) or real-time sensor data.
8
+
9
+ ## When to Use Binary vs JSON
10
+
11
+ | Scenario | Use | Why |
12
+ |---|---|---|
13
+ | Forms, chat, CRUD | **JSON** (default `setState`) | Low frequency, readability matters |
14
+ | Dashboard metrics | **JSON** | Updates every few seconds |
15
+ | Game state (30-60 fps) | **Binary Delta** | Hundreds of updates/sec, payload size matters |
16
+ | Real-time collaboration (cursors) | **Binary Delta** | High frequency, small payloads |
17
+ | IoT sensor streams | **Binary Delta** | Continuous data, compact encoding |
18
+
19
+ **Rule of thumb:** If you're sending state updates more than ~10 times per second, Binary Delta will reduce bandwidth and latency significantly.
20
+
21
+ ## Wire Format
22
+
23
+ Each binary frame has this structure:
24
+
25
+ ```
26
+ [0x01] [idLen:u8] [componentId:utf8] [payload:bytes]
27
+ 1B 1B N bytes M bytes
28
+ ```
29
+
30
+ | Field | Size | Description |
31
+ |---|---|---|
32
+ | `0x01` | 1 byte | BINARY_STATE_DELTA marker |
33
+ | `idLen` | 1 byte | Length of componentId string |
34
+ | `componentId` | N bytes | UTF-8 encoded component ID |
35
+ | `payload` | M bytes | Your custom-encoded delta |
36
+
37
+ Total overhead: **2 + componentId.length** bytes. The payload is entirely yours to define.
38
+
39
+ ## Server-Side: `sendBinaryDelta()`
40
+
41
+ ### API
42
+
43
+ ```typescript
44
+ public sendBinaryDelta(
45
+ delta: Partial<TState>,
46
+ encoder: (delta: Partial<TState>) => Uint8Array
47
+ ): void
48
+ ```
49
+
50
+ - **delta** - Object with the state fields that changed (same shape as `setState`)
51
+ - **encoder** - Function that serializes the delta into bytes
52
+
53
+ ### Behavior
54
+
55
+ 1. Compares `delta` against current state - only actually changed fields are kept
56
+ 2. Updates internal state (same as `setState`)
57
+ 3. Calls your `encoder` with only the changed fields
58
+ 4. Wraps the result in the wire format and sends it
59
+ 5. If nothing changed, no frame is sent
60
+ 6. If WebSocket is closed (readyState !== 1), state updates but no frame is sent
61
+
62
+ ### Example: Simple Component
63
+
64
+ ```typescript
65
+ // app/server/live/LiveTracker.ts
66
+ import { LiveComponent } from '@core/types/types'
67
+
68
+ // Encoder: convert delta to binary using DataView
69
+ function encodePosition(delta: Record<string, any>): Uint8Array {
70
+ // Calculate size: 1 byte flags + 4 bytes per float field
71
+ let flags = 0
72
+ let size = 1 // flags byte
73
+
74
+ if ('x' in delta) { flags |= 0x01; size += 4 }
75
+ if ('y' in delta) { flags |= 0x02; size += 4 }
76
+ if ('speed' in delta) { flags |= 0x04; size += 4 }
77
+
78
+ const buffer = new ArrayBuffer(size)
79
+ const dv = new DataView(buffer)
80
+ let offset = 0
81
+
82
+ dv.setUint8(offset, flags); offset += 1
83
+ if ('x' in delta) { dv.setFloat32(offset, delta.x, true); offset += 4 }
84
+ if ('y' in delta) { dv.setFloat32(offset, delta.y, true); offset += 4 }
85
+ if ('speed' in delta) { dv.setFloat32(offset, delta.speed, true); offset += 4 }
86
+
87
+ return new Uint8Array(buffer)
88
+ }
89
+
90
+ export class LiveTracker extends LiveComponent<typeof LiveTracker.defaultState> {
91
+ static componentName = 'LiveTracker'
92
+ static publicActions = ['updatePosition'] as const
93
+ static defaultState = {
94
+ x: 0,
95
+ y: 0,
96
+ speed: 0
97
+ }
98
+
99
+ declare x: number
100
+ declare y: number
101
+ declare speed: number
102
+
103
+ private _interval?: ReturnType<typeof setInterval>
104
+
105
+ protected onMount() {
106
+ // Send position 30 times per second
107
+ this._interval = setInterval(() => {
108
+ this.sendBinaryDelta(
109
+ { x: this.x + Math.random(), y: this.y + Math.random() },
110
+ encodePosition
111
+ )
112
+ }, 33) // ~30fps
113
+ }
114
+
115
+ protected onDestroy() {
116
+ clearInterval(this._interval)
117
+ }
118
+
119
+ async updatePosition(payload: { x: number; y: number }) {
120
+ this.sendBinaryDelta(
121
+ { x: payload.x, y: payload.y },
122
+ encodePosition
123
+ )
124
+ return { success: true }
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## Client-Side: `binaryDecoder` option
130
+
131
+ ### With React (`useLiveComponent` / `Live.use`)
132
+
133
+ Pass the `binaryDecoder` option when mounting the component. The decoder receives the raw payload bytes (without the wire format header - that's already stripped) and must return an object to merge into state.
134
+
135
+ ```typescript
136
+ // app/client/src/live/TrackerDemo.tsx
137
+ import { useLiveComponent } from '@fluxstack/live-react'
138
+
139
+ // Decoder: must mirror the encoder logic
140
+ function decodePosition(buffer: Uint8Array): Record<string, any> {
141
+ const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
142
+ let offset = 0
143
+
144
+ const flags = dv.getUint8(offset); offset += 1
145
+ const result: Record<string, any> = {}
146
+
147
+ if (flags & 0x01) { result.x = dv.getFloat32(offset, true); offset += 4 }
148
+ if (flags & 0x02) { result.y = dv.getFloat32(offset, true); offset += 4 }
149
+ if (flags & 0x04) { result.speed = dv.getFloat32(offset, true); offset += 4 }
150
+
151
+ return result
152
+ }
153
+
154
+ export function TrackerDemo() {
155
+ const { state, call, connected } = useLiveComponent('LiveTracker', {
156
+ initialState: { x: 0, y: 0, speed: 0 },
157
+ binaryDecoder: decodePosition // <-- register decoder here
158
+ })
159
+
160
+ return (
161
+ <div>
162
+ <p>Position: ({state.x.toFixed(2)}, {state.y.toFixed(2)})</p>
163
+ <p>Speed: {state.speed.toFixed(2)}</p>
164
+ <p>{connected ? 'Connected' : 'Disconnected'}</p>
165
+ </div>
166
+ )
167
+ }
168
+ ```
169
+
170
+ ### With Vanilla JS (`LiveComponentHandle`)
171
+
172
+ ```typescript
173
+ import { LiveConnection, LiveComponentHandle } from '@fluxstack/live-client'
174
+
175
+ const conn = new LiveConnection({ url: 'ws://localhost:3000/api/live/ws' })
176
+ const tracker = new LiveComponentHandle(conn, 'LiveTracker', {
177
+ x: 0, y: 0, speed: 0
178
+ })
179
+
180
+ await tracker.mount()
181
+
182
+ // Register binary decoder AFTER mount
183
+ tracker.setBinaryDecoder(decodePosition)
184
+
185
+ tracker.onStateChange((state, delta) => {
186
+ console.log('Position:', state.x, state.y)
187
+ })
188
+ ```
189
+
190
+ **Important:** `setBinaryDecoder()` must be called AFTER `mount()`. The component needs a `componentId` (assigned by the server on mount) to register the binary handler.
191
+
192
+ ## Writing Encoders and Decoders
193
+
194
+ ### Strategy 1: DataView (Best Performance)
195
+
196
+ Use `DataView` with typed fields. Best for fixed schemas with numbers.
197
+
198
+ ```typescript
199
+ // Shared between server and client (e.g. app/shared/codec/trackerCodec.ts)
200
+
201
+ export function encode(delta: Record<string, any>): Uint8Array {
202
+ let flags = 0, size = 1
203
+ if ('x' in delta) { flags |= 0x01; size += 4 }
204
+ if ('y' in delta) { flags |= 0x02; size += 4 }
205
+
206
+ const buf = new ArrayBuffer(size)
207
+ const dv = new DataView(buf)
208
+ let off = 0
209
+ dv.setUint8(off, flags); off += 1
210
+ if (flags & 0x01) { dv.setFloat32(off, delta.x, true); off += 4 }
211
+ if (flags & 0x02) { dv.setFloat32(off, delta.y, true); off += 4 }
212
+ return new Uint8Array(buf)
213
+ }
214
+
215
+ export function decode(buffer: Uint8Array): Record<string, any> {
216
+ const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
217
+ let off = 0
218
+ const flags = dv.getUint8(off); off += 1
219
+ const result: Record<string, any> = {}
220
+ if (flags & 0x01) { result.x = dv.getFloat32(off, true); off += 4 }
221
+ if (flags & 0x02) { result.y = dv.getFloat32(off, true); off += 4 }
222
+ return result
223
+ }
224
+ ```
225
+
226
+ **Tip:** Put codec files in `app/shared/` so both server and client can import them.
227
+
228
+ ### Strategy 2: JSON-in-Binary (Simplest)
229
+
230
+ If you want binary transport without writing a custom codec, just JSON-encode into bytes:
231
+
232
+ ```typescript
233
+ function encode(delta: Record<string, any>): Uint8Array {
234
+ return new TextEncoder().encode(JSON.stringify(delta))
235
+ }
236
+
237
+ function decode(buffer: Uint8Array): Record<string, any> {
238
+ return JSON.parse(new TextDecoder().decode(buffer))
239
+ }
240
+ ```
241
+
242
+ This still bypasses the JSON batcher (lower latency) but doesn't save bandwidth. Good for prototyping before writing a proper codec.
243
+
244
+ ### Strategy 3: Bitmask Flags (Complex Schemas)
245
+
246
+ For state with many optional fields (like game state with tanks, bullets, explosions), use bitmask flags to indicate which fields are present:
247
+
248
+ ```typescript
249
+ // Field presence flags
250
+ const FLAG_TANKS = 0x01
251
+ const FLAG_BULLETS = 0x02
252
+ const FLAG_EXPLOSIONS = 0x04
253
+
254
+ function encode(delta: Record<string, any>): Uint8Array {
255
+ let flags = 0
256
+ if (delta.tanks) flags |= FLAG_TANKS
257
+ if (delta.bullets) flags |= FLAG_BULLETS
258
+ if (delta.explosions) flags |= FLAG_EXPLOSIONS
259
+
260
+ // Calculate total size, allocate buffer, write fields...
261
+ // See the full game codec example below
262
+ }
263
+ ```
264
+
265
+ ### Writing Strings in Binary
266
+
267
+ Helper functions for encoding/decoding strings inside binary payloads:
268
+
269
+ ```typescript
270
+ const textEncoder = new TextEncoder()
271
+ const textDecoder = new TextDecoder()
272
+
273
+ // Write: [1 byte length][N bytes UTF-8]
274
+ function writeString(dv: DataView, offset: number, str: string): number {
275
+ const bytes = textEncoder.encode(str)
276
+ dv.setUint8(offset, bytes.length) // max 255 chars
277
+ offset += 1
278
+ for (let i = 0; i < bytes.length; i++) {
279
+ dv.setUint8(offset + i, bytes[i])
280
+ }
281
+ return offset + bytes.length
282
+ }
283
+
284
+ // Read: [1 byte length][N bytes UTF-8]
285
+ function readString(dv: DataView, offset: number): [string, number] {
286
+ const len = dv.getUint8(offset)
287
+ offset += 1
288
+ const bytes = new Uint8Array(dv.buffer, dv.byteOffset + offset, len)
289
+ return [textDecoder.decode(bytes), offset + len]
290
+ }
291
+ ```
292
+
293
+ ## Real-World Example: Game State Codec
294
+
295
+ This codec is used by Battle Tanks to encode tanks, bullets, explosions, and laser beams into a single binary frame. It uses bitmask flags, DataView for typed fields, and string helpers for IDs.
296
+
297
+ ```typescript
298
+ // app/shared/codec/gameCodec.ts
299
+
300
+ interface TankDynamic {
301
+ id: string
302
+ x: number
303
+ z: number
304
+ rot: number
305
+ tRot: number
306
+ hp: number
307
+ alive: boolean
308
+ laserCharge: number
309
+ }
310
+
311
+ const FLAG_TANKS = 0x01
312
+ const FLAG_BULLETS = 0x02
313
+ const FLAG_EXPLOSIONS = 0x04
314
+ const FLAG_LASERS = 0x08
315
+
316
+ export function encodeGameState(delta: Record<string, any>): Uint8Array {
317
+ let size = 1 + 4 // flags (1B) + matchTime (4B)
318
+ let flags = 0
319
+
320
+ const tanks: TankDynamic[] | undefined = delta.tanks
321
+ if (tanks) {
322
+ flags |= FLAG_TANKS
323
+ size += 2 // tank count (uint16)
324
+ for (const t of tanks) {
325
+ const idBytes = new TextEncoder().encode(t.id)
326
+ // 1B idLen + id + 4 floats (x,z,rot,tRot) + hp (2B) + alive (1B) + laserCharge (4B)
327
+ size += 1 + idBytes.length + 16 + 2 + 1 + 4
328
+ }
329
+ }
330
+
331
+ // ... similar for bullets, explosions, lasers ...
332
+
333
+ const buffer = new ArrayBuffer(size)
334
+ const dv = new DataView(buffer)
335
+ let offset = 0
336
+
337
+ dv.setUint8(offset, flags); offset += 1
338
+ dv.setUint32(offset, delta.matchTime ?? 0, true); offset += 4
339
+
340
+ if (tanks) {
341
+ dv.setUint16(offset, tanks.length, true); offset += 2
342
+ for (const t of tanks) {
343
+ offset = writeString(dv, offset, t.id)
344
+ dv.setFloat32(offset, t.x, true); offset += 4
345
+ dv.setFloat32(offset, t.z, true); offset += 4
346
+ dv.setFloat32(offset, t.rot, true); offset += 4
347
+ dv.setFloat32(offset, t.tRot, true); offset += 4
348
+ dv.setUint16(offset, t.hp, true); offset += 2
349
+ dv.setUint8(offset, t.alive ? 1 : 0); offset += 1
350
+ dv.setFloat32(offset, t.laserCharge, true); offset += 4
351
+ }
352
+ }
353
+
354
+ return new Uint8Array(buffer)
355
+ }
356
+
357
+ export function decodeGameState(buffer: Uint8Array): Record<string, any> {
358
+ const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
359
+ let offset = 0
360
+
361
+ const flags = dv.getUint8(offset); offset += 1
362
+ const matchTime = dv.getUint32(offset, true); offset += 4
363
+ const result: Record<string, any> = { matchTime }
364
+
365
+ if (flags & FLAG_TANKS) {
366
+ const count = dv.getUint16(offset, true); offset += 2
367
+ const tanks: TankDynamic[] = []
368
+ for (let i = 0; i < count; i++) {
369
+ let id: string
370
+ ;[id, offset] = readString(dv, offset)
371
+ const x = dv.getFloat32(offset, true); offset += 4
372
+ const z = dv.getFloat32(offset, true); offset += 4
373
+ const rot = dv.getFloat32(offset, true); offset += 4
374
+ const tRot = dv.getFloat32(offset, true); offset += 4
375
+ const hp = dv.getUint16(offset, true); offset += 2
376
+ const alive = dv.getUint8(offset) === 1; offset += 1
377
+ const laserCharge = dv.getFloat32(offset, true); offset += 4
378
+ tanks.push({ id, x, z, rot, tRot, hp, alive, laserCharge })
379
+ }
380
+ result.tanks = tanks
381
+ }
382
+
383
+ // ... similar for bullets, explosions, lasers ...
384
+
385
+ return result
386
+ }
387
+ ```
388
+
389
+ ### Server Usage (Game Loop)
390
+
391
+ ```typescript
392
+ import { encodeGameState } from '@app/shared/codec/gameCodec'
393
+
394
+ export class LiveBattleTanks extends LiveComponent<typeof LiveBattleTanks.defaultState> {
395
+ static componentName = 'LiveBattleTanks'
396
+ static singleton = true
397
+ static publicActions = ['join', 'move', 'shoot'] as const
398
+ static defaultState = {
399
+ tanks: [] as TankDynamic[],
400
+ bullets: [] as any[],
401
+ explosions: [] as any[],
402
+ matchTime: 0
403
+ }
404
+
405
+ private _loop?: ReturnType<typeof setInterval>
406
+
407
+ protected onMount() {
408
+ // Game loop at 30fps
409
+ this._loop = setInterval(() => {
410
+ this.tick()
411
+ this.sendBinaryDelta(
412
+ {
413
+ tanks: this.state.tanks,
414
+ bullets: this.state.bullets,
415
+ explosions: this.state.explosions,
416
+ matchTime: this.state.matchTime
417
+ },
418
+ encodeGameState
419
+ )
420
+ }, 33)
421
+ }
422
+
423
+ protected onDestroy() {
424
+ clearInterval(this._loop)
425
+ }
426
+
427
+ private tick() {
428
+ // Update physics, process collisions, etc.
429
+ this.state.matchTime += 33
430
+ }
431
+ }
432
+ ```
433
+
434
+ ### Client Usage (React)
435
+
436
+ ```typescript
437
+ import { useLiveComponent } from '@fluxstack/live-react'
438
+ import { decodeGameState } from '@app/shared/codec/gameCodec'
439
+
440
+ export function BattleTanks() {
441
+ const { state, call } = useLiveComponent('LiveBattleTanks', {
442
+ initialState: { tanks: [], bullets: [], explosions: [], matchTime: 0 },
443
+ binaryDecoder: decodeGameState
444
+ })
445
+
446
+ // Render game using state.tanks, state.bullets, etc.
447
+ return <GameCanvas tanks={state.tanks} bullets={state.bullets} />
448
+ }
449
+ ```
450
+
451
+ ## Bandwidth Comparison
452
+
453
+ For a game with 8 tanks, 20 bullets, and 3 explosions at 30fps:
454
+
455
+ | Method | Payload Size | Per Second | Savings |
456
+ |---|---|---|---|
457
+ | JSON (`setState`) | ~2.4 KB | ~72 KB/s | baseline |
458
+ | Binary (DataView) | ~0.5 KB | ~15 KB/s | **~80%** |
459
+
460
+ Binary encoding is especially effective when state contains many numeric fields (floats, integers) since JSON encodes numbers as variable-length text while DataView uses fixed-size typed representations.
461
+
462
+ ## Key Differences: `sendBinaryDelta` vs `setState`
463
+
464
+ | | `setState` | `sendBinaryDelta` |
465
+ |---|---|---|
466
+ | **Format** | JSON | Custom binary |
467
+ | **Batching** | Merged per microtask | Immediate send |
468
+ | **Deduplication** | Yes (by componentId) | No |
469
+ | **Encoder** | Built-in (JSON.stringify) | You provide it |
470
+ | **Client decoder** | Built-in (JSON.parse) | You provide it |
471
+ | **Best for** | Low-frequency, readable data | High-frequency, compact data |
472
+ | **State update** | Yes | Yes (same internal behavior) |
473
+
474
+ Both methods update internal state identically. The difference is only in how the data is serialized and sent over the wire.
475
+
476
+ ## Important Notes
477
+
478
+ - Encoder and decoder must be **symmetric** - what you encode, you must decode in the same order and format
479
+ - Put codec files in `app/shared/` so server and client share the same code
480
+ - `sendBinaryDelta` only sends fields that actually changed (same diffing as `setState`)
481
+ - Binary frames bypass the JSON batcher and message deduplication
482
+ - Use `setBinaryDecoder()` only AFTER `mount()` (vanilla JS client)
483
+ - With React, just pass `binaryDecoder` in options - lifecycle is handled automatically
484
+ - If both `setState` and `sendBinaryDelta` are used on the same component, the client handles both (JSON messages go through the normal path, binary frames go through the decoder)
485
+
486
+ ## Files
487
+
488
+ **Core (Server)**
489
+ - `packages/core/src/component/LiveComponent.ts` - `sendBinaryDelta()` method
490
+ - `packages/core/src/component/managers/ComponentStateManager.ts` - Wire format implementation
491
+
492
+ **Client (Browser)**
493
+ - `packages/client/src/component.ts` - `setBinaryDecoder()` method
494
+ - `packages/client/src/connection.ts` - `handleBinaryMessage()` + `registerBinaryHandler()`
495
+
496
+ **React**
497
+ - `packages/react/src/hooks/useLiveComponent.ts` - `binaryDecoder` option in `UseLiveComponentOptions`
498
+
499
+ **Tests**
500
+ - `packages/core/src/__tests__/component/LiveComponent.binary.test.ts` - Wire format and behavior tests
501
+ - `packages/core/src/__tests__/component/fixtures/gameCodec.ts` - Full game codec example
502
+
503
+ ## Related
504
+
505
+ - [Live Components](./live-components.md) - Core Live Component documentation
506
+ - [Live Upload](./live-upload.md) - Chunked file upload (different binary protocol)
507
+ - [Live Rooms](./live-rooms.md) - Multi-room communication
@@ -1035,6 +1035,7 @@ export function UploadDemo() {
1035
1035
  - [Live Logging](./live-logging.md) - Per-component logging control
1036
1036
  - [Live Rooms](./live-rooms.md) - Multi-room real-time communication
1037
1037
  - [Live Upload](./live-upload.md) - Chunked file upload
1038
+ - [Live Binary Delta](./live-binary-delta.md) - High-frequency binary state sync
1038
1039
  - [Project Structure](../patterns/project-structure.md)
1039
1040
  - [Type Safety Patterns](../patterns/type-safety.md)
1040
1041
  - [WebSocket Plugin](../core/plugin-system.md)