redux-cluster-ws 2.0.0 → 2.0.2
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/package.json +8 -3
- package/FUNDING.yml +0 -7
- package/eslint.config.js +0 -143
- package/examples/browser-example.cjs +0 -350
- package/examples/browser.html +0 -255
- package/examples/client.cjs +0 -155
- package/examples/cross-library-browser.html +0 -655
- package/examples/cross-library-client.cjs +0 -190
- package/examples/cross-library-server.cjs +0 -213
- package/examples/server.cjs +0 -96
|
@@ -1,655 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Cross-library Demo - Browser Client</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
10
|
-
max-width: 1200px;
|
|
11
|
-
margin: 0 auto;
|
|
12
|
-
padding: 20px;
|
|
13
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
14
|
-
min-height: 100vh;
|
|
15
|
-
color: #333;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.container {
|
|
19
|
-
background: white;
|
|
20
|
-
border-radius: 12px;
|
|
21
|
-
padding: 30px;
|
|
22
|
-
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
h1 {
|
|
26
|
-
color: #4a5568;
|
|
27
|
-
text-align: center;
|
|
28
|
-
margin-bottom: 30px;
|
|
29
|
-
font-size: 2.5em;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.status {
|
|
33
|
-
text-align: center;
|
|
34
|
-
padding: 15px;
|
|
35
|
-
border-radius: 8px;
|
|
36
|
-
margin-bottom: 20px;
|
|
37
|
-
font-weight: bold;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.connected {
|
|
41
|
-
background: #d4edda;
|
|
42
|
-
color: #155724;
|
|
43
|
-
border: 1px solid #c3e6cb;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.disconnected {
|
|
47
|
-
background: #f8d7da;
|
|
48
|
-
color: #721c24;
|
|
49
|
-
border: 1px solid #f5c6cb;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.section {
|
|
53
|
-
margin: 20px 0;
|
|
54
|
-
padding: 20px;
|
|
55
|
-
border: 1px solid #e2e8f0;
|
|
56
|
-
border-radius: 8px;
|
|
57
|
-
background: #f8fafc;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.section h3 {
|
|
61
|
-
margin-top: 0;
|
|
62
|
-
color: #2d3748;
|
|
63
|
-
display: flex;
|
|
64
|
-
align-items: center;
|
|
65
|
-
gap: 10px;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.add-todo {
|
|
69
|
-
display: flex;
|
|
70
|
-
gap: 10px;
|
|
71
|
-
margin-bottom: 20px;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.add-todo input {
|
|
75
|
-
flex: 1;
|
|
76
|
-
padding: 12px;
|
|
77
|
-
border: 1px solid #cbd5e0;
|
|
78
|
-
border-radius: 6px;
|
|
79
|
-
font-size: 14px;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.add-todo button {
|
|
83
|
-
padding: 12px 20px;
|
|
84
|
-
background: #4299e1;
|
|
85
|
-
color: white;
|
|
86
|
-
border: none;
|
|
87
|
-
border-radius: 6px;
|
|
88
|
-
cursor: pointer;
|
|
89
|
-
font-size: 14px;
|
|
90
|
-
font-weight: 500;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.add-todo button:hover {
|
|
94
|
-
background: #3182ce;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.add-todo button:disabled {
|
|
98
|
-
background: #a0aec0;
|
|
99
|
-
cursor: not-allowed;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.todo-list {
|
|
103
|
-
max-height: 300px;
|
|
104
|
-
overflow-y: auto;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.todo-item {
|
|
108
|
-
display: flex;
|
|
109
|
-
align-items: center;
|
|
110
|
-
gap: 10px;
|
|
111
|
-
padding: 10px;
|
|
112
|
-
border-bottom: 1px solid #e2e8f0;
|
|
113
|
-
transition: background 0.2s;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.todo-item:hover {
|
|
117
|
-
background: #edf2f7;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.todo-item.completed {
|
|
121
|
-
opacity: 0.6;
|
|
122
|
-
text-decoration: line-through;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.todo-toggle {
|
|
126
|
-
width: 20px;
|
|
127
|
-
height: 20px;
|
|
128
|
-
cursor: pointer;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.user-list {
|
|
132
|
-
display: grid;
|
|
133
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
134
|
-
gap: 10px;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.user-item {
|
|
138
|
-
padding: 10px;
|
|
139
|
-
background: white;
|
|
140
|
-
border: 1px solid #e2e8f0;
|
|
141
|
-
border-radius: 6px;
|
|
142
|
-
display: flex;
|
|
143
|
-
align-items: center;
|
|
144
|
-
gap: 8px;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.user-cluster {
|
|
148
|
-
border-left: 4px solid #ed8936;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.user-web {
|
|
152
|
-
border-left: 4px solid #38b2ac;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.stats {
|
|
156
|
-
display: grid;
|
|
157
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
158
|
-
gap: 15px;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.stat-card {
|
|
162
|
-
text-align: center;
|
|
163
|
-
padding: 20px;
|
|
164
|
-
background: white;
|
|
165
|
-
border-radius: 8px;
|
|
166
|
-
border: 1px solid #e2e8f0;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.stat-number {
|
|
170
|
-
font-size: 2em;
|
|
171
|
-
font-weight: bold;
|
|
172
|
-
color: #4299e1;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.stat-label {
|
|
176
|
-
color: #718096;
|
|
177
|
-
margin-top: 5px;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.log {
|
|
181
|
-
background: #1a202c;
|
|
182
|
-
color: #e2e8f0;
|
|
183
|
-
padding: 15px;
|
|
184
|
-
border-radius: 6px;
|
|
185
|
-
font-family: "Courier New", monospace;
|
|
186
|
-
font-size: 12px;
|
|
187
|
-
max-height: 200px;
|
|
188
|
-
overflow-y: auto;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
.log-entry {
|
|
192
|
-
margin: 2px 0;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.architecture-note {
|
|
196
|
-
background: #ebf8ff;
|
|
197
|
-
border: 1px solid #bee3f8;
|
|
198
|
-
border-radius: 8px;
|
|
199
|
-
padding: 15px;
|
|
200
|
-
margin-bottom: 20px;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.architecture-note h4 {
|
|
204
|
-
color: #2b6cb0;
|
|
205
|
-
margin-top: 0;
|
|
206
|
-
}
|
|
207
|
-
</style>
|
|
208
|
-
</head>
|
|
209
|
-
<body>
|
|
210
|
-
<div class="container">
|
|
211
|
-
<h1>🔄 Cross-Library Demo</h1>
|
|
212
|
-
|
|
213
|
-
<div class="architecture-note">
|
|
214
|
-
<h4>🏗️ Architecture</h4>
|
|
215
|
-
<p>
|
|
216
|
-
This demo shows <strong>redux-cluster</strong> (IPC/TCP) working with
|
|
217
|
-
<strong>redux-cluster-ws</strong> (WebSocket):
|
|
218
|
-
</p>
|
|
219
|
-
<ul>
|
|
220
|
-
<li>
|
|
221
|
-
🔧 <strong>Server & Workers</strong>: Use redux-cluster for IPC/TCP
|
|
222
|
-
communication
|
|
223
|
-
</li>
|
|
224
|
-
<li>
|
|
225
|
-
🌐 <strong>Web Clients</strong>: Use redux-cluster-ws for WebSocket
|
|
226
|
-
communication
|
|
227
|
-
</li>
|
|
228
|
-
<li>
|
|
229
|
-
📊 <strong>Shared State</strong>: All participants see the same
|
|
230
|
-
Redux store state
|
|
231
|
-
</li>
|
|
232
|
-
<li>
|
|
233
|
-
⚡ <strong>Real-time</strong>: Changes sync instantly across all
|
|
234
|
-
connections
|
|
235
|
-
</li>
|
|
236
|
-
</ul>
|
|
237
|
-
</div>
|
|
238
|
-
|
|
239
|
-
<div id="status" class="status disconnected">
|
|
240
|
-
🔌 Connecting to server...
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
<div class="section">
|
|
244
|
-
<h3>📝 Add Todo</h3>
|
|
245
|
-
<div class="add-todo">
|
|
246
|
-
<input
|
|
247
|
-
type="text"
|
|
248
|
-
id="todoInput"
|
|
249
|
-
placeholder="Enter a new todo..."
|
|
250
|
-
disabled
|
|
251
|
-
/>
|
|
252
|
-
<button id="addButton" disabled>Add Todo</button>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
|
|
256
|
-
<div class="section">
|
|
257
|
-
<h3>📋 Todo List</h3>
|
|
258
|
-
<div id="todoList" class="todo-list">
|
|
259
|
-
<p>No todos yet. Add one above!</p>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
|
|
263
|
-
<div class="section">
|
|
264
|
-
<h3>👥 Connected Users</h3>
|
|
265
|
-
<div id="userList" class="user-list">
|
|
266
|
-
<p>Loading users...</p>
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
<div class="section">
|
|
271
|
-
<h3>📊 Statistics</h3>
|
|
272
|
-
<div class="stats">
|
|
273
|
-
<div class="stat-card">
|
|
274
|
-
<div class="stat-number" id="todoCount">0</div>
|
|
275
|
-
<div class="stat-label">Total Todos</div>
|
|
276
|
-
</div>
|
|
277
|
-
<div class="stat-card">
|
|
278
|
-
<div class="stat-number" id="userCount">0</div>
|
|
279
|
-
<div class="stat-label">Active Users</div>
|
|
280
|
-
</div>
|
|
281
|
-
<div class="stat-card">
|
|
282
|
-
<div class="stat-number" id="completedCount">0</div>
|
|
283
|
-
<div class="stat-label">Completed</div>
|
|
284
|
-
</div>
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
|
|
288
|
-
<div class="section">
|
|
289
|
-
<h3>📜 Activity Log</h3>
|
|
290
|
-
<div id="log" class="log">
|
|
291
|
-
<div class="log-entry">Initializing...</div>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
|
|
296
|
-
<script>
|
|
297
|
-
// Redux implementation
|
|
298
|
-
function createStore(reducer) {
|
|
299
|
-
let state = reducer(undefined, {});
|
|
300
|
-
let listeners = [];
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
getState: () => state,
|
|
304
|
-
dispatch: (action) => {
|
|
305
|
-
state = reducer(state, action);
|
|
306
|
-
listeners.forEach((listener) => listener());
|
|
307
|
-
return action;
|
|
308
|
-
},
|
|
309
|
-
subscribe: (listener) => {
|
|
310
|
-
listeners.push(listener);
|
|
311
|
-
return () => {
|
|
312
|
-
listeners = listeners.filter((l) => l !== listener);
|
|
313
|
-
};
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Todo reducer (same as server)
|
|
319
|
-
function todoReducer(
|
|
320
|
-
state = { todos: [], users: [], stats: { total: 0 } },
|
|
321
|
-
action
|
|
322
|
-
) {
|
|
323
|
-
switch (action.type) {
|
|
324
|
-
case "ADD_TODO":
|
|
325
|
-
return {
|
|
326
|
-
...state,
|
|
327
|
-
todos: [
|
|
328
|
-
...state.todos,
|
|
329
|
-
{
|
|
330
|
-
id: Date.now(),
|
|
331
|
-
text: action.payload.text,
|
|
332
|
-
completed: false,
|
|
333
|
-
createdBy: action.payload.user || "anonymous",
|
|
334
|
-
timestamp: new Date().toISOString(),
|
|
335
|
-
},
|
|
336
|
-
],
|
|
337
|
-
stats: { total: state.stats.total + 1 },
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
case "TOGGLE_TODO":
|
|
341
|
-
return {
|
|
342
|
-
...state,
|
|
343
|
-
todos: state.todos.map((todo) =>
|
|
344
|
-
todo.id === action.payload.id
|
|
345
|
-
? { ...todo, completed: !todo.completed }
|
|
346
|
-
: todo
|
|
347
|
-
),
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
case "ADD_USER":
|
|
351
|
-
const userExists = state.users.find(
|
|
352
|
-
(u) => u.name === action.payload.name
|
|
353
|
-
);
|
|
354
|
-
if (userExists) return state;
|
|
355
|
-
|
|
356
|
-
return {
|
|
357
|
-
...state,
|
|
358
|
-
users: [
|
|
359
|
-
...state.users,
|
|
360
|
-
{
|
|
361
|
-
id: Date.now(),
|
|
362
|
-
name: action.payload.name,
|
|
363
|
-
joinedAt: new Date().toISOString(),
|
|
364
|
-
type: action.payload.type || "web",
|
|
365
|
-
},
|
|
366
|
-
],
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
case "REMOVE_USER":
|
|
370
|
-
return {
|
|
371
|
-
...state,
|
|
372
|
-
users: state.users.filter((u) => u.id !== action.payload.id),
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
default:
|
|
376
|
-
return state;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// WebSocket client implementation
|
|
381
|
-
class WebSocketClient {
|
|
382
|
-
constructor(store, config) {
|
|
383
|
-
this.store = store;
|
|
384
|
-
this.config = config;
|
|
385
|
-
this.authenticated = false;
|
|
386
|
-
this.originalDispatch = store.dispatch;
|
|
387
|
-
|
|
388
|
-
// Hash credentials (simple version for demo)
|
|
389
|
-
this.login = this.simpleHash(`REDUX_CLUSTER${config.login}`);
|
|
390
|
-
this.password = this.simpleHash(`REDUX_CLUSTER${config.password}`);
|
|
391
|
-
|
|
392
|
-
this.store.dispatch = this.dispatch.bind(this);
|
|
393
|
-
this.connect();
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
simpleHash(str) {
|
|
397
|
-
let hash = 0;
|
|
398
|
-
for (let i = 0; i < str.length; i++) {
|
|
399
|
-
const char = str.charCodeAt(i);
|
|
400
|
-
hash = (hash << 5) - hash + char;
|
|
401
|
-
hash = hash & hash;
|
|
402
|
-
}
|
|
403
|
-
return hash.toString(16);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
connect() {
|
|
407
|
-
const url = `ws://${this.config.host.replace(/^ws:\/\//, "")}:${
|
|
408
|
-
this.config.port
|
|
409
|
-
}/redux-cluster-${this.store.RCHash}`;
|
|
410
|
-
log(`Connecting to: ${url}`);
|
|
411
|
-
|
|
412
|
-
this.ws = new WebSocket(url);
|
|
413
|
-
|
|
414
|
-
this.ws.onopen = () => {
|
|
415
|
-
log("WebSocket connected");
|
|
416
|
-
this.sendMessage({
|
|
417
|
-
_msg: "REDUX_CLUSTER_SOCKET_AUTH",
|
|
418
|
-
_hash: this.store.RCHash,
|
|
419
|
-
_login: this.login,
|
|
420
|
-
_password: this.password,
|
|
421
|
-
});
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
this.ws.onmessage = (event) => {
|
|
425
|
-
try {
|
|
426
|
-
const message = JSON.parse(event.data);
|
|
427
|
-
this.handleMessage(message);
|
|
428
|
-
} catch (error) {
|
|
429
|
-
log(`Message parse error: ${error.message}`);
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
this.ws.onclose = () => {
|
|
434
|
-
log("WebSocket disconnected");
|
|
435
|
-
this.authenticated = false;
|
|
436
|
-
this.store.connected = false;
|
|
437
|
-
updateConnectionStatus(false);
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
this.ws.onerror = (error) => {
|
|
441
|
-
log(`WebSocket error: ${error}`);
|
|
442
|
-
this.authenticated = false;
|
|
443
|
-
this.store.connected = false;
|
|
444
|
-
updateConnectionStatus(false);
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
handleMessage(message) {
|
|
449
|
-
if (message._hash !== this.store.RCHash) return;
|
|
450
|
-
|
|
451
|
-
switch (message._msg) {
|
|
452
|
-
case "REDUX_CLUSTER_MSGTOWORKER":
|
|
453
|
-
if (message._action) {
|
|
454
|
-
log(`Received action: ${message._action.type}`);
|
|
455
|
-
this.originalDispatch(message._action);
|
|
456
|
-
}
|
|
457
|
-
break;
|
|
458
|
-
|
|
459
|
-
case "REDUX_CLUSTER_SOCKET_AUTHSTATE":
|
|
460
|
-
if (message._value === true) {
|
|
461
|
-
this.authenticated = true;
|
|
462
|
-
this.store.connected = true;
|
|
463
|
-
updateConnectionStatus(true);
|
|
464
|
-
log("Authentication successful");
|
|
465
|
-
|
|
466
|
-
this.sendMessage({
|
|
467
|
-
_msg: "REDUX_CLUSTER_START",
|
|
468
|
-
_hash: this.store.RCHash,
|
|
469
|
-
});
|
|
470
|
-
} else {
|
|
471
|
-
this.authenticated = false;
|
|
472
|
-
this.store.connected = false;
|
|
473
|
-
updateConnectionStatus(false);
|
|
474
|
-
log("Authentication failed");
|
|
475
|
-
}
|
|
476
|
-
break;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
dispatch(action) {
|
|
481
|
-
try {
|
|
482
|
-
if (
|
|
483
|
-
this.ws &&
|
|
484
|
-
this.ws.readyState === WebSocket.OPEN &&
|
|
485
|
-
this.authenticated
|
|
486
|
-
) {
|
|
487
|
-
log(`Sending action: ${action.type}`);
|
|
488
|
-
this.sendMessage({
|
|
489
|
-
_msg: "REDUX_CLUSTER_MSGTOMASTER",
|
|
490
|
-
_hash: this.store.RCHash,
|
|
491
|
-
_action: action,
|
|
492
|
-
});
|
|
493
|
-
} else {
|
|
494
|
-
log("Cannot dispatch: not connected");
|
|
495
|
-
}
|
|
496
|
-
} catch (error) {
|
|
497
|
-
log(`Dispatch error: ${error.message}`);
|
|
498
|
-
}
|
|
499
|
-
return action;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
sendMessage(message) {
|
|
503
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
504
|
-
this.ws.send(JSON.stringify(message));
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// UI functions
|
|
510
|
-
const elements = {
|
|
511
|
-
status: document.getElementById("status"),
|
|
512
|
-
todoInput: document.getElementById("todoInput"),
|
|
513
|
-
addButton: document.getElementById("addButton"),
|
|
514
|
-
todoList: document.getElementById("todoList"),
|
|
515
|
-
userList: document.getElementById("userList"),
|
|
516
|
-
todoCount: document.getElementById("todoCount"),
|
|
517
|
-
userCount: document.getElementById("userCount"),
|
|
518
|
-
completedCount: document.getElementById("completedCount"),
|
|
519
|
-
log: document.getElementById("log"),
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
function log(message) {
|
|
523
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
524
|
-
const entry = document.createElement("div");
|
|
525
|
-
entry.className = "log-entry";
|
|
526
|
-
entry.textContent = `[${timestamp}] ${message}`;
|
|
527
|
-
elements.log.appendChild(entry);
|
|
528
|
-
elements.log.scrollTop = elements.log.scrollHeight;
|
|
529
|
-
console.log(message);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function updateConnectionStatus(connected) {
|
|
533
|
-
if (connected) {
|
|
534
|
-
elements.status.textContent = "✅ Connected to cross-library server";
|
|
535
|
-
elements.status.className = "status connected";
|
|
536
|
-
elements.todoInput.disabled = false;
|
|
537
|
-
elements.addButton.disabled = false;
|
|
538
|
-
} else {
|
|
539
|
-
elements.status.textContent = "❌ Disconnected from server";
|
|
540
|
-
elements.status.className = "status disconnected";
|
|
541
|
-
elements.todoInput.disabled = true;
|
|
542
|
-
elements.addButton.disabled = true;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function updateUI() {
|
|
547
|
-
const state = store.getState();
|
|
548
|
-
|
|
549
|
-
// Update stats
|
|
550
|
-
elements.todoCount.textContent = state.todos.length;
|
|
551
|
-
elements.userCount.textContent = state.users.length;
|
|
552
|
-
elements.completedCount.textContent = state.todos.filter(
|
|
553
|
-
(t) => t.completed
|
|
554
|
-
).length;
|
|
555
|
-
|
|
556
|
-
// Update todo list
|
|
557
|
-
if (state.todos.length === 0) {
|
|
558
|
-
elements.todoList.innerHTML = "<p>No todos yet. Add one above!</p>";
|
|
559
|
-
} else {
|
|
560
|
-
elements.todoList.innerHTML = state.todos
|
|
561
|
-
.map(
|
|
562
|
-
(todo) => `
|
|
563
|
-
<div class="todo-item ${todo.completed ? "completed" : ""}">
|
|
564
|
-
<input type="checkbox" class="todo-toggle"
|
|
565
|
-
${todo.completed ? "checked" : ""}
|
|
566
|
-
onchange="toggleTodo(${todo.id})">
|
|
567
|
-
<span>"${todo.text}" by ${todo.createdBy}</span>
|
|
568
|
-
</div>
|
|
569
|
-
`
|
|
570
|
-
)
|
|
571
|
-
.join("");
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Update user list
|
|
575
|
-
if (state.users.length === 0) {
|
|
576
|
-
elements.userList.innerHTML = "<p>No users connected</p>";
|
|
577
|
-
} else {
|
|
578
|
-
elements.userList.innerHTML = state.users
|
|
579
|
-
.map(
|
|
580
|
-
(user) => `
|
|
581
|
-
<div class="user-item user-${user.type}">
|
|
582
|
-
<span>${user.type === "cluster" ? "🔧" : "🌐"}</span>
|
|
583
|
-
<span>${user.name}</span>
|
|
584
|
-
</div>
|
|
585
|
-
`
|
|
586
|
-
)
|
|
587
|
-
.join("");
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function addTodo() {
|
|
592
|
-
const text = elements.todoInput.value.trim();
|
|
593
|
-
if (text && store.connected) {
|
|
594
|
-
store.dispatch({
|
|
595
|
-
type: "ADD_TODO",
|
|
596
|
-
payload: {
|
|
597
|
-
text,
|
|
598
|
-
user: "Browser Client",
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
elements.todoInput.value = "";
|
|
602
|
-
log(`Added todo: "${text}"`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function toggleTodo(id) {
|
|
607
|
-
if (store.connected) {
|
|
608
|
-
store.dispatch({
|
|
609
|
-
type: "TOGGLE_TODO",
|
|
610
|
-
payload: { id },
|
|
611
|
-
});
|
|
612
|
-
log(`Toggled todo ${id}`);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Initialize app
|
|
617
|
-
const store = createStore(todoReducer);
|
|
618
|
-
store.connected = false;
|
|
619
|
-
store.RCHash = "cross-library-demo";
|
|
620
|
-
|
|
621
|
-
// Connect WebSocket
|
|
622
|
-
const wsClient = new WebSocketClient(store, {
|
|
623
|
-
host: "127.0.0.1",
|
|
624
|
-
port: 8890,
|
|
625
|
-
login: "web-client",
|
|
626
|
-
password: "web123",
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
// Subscribe to state changes
|
|
630
|
-
store.subscribe(updateUI);
|
|
631
|
-
|
|
632
|
-
// Event listeners
|
|
633
|
-
elements.addButton.addEventListener("click", addTodo);
|
|
634
|
-
elements.todoInput.addEventListener("keypress", (e) => {
|
|
635
|
-
if (e.key === "Enter") addTodo();
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
// Add user when connected
|
|
639
|
-
setTimeout(() => {
|
|
640
|
-
if (store.connected) {
|
|
641
|
-
store.dispatch({
|
|
642
|
-
type: "ADD_USER",
|
|
643
|
-
payload: { name: "Browser Client", type: "web" },
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
}, 1000);
|
|
647
|
-
|
|
648
|
-
// Make toggleTodo globally available
|
|
649
|
-
window.toggleTodo = toggleTodo;
|
|
650
|
-
|
|
651
|
-
log("Browser client initialized");
|
|
652
|
-
updateConnectionStatus(false);
|
|
653
|
-
</script>
|
|
654
|
-
</body>
|
|
655
|
-
</html>
|