create-fluxstack 1.15.0 → 1.17.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.
- package/CHANGELOG.md +80 -0
- package/LLMD/INDEX.md +4 -3
- package/LLMD/resources/live-binary-delta.md +507 -0
- package/LLMD/resources/live-components.md +1 -0
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/src/App.tsx +23 -14
- package/app/client/src/components/AppLayout.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +4 -4
- package/app/client/src/live/PingPongDemo.tsx +199 -0
- package/app/client/src/live/RoomChatDemo.tsx +187 -22
- package/app/client/src/live/SharedCounterDemo.tsx +142 -0
- package/app/server/live/LivePingPong.ts +61 -0
- package/app/server/live/LiveRoomChat.ts +106 -38
- package/app/server/live/LiveSharedCounter.ts +73 -0
- package/app/server/live/rooms/ChatRoom.ts +68 -0
- package/app/server/live/rooms/CounterRoom.ts +51 -0
- package/app/server/live/rooms/DirectoryRoom.ts +42 -0
- package/app/server/live/rooms/PingRoom.ts +40 -0
- package/core/build/bundler.ts +40 -26
- package/core/build/flux-plugins-generator.ts +325 -325
- package/core/build/index.ts +92 -21
- package/core/cli/command-registry.ts +44 -46
- package/core/cli/commands/build.ts +11 -6
- package/core/cli/commands/create.ts +7 -5
- package/core/cli/commands/dev.ts +6 -5
- package/core/cli/commands/help.ts +3 -2
- package/core/cli/commands/make-plugin.ts +8 -7
- package/core/cli/commands/plugin-add.ts +60 -43
- package/core/cli/commands/plugin-deps.ts +73 -57
- package/core/cli/commands/plugin-list.ts +44 -41
- package/core/cli/commands/plugin-remove.ts +33 -22
- package/core/cli/generators/component.ts +770 -769
- package/core/cli/generators/controller.ts +9 -8
- package/core/cli/generators/index.ts +148 -146
- package/core/cli/generators/interactive.ts +228 -227
- package/core/cli/generators/plugin.ts +11 -10
- package/core/cli/generators/prompts.ts +83 -82
- package/core/cli/generators/route.ts +7 -6
- package/core/cli/generators/service.ts +10 -9
- package/core/cli/generators/template-engine.ts +2 -1
- package/core/cli/generators/types.ts +7 -7
- package/core/cli/generators/utils.ts +191 -191
- package/core/cli/index.ts +9 -8
- package/core/cli/plugin-discovery.ts +2 -2
- package/core/client/hooks/useAuth.ts +48 -48
- package/core/client/index.ts +0 -16
- package/core/client/standalone.ts +18 -17
- package/core/client/state/createStore.ts +192 -192
- package/core/client/state/index.ts +14 -14
- package/core/config/index.ts +1 -0
- package/core/framework/client.ts +131 -131
- package/core/framework/index.ts +7 -7
- package/core/framework/server.ts +72 -112
- package/core/framework/types.ts +2 -2
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +6 -3
- package/core/plugins/built-in/monitoring/index.ts +110 -68
- package/core/plugins/built-in/static/index.ts +2 -2
- package/core/plugins/built-in/swagger/index.ts +9 -9
- package/core/plugins/built-in/vite/index.ts +3 -3
- package/core/plugins/built-in/vite/vite-dev.ts +3 -3
- package/core/plugins/config.ts +50 -47
- package/core/plugins/discovery.ts +10 -4
- package/core/plugins/executor.ts +2 -2
- package/core/plugins/index.ts +206 -203
- package/core/plugins/manager.ts +21 -20
- package/core/plugins/registry.ts +76 -12
- package/core/plugins/types.ts +14 -14
- package/core/server/framework.ts +3 -189
- package/core/server/live/auto-generated-components.ts +11 -35
- package/core/server/live/index.ts +41 -36
- package/core/server/live/websocket-plugin.ts +48 -3
- package/core/server/middleware/elysia-helpers.ts +16 -15
- package/core/server/middleware/errorHandling.ts +14 -14
- package/core/server/middleware/index.ts +31 -31
- package/core/server/plugins/database.ts +181 -180
- package/core/server/plugins/static-files-plugin.ts +4 -3
- package/core/server/plugins/swagger.ts +11 -8
- package/core/server/rooms/RoomBroadcaster.ts +11 -10
- package/core/server/rooms/RoomSystem.ts +14 -11
- package/core/server/services/BaseService.ts +7 -7
- package/core/server/services/ServiceContainer.ts +5 -5
- package/core/server/services/index.ts +8 -8
- package/core/templates/create-project.ts +28 -27
- package/core/testing/index.ts +9 -9
- package/core/testing/setup.ts +73 -73
- package/core/types/api.ts +168 -168
- package/core/types/config.ts +5 -5
- package/core/types/index.ts +1 -1
- package/core/types/plugin.ts +2 -2
- package/core/types/types.ts +3 -3
- package/core/utils/build-logger.ts +324 -324
- package/core/utils/config-schema.ts +480 -480
- package/core/utils/env.ts +10 -8
- package/core/utils/errors/codes.ts +114 -114
- package/core/utils/errors/handlers.ts +30 -20
- package/core/utils/errors/index.ts +54 -46
- package/core/utils/errors/middleware.ts +113 -113
- package/core/utils/helpers.ts +19 -16
- package/core/utils/logger/colors.ts +114 -114
- package/core/utils/logger/config.ts +2 -2
- package/core/utils/logger/formatter.ts +82 -82
- package/core/utils/logger/group-logger.ts +101 -101
- package/core/utils/logger/index.ts +13 -3
- package/core/utils/logger/startup-banner.ts +2 -2
- package/core/utils/logger/winston-logger.ts +152 -152
- package/core/utils/monitoring/index.ts +211 -211
- package/core/utils/sync-version.ts +67 -66
- package/core/utils/version.ts +1 -1
- package/package.json +11 -6
- package/playwright-report/index.html +85 -0
- package/playwright.config.ts +31 -0
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -302
- package/plugins/crypto-auth/client/components/index.ts +11 -11
- package/plugins/crypto-auth/client/index.ts +11 -11
- package/plugins/crypto-auth/package.json +65 -65
- package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +6 -5
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +6 -5
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +3 -3
- package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
- package/plugins/crypto-auth/server/middlewares.ts +19 -19
- package/tsconfig.json +4 -1
- package/vite.config.ts +13 -0
- package/app/client/.live-stubs/LiveAdminPanel.js +0 -5
- package/app/client/.live-stubs/LiveChat.js +0 -7
- package/app/client/.live-stubs/LiveCounter.js +0 -9
- package/app/client/.live-stubs/LiveForm.js +0 -11
- package/app/client/.live-stubs/LiveLocalCounter.js +0 -8
- package/app/client/.live-stubs/LiveRoomChat.js +0 -10
- package/app/client/.live-stubs/LiveTodoList.js +0 -9
- package/app/client/.live-stubs/LiveUpload.js +0 -15
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/client/src/live/TodoListDemo.tsx +0 -158
- package/app/server/live/LiveChat.ts +0 -78
- package/app/server/live/LiveTodoList.ts +0 -110
- package/app/server/live/register-components.ts +0 -19
- package/core/build/live-components-generator.ts +0 -312
- package/core/client/components/LiveDebugger.tsx +0 -1324
- package/core/live/ComponentRegistry.ts +0 -403
- package/core/live/types.ts +0 -241
- package/workspace.json +0 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to FluxStack will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.16.0] - 2026-03-13
|
|
6
|
+
|
|
7
|
+
### Major Refactor: Extract Live Components to Monorepo
|
|
8
|
+
|
|
9
|
+
Live Components code has been extracted from `core/` into standalone npm packages under the `@fluxstack/live` scope. This reduces the framework core by ~11,000 lines and allows the Live system to be versioned and published independently.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Live Components are now npm packages**: `@fluxstack/live`, `@fluxstack/live-client`, `@fluxstack/live-react`, `@fluxstack/live-elysia`
|
|
14
|
+
- `core/server/live/` reduced from full implementation to thin re-exports from `@fluxstack/live` and `@fluxstack/live-elysia`
|
|
15
|
+
- `core/client/` reduced from full implementation to re-exports from `@fluxstack/live-client` and `@fluxstack/live-react`
|
|
16
|
+
- Vite config now includes source aliases for `@fluxstack/live`, `@fluxstack/live-client`, and `@fluxstack/live-react` (frontend dev uses TypeScript source directly)
|
|
17
|
+
- Tests migrated to v0.3.0 API: `setLiveComponentContext` DI pattern replaces `vi.mock`, async flush for `WsSendBatcher`
|
|
18
|
+
- CI Bun version updated to 1.3.2
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Typed LiveRoom demos: `LivePingPong`, `LiveSharedCounter` with dedicated Room classes (`ChatRoom`, `CounterRoom`, `DirectoryRoom`, `PingRoom`)
|
|
23
|
+
- `PingPongDemo.tsx`, `SharedCounterDemo.tsx` — new frontend demo components
|
|
24
|
+
- `LLMD/resources/live-binary-delta.md` — binary delta codec documentation
|
|
25
|
+
- `plugins/*/bun.lock` added to `.gitignore`
|
|
26
|
+
- Bundler now logs stdout/stderr on build failure for CI debugging
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- `core/server/live/ComponentRegistry.ts`, `WebSocketConnectionManager.ts`, `StateSignature.ts`, `LiveRoomManager.ts`, `RoomEventBus.ts`, `RoomStateManager.ts`, `FileUploadManager.ts`, `LiveComponentPerformanceMonitor.ts`, `LiveDebugger.ts`, `LiveLogger.ts` — moved to `@fluxstack/live`
|
|
31
|
+
- `core/server/live/auth/` — moved to `@fluxstack/live`
|
|
32
|
+
- `core/server/live/__tests__/` — moved to `fluxstack-live` monorepo
|
|
33
|
+
- `core/client/LiveComponentsProvider.tsx`, `Live.tsx`, `LiveDebugger.tsx` — moved to `@fluxstack/live-react`
|
|
34
|
+
- `core/client/hooks/useLiveComponent.ts`, `useRoom.ts`, `useRoomProxy.ts`, `useLiveDebugger.ts`, `useChunkedUpload.ts`, `useLiveChunkedUpload.ts`, `AdaptiveChunkSizer.ts`, `state-validator.ts` — moved to `@fluxstack/live-client`
|
|
35
|
+
- `core/build/vite-plugin-live-strip.ts` — moved to `@fluxstack/live`
|
|
36
|
+
- `LiveDebugger` UI and exports (removed entirely, not extracted)
|
|
37
|
+
- `LiveChat` and `LiveTodoList` demo components (replaced by new typed demos)
|
|
38
|
+
- `ChatDemo.tsx`, `TodoListDemo.tsx`, `LiveDebuggerPanel.tsx` — replaced by new demos
|
|
39
|
+
- `workspace.json` — stale config referencing non-existent `./packages/*`
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Bun bundler failing on Linux CI with `"Could not resolve: @fluxstack/live"` — caused by `"bun"` export condition in `@fluxstack/live@0.3.0` pointing to non-existent `src/` (fixed in `@fluxstack/live@0.3.1`)
|
|
44
|
+
- `live-components-generator.ts` basename extraction bug
|
|
45
|
+
- Vite aliases made conditional for CI compatibility
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## [1.14.0] - 2026-02-15
|
|
50
|
+
|
|
51
|
+
### Security
|
|
52
|
+
|
|
53
|
+
- Harden Live Components against critical vulnerabilities: action whitelist enforcement, state prototype pollution guard, WebSocket message size limits, rate limiting
|
|
54
|
+
- Add `$private` server-only state with `TPrivate` generic for type-safe private data
|
|
55
|
+
- `ExtractActions` type now respects `publicActions` whitelist
|
|
56
|
+
- Harden static-files plugin with security headers, path traversal protection, streaming support
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **Live Component Debugger**: draggable floating window for real-time component inspection with rooms tab, collapsible JSON tree, settings panel (font size, compact mode, word wrap)
|
|
61
|
+
- Server-controlled debug activation (`DEBUG_LIVE` env var, defaults to `false`)
|
|
62
|
+
- Live component logs forwarded to debug panel as `LOG` events
|
|
63
|
+
- Two-channel logging architecture (LiveLogger for structured logs)
|
|
64
|
+
- Dynamic favicon and page title that change per route
|
|
65
|
+
- FluxStack logo in navbar with route-based hue shift and Live Docs button
|
|
66
|
+
- Mobile responsive layout with floating logo and adaptive sizing
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- Prevent LiveDebugger from being imported in client-side bundle
|
|
71
|
+
- Normalize path separators in static file serving on Windows
|
|
72
|
+
- Handle undefined type from reactive config with nullish coalesce
|
|
73
|
+
- `setValue` added to `LiveForm.publicActions` to enable `$field()` sync
|
|
74
|
+
- Preserve active tab when selecting component in debugger
|
|
75
|
+
- Stop click propagation on expanded event details
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- Static-files plugin refactored to use Bun-native APIs instead of Node `fs`
|
|
80
|
+
- Debug config read from FluxStack config system instead of raw env vars
|
package/LLMD/INDEX.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# FluxStack LLM Documentation
|
|
2
2
|
|
|
3
|
-
**Version:**
|
|
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) -
|
|
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)
|