create-fluxstack 1.20.1 → 1.21.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/LLMD/resources/live-components.md +103 -57
- package/LLMD/resources/live-rooms.md +187 -88
- package/README.md +27 -25
- package/app/client/.live-stubs/LiveCounter.js +4 -4
- package/app/client/src/App.tsx +11 -12
- package/app/client/src/components/AppLayout.tsx +290 -252
- package/app/client/src/components/BackButton.tsx +16 -13
- package/app/client/src/components/DemoPage.tsx +135 -22
- package/app/client/src/index.css +21 -11
- package/app/client/src/live/AuthDemo.tsx +270 -333
- package/app/client/src/live/CounterDemo.tsx +151 -206
- package/app/client/src/live/FormDemo.tsx +140 -119
- package/app/client/src/live/PingPongDemo.tsx +180 -202
- package/app/client/src/live/RoomChatDemo.tsx +397 -374
- package/app/client/src/pages/HomePage.tsx +170 -104
- package/app/server/live/LiveCounter.ts +71 -68
- package/app/server/live/LiveSharedCounter.ts +18 -12
- package/app/server/live/auto-generated-components.ts +1 -3
- package/app/server/live/rooms/CounterRoom.ts +15 -10
- package/core/client/index.ts +0 -3
- package/core/client/state/createStore.ts +88 -88
- package/core/client/state/index.ts +5 -5
- package/core/server/live/auto-generated-components.ts +1 -3
- package/core/utils/version.ts +1 -1
- package/package.json +1 -1
- package/tsconfig.json +7 -6
- package/app/client/src/components/LiveUploadWidget.tsx +0 -200
- package/app/client/src/live/UploadDemo.tsx +0 -21
- package/app/server/live/LiveUpload.ts +0 -96
- package/core/client/hooks/useLiveUpload.ts +0 -70
|
@@ -72,11 +72,12 @@ export class LiveLocalCounter extends LiveComponent<typeof LiveLocalCounter.defa
|
|
|
72
72
|
6. **Mandatory `publicActions`** - Components without it deny ALL remote actions (secure by default)
|
|
73
73
|
7. **Client link** - `import type { Demo as _Client }` enables Ctrl+Click in IDE
|
|
74
74
|
|
|
75
|
-
### With
|
|
75
|
+
### With Typed Rooms (Advanced)
|
|
76
76
|
|
|
77
77
|
```typescript
|
|
78
78
|
// app/server/live/LiveCounter.ts
|
|
79
79
|
import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
|
|
80
|
+
import { CounterRoom } from './rooms/CounterRoom'
|
|
80
81
|
|
|
81
82
|
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
82
83
|
static componentName = 'LiveCounter'
|
|
@@ -86,9 +87,11 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
|
|
|
86
87
|
lastUpdatedBy: null as string | null,
|
|
87
88
|
connectedUsers: 0
|
|
88
89
|
}
|
|
89
|
-
protected roomType = 'counter'
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
private roomId: string
|
|
92
|
+
private unsubscribeCounter: (() => void) | null = null
|
|
93
|
+
private unsubscribePresence: (() => void) | null = null
|
|
94
|
+
|
|
92
95
|
constructor(
|
|
93
96
|
initialState: Partial<typeof LiveCounter.defaultState> = {},
|
|
94
97
|
ws: FluxStackWebSocket,
|
|
@@ -96,59 +99,90 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
|
|
|
96
99
|
) {
|
|
97
100
|
super(initialState, ws, options)
|
|
98
101
|
|
|
99
|
-
this.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
this.roomId = options?.room ?? 'default'
|
|
103
|
+
const room = this.$room(CounterRoom, this.roomId)
|
|
104
|
+
room.join()
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
// Load authoritative room state for new joiners.
|
|
107
|
+
this.setState({
|
|
108
|
+
count: room.state.count,
|
|
109
|
+
lastUpdatedBy: room.state.lastUpdatedBy,
|
|
110
|
+
connectedUsers: room.state.onlineCount
|
|
105
111
|
})
|
|
106
112
|
|
|
107
|
-
this.
|
|
108
|
-
|
|
113
|
+
this.unsubscribeCounter = room.on('counter:updated', (data) => {
|
|
114
|
+
this.setState({
|
|
115
|
+
count: data.count,
|
|
116
|
+
lastUpdatedBy: data.updatedBy
|
|
117
|
+
})
|
|
118
|
+
})
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
{ connectedUsers: newCount },
|
|
114
|
-
{ connectedUsers: newCount }
|
|
115
|
-
)
|
|
120
|
+
this.unsubscribePresence = room.on('presence:changed', (data) => {
|
|
121
|
+
this.setState({ connectedUsers: data.onlineCount })
|
|
122
|
+
})
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
async increment() {
|
|
119
|
-
const
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
{ count: newCount, lastUpdatedBy: this.userId || 'anonymous' }
|
|
123
|
-
)
|
|
124
|
-
return { success: true, count: newCount }
|
|
126
|
+
const room = this.$room(CounterRoom, this.roomId)
|
|
127
|
+
const count = room.increment(this.userId || 'anonymous')
|
|
128
|
+
return { success: true, count }
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
async decrement() {
|
|
128
|
-
const
|
|
129
|
-
this.
|
|
130
|
-
|
|
131
|
-
{ count: newCount, lastUpdatedBy: this.userId || 'anonymous' }
|
|
132
|
-
)
|
|
133
|
-
return { success: true, count: newCount }
|
|
132
|
+
const room = this.$room(CounterRoom, this.roomId)
|
|
133
|
+
const count = room.decrement(this.userId || 'anonymous')
|
|
134
|
+
return { success: true, count }
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
async reset() {
|
|
137
|
-
this
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
return { success: true, count: 0 }
|
|
138
|
+
const room = this.$room(CounterRoom, this.roomId)
|
|
139
|
+
const count = room.reset(this.userId || 'anonymous')
|
|
140
|
+
return { success: true, count }
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
destroy() {
|
|
145
|
-
|
|
146
|
-
this.
|
|
144
|
+
this.unsubscribeCounter?.()
|
|
145
|
+
this.unsubscribePresence?.()
|
|
147
146
|
super.destroy()
|
|
148
147
|
}
|
|
149
148
|
}
|
|
150
149
|
```
|
|
151
150
|
|
|
151
|
+
Presence must be tracked by authoritative room state, not by incrementing each component instance's local `connectedUsers`. A new tab creates a new component instance with local state starting at `0`, so local arithmetic can publish the wrong count. Put membership accounting in the `LiveRoom` lifecycle:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// app/server/live/rooms/CounterRoom.ts
|
|
155
|
+
interface CounterEvents {
|
|
156
|
+
'counter:updated': { count: number; updatedBy: string }
|
|
157
|
+
'presence:changed': { onlineCount: number }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class CounterRoom extends LiveRoom<CounterState, {}, CounterEvents> {
|
|
161
|
+
static roomName = 'counter'
|
|
162
|
+
static defaultState = { count: 0, lastUpdatedBy: null, onlineCount: 0 }
|
|
163
|
+
static defaultMeta = {}
|
|
164
|
+
|
|
165
|
+
onJoin() {
|
|
166
|
+
const onlineCount = this.state.onlineCount + 1
|
|
167
|
+
this.setState({ onlineCount })
|
|
168
|
+
this.emit('presence:changed', { onlineCount })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
onLeave() {
|
|
172
|
+
const onlineCount = Math.max(0, this.state.onlineCount - 1)
|
|
173
|
+
this.setState({ onlineCount })
|
|
174
|
+
this.emit('presence:changed', { onlineCount })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
increment(username: string) {
|
|
178
|
+
const count = this.state.count + 1
|
|
179
|
+
this.setState({ count, lastUpdatedBy: username })
|
|
180
|
+
this.emit('counter:updated', { count, updatedBy: username })
|
|
181
|
+
return count
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
152
186
|
## Lifecycle Hooks
|
|
153
187
|
|
|
154
188
|
The `@fluxstack/live` framework provides a full lifecycle hook system. All hooks are **optional** -- override only what you need. The example components in `app/server/live/` do not use all of them, but they are all available in the framework API.
|
|
@@ -274,7 +308,7 @@ Rehydration (reconnect with saved state):
|
|
|
274
308
|
|
|
275
309
|
- All hooks are optional -- override only what you need
|
|
276
310
|
- All hook errors are caught and logged -- they never break the system
|
|
277
|
-
- Constructor is still needed ONLY for `this.onRoomEvent()`
|
|
311
|
+
- Constructor is still needed ONLY for room setup/subscriptions (`this.$room(...).join()`, `room.on(...)`, or legacy `this.onRoomEvent()`)
|
|
278
312
|
- All hooks are in BLOCKED_ACTIONS -- clients cannot call them remotely
|
|
279
313
|
|
|
280
314
|
## Custom ID Generator
|
|
@@ -597,6 +631,7 @@ export class LiveSharedCounter extends LiveComponent<typeof LiveSharedCounter.de
|
|
|
597
631
|
}
|
|
598
632
|
|
|
599
633
|
private counterUnsub: (() => void) | null = null
|
|
634
|
+
private presenceUnsub: (() => void) | null = null
|
|
600
635
|
|
|
601
636
|
constructor(initialState: Partial<typeof LiveSharedCounter.defaultState> = {}, ws: FluxStackWebSocket, options?: { room?: string; userId?: string }) {
|
|
602
637
|
super(initialState, ws, options)
|
|
@@ -615,6 +650,10 @@ export class LiveSharedCounter extends LiveComponent<typeof LiveSharedCounter.de
|
|
|
615
650
|
this.counterUnsub = room.on('counter:updated', (data) => {
|
|
616
651
|
this.setState({ count: data.count, lastUpdatedBy: data.updatedBy })
|
|
617
652
|
})
|
|
653
|
+
|
|
654
|
+
this.presenceUnsub = room.on('presence:changed', (data) => {
|
|
655
|
+
this.setState({ onlineCount: data.onlineCount })
|
|
656
|
+
})
|
|
618
657
|
}
|
|
619
658
|
|
|
620
659
|
async increment() {
|
|
@@ -625,6 +664,7 @@ export class LiveSharedCounter extends LiveComponent<typeof LiveSharedCounter.de
|
|
|
625
664
|
|
|
626
665
|
destroy() {
|
|
627
666
|
this.counterUnsub?.()
|
|
667
|
+
this.presenceUnsub?.()
|
|
628
668
|
super.destroy()
|
|
629
669
|
}
|
|
630
670
|
}
|
|
@@ -893,11 +933,11 @@ All components in same room receive events:
|
|
|
893
933
|
```typescript
|
|
894
934
|
// User A increments
|
|
895
935
|
await counter.increment()
|
|
896
|
-
//
|
|
936
|
+
// Calls CounterRoom.increment(), which updates room state and emits counter:updated
|
|
897
937
|
|
|
898
938
|
// User B's component receives event
|
|
899
|
-
|
|
900
|
-
this.setState({ count: data.count })
|
|
939
|
+
const unsub = room.on('counter:updated', (data) => {
|
|
940
|
+
this.setState({ count: data.count, lastUpdatedBy: data.updatedBy })
|
|
901
941
|
})
|
|
902
942
|
// User B sees updated count
|
|
903
943
|
```
|
|
@@ -907,25 +947,31 @@ this.onRoomEvent('COUNT_CHANGED', (data) => {
|
|
|
907
947
|
Track connected users in room:
|
|
908
948
|
|
|
909
949
|
```typescript
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
this.emitRoomEventWithState('USER_COUNT_CHANGED',
|
|
916
|
-
{ connectedUsers: newCount },
|
|
917
|
-
{ connectedUsers: newCount }
|
|
918
|
-
)
|
|
950
|
+
// In the typed room class, not in each component instance.
|
|
951
|
+
onJoin() {
|
|
952
|
+
const onlineCount = this.state.onlineCount + 1
|
|
953
|
+
this.setState({ onlineCount })
|
|
954
|
+
this.emit('presence:changed', { onlineCount })
|
|
919
955
|
}
|
|
920
956
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
this.
|
|
925
|
-
|
|
957
|
+
onLeave() {
|
|
958
|
+
const onlineCount = Math.max(0, this.state.onlineCount - 1)
|
|
959
|
+
this.setState({ onlineCount })
|
|
960
|
+
this.emit('presence:changed', { onlineCount })
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// In the component constructor.
|
|
964
|
+
const room = this.$room(CounterRoom, options?.room ?? 'default')
|
|
965
|
+
room.join()
|
|
966
|
+
this.setState({ connectedUsers: room.state.onlineCount })
|
|
967
|
+
|
|
968
|
+
const unsub = room.on('presence:changed', (data) => {
|
|
969
|
+
this.setState({ connectedUsers: data.onlineCount })
|
|
926
970
|
}
|
|
927
971
|
```
|
|
928
972
|
|
|
973
|
+
Do not calculate presence with `this.state.connectedUsers + 1` inside each component. Each browser tab mounts a separate component instance, so local state starts from its own value and can publish stale counts. Use room state (`room.state.onlineCount`) as the source of truth.
|
|
974
|
+
|
|
929
975
|
## Error Handling
|
|
930
976
|
|
|
931
977
|
```typescript
|
|
@@ -968,8 +1014,8 @@ The app includes these live components in `app/server/live/`:
|
|
|
968
1014
|
| Component | Description | Features |
|
|
969
1015
|
|-----------|-------------|----------|
|
|
970
1016
|
| `LiveLocalCounter` | Simple counter, no room events | Direct state access, `declare` |
|
|
971
|
-
| `LiveCounter` | Shared counter
|
|
972
|
-
| `LiveSharedCounter` | Shared counter using typed `CounterRoom` | `$room(CounterRoom, 'global')` |
|
|
1017
|
+
| `LiveCounter` | Shared counter using typed `CounterRoom` | `$room(CounterRoom, roomId)`, `presence:changed` |
|
|
1018
|
+
| `LiveSharedCounter` | Shared counter using typed `CounterRoom` | `$room(CounterRoom, 'global')`, `presence:changed` |
|
|
973
1019
|
| `LiveForm` | Reactive form with server validation | `setValue`, `validate`, `submit` |
|
|
974
1020
|
| `LivePingPong` | Binary codec demo (msgpack) | Typed `PingRoom`, round-trip timing |
|
|
975
1021
|
| `LiveRoomChat` | Multi-room chat with directory | `ChatRoom`, `DirectoryRoom`, password rooms |
|
|
@@ -981,7 +1027,7 @@ The app includes these live components in `app/server/live/`:
|
|
|
981
1027
|
|
|
982
1028
|
```
|
|
983
1029
|
app/server/live/
|
|
984
|
-
├── LiveCounter.ts # Shared counter
|
|
1030
|
+
├── LiveCounter.ts # Shared counter using typed CounterRoom
|
|
985
1031
|
├── LiveLocalCounter.ts # Local counter (no room)
|
|
986
1032
|
├── LiveForm.ts # Reactive form
|
|
987
1033
|
├── LivePingPong.ts # Binary codec demo
|
|
@@ -1038,7 +1084,7 @@ export class MyComponent extends LiveComponent<typeof MyComponent.defaultState>
|
|
|
1038
1084
|
- Use `declare` for each state property (TypeScript type hint)
|
|
1039
1085
|
- Use `onMount()` for async initialization (rooms, auth, data fetching)
|
|
1040
1086
|
- Use `onDestroy()` for cleanup (timers, connections) -- sync only
|
|
1041
|
-
-
|
|
1087
|
+
- Prefer typed `LiveRoom` state/methods for shared state and presence; use `emitRoomEventWithState` only for simple legacy untyped-room events
|
|
1042
1088
|
- Handle errors in actions (throw Error)
|
|
1043
1089
|
- Add client link: `import type { Demo as _Client } from '@client/...'`
|
|
1044
1090
|
- Use `$persistent` for data that should survive HMR reloads
|