@signe/room 2.9.4 → 3.0.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 (81) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/chunk-EUXUH3YW.js +15 -0
  3. package/dist/chunk-EUXUH3YW.js.map +1 -0
  4. package/dist/cloudflare/index.d.ts +71 -0
  5. package/dist/cloudflare/index.js +320 -0
  6. package/dist/cloudflare/index.js.map +1 -0
  7. package/dist/index.d.ts +65 -188
  8. package/dist/index.js +742 -146
  9. package/dist/index.js.map +1 -1
  10. package/dist/node/index.d.ts +164 -0
  11. package/dist/node/index.js +786 -0
  12. package/dist/node/index.js.map +1 -0
  13. package/dist/party-dNs-hqkq.d.ts +175 -0
  14. package/examples/cloudflare/README.md +62 -0
  15. package/examples/cloudflare/node_modules/.bin/tsc +17 -0
  16. package/examples/cloudflare/node_modules/.bin/tsserver +17 -0
  17. package/examples/cloudflare/node_modules/.bin/wrangler +17 -0
  18. package/examples/cloudflare/node_modules/.bin/wrangler2 +17 -0
  19. package/examples/cloudflare/package.json +24 -0
  20. package/examples/cloudflare/public/index.html +443 -0
  21. package/examples/cloudflare/src/index.ts +28 -0
  22. package/examples/cloudflare/src/room.ts +44 -0
  23. package/examples/cloudflare/tsconfig.json +10 -0
  24. package/examples/cloudflare/wrangler.jsonc +25 -0
  25. package/examples/node/README.md +57 -0
  26. package/examples/node/node_modules/.bin/tsc +17 -0
  27. package/examples/node/node_modules/.bin/tsserver +17 -0
  28. package/examples/node/node_modules/.bin/tsx +17 -0
  29. package/examples/node/package.json +23 -0
  30. package/examples/node/public/index.html +443 -0
  31. package/examples/node/room.ts +44 -0
  32. package/examples/node/server.sqlite.ts +52 -0
  33. package/examples/node/server.ts +51 -0
  34. package/examples/node/tsconfig.json +10 -0
  35. package/examples/node-game/README.md +66 -0
  36. package/examples/node-game/package.json +23 -0
  37. package/examples/node-game/public/index.html +705 -0
  38. package/examples/node-game/room.ts +145 -0
  39. package/examples/node-game/server.sqlite.ts +54 -0
  40. package/examples/node-game/server.ts +53 -0
  41. package/examples/node-game/tsconfig.json +10 -0
  42. package/examples/node-shard/README.md +32 -0
  43. package/examples/node-shard/dev.ts +39 -0
  44. package/examples/node-shard/package.json +24 -0
  45. package/examples/node-shard/public/index.html +777 -0
  46. package/examples/node-shard/room-server.ts +68 -0
  47. package/examples/node-shard/room.ts +105 -0
  48. package/examples/node-shard/shared.ts +6 -0
  49. package/examples/node-shard/tsconfig.json +14 -0
  50. package/examples/node-shard/world-server.ts +169 -0
  51. package/package.json +14 -5
  52. package/readme.md +377 -11
  53. package/src/cloudflare/index.ts +474 -0
  54. package/src/mock.ts +29 -7
  55. package/src/node/index.ts +1112 -0
  56. package/src/server.ts +626 -90
  57. package/src/session.guard.ts +6 -2
  58. package/src/shard.ts +91 -23
  59. package/src/storage.ts +29 -5
  60. package/src/testing.ts +4 -3
  61. package/src/types/party.ts +4 -1
  62. package/src/world.guard.ts +23 -4
  63. package/src/world.ts +170 -79
  64. package/examples/game/.vscode/launch.json +0 -11
  65. package/examples/game/.vscode/settings.json +0 -11
  66. package/examples/game/README.md +0 -40
  67. package/examples/game/app/client.tsx +0 -15
  68. package/examples/game/app/components/Admin.tsx +0 -1089
  69. package/examples/game/app/components/Room.tsx +0 -162
  70. package/examples/game/app/styles.css +0 -31
  71. package/examples/game/package-lock.json +0 -225
  72. package/examples/game/package.json +0 -20
  73. package/examples/game/party/game.room.ts +0 -32
  74. package/examples/game/party/server.ts +0 -10
  75. package/examples/game/party/shard.ts +0 -5
  76. package/examples/game/partykit.json +0 -14
  77. package/examples/game/public/favicon.ico +0 -0
  78. package/examples/game/public/index.html +0 -27
  79. package/examples/game/public/normalize.css +0 -351
  80. package/examples/game/shared/room.schema.ts +0 -14
  81. package/examples/game/tsconfig.json +0 -109
@@ -1,1089 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useState, useEffect, useRef } from 'react';
4
- import Room from './Room';
5
- import { connectionRoom } from '../../../../../sync/src/client';
6
- import { signal, effect } from '@signe/reactive';
7
-
8
- // Classe client pour représenter le world côté client
9
- class WorldClient {
10
- rooms = signal<Record<string, any>>({});
11
- shards = signal<Record<string, any>>({});
12
- roomShards = signal<Record<string, string[]>>({});
13
- }
14
-
15
- // Interfaces pour les données du World
16
- interface RoomConfig {
17
- name: string;
18
- balancingStrategy: 'round-robin' | 'least-connections' | 'random';
19
- public: boolean;
20
- maxPlayersPerShard: number;
21
- minShards: number;
22
- maxShards?: number;
23
- }
24
-
25
- interface ShardInfo {
26
- id: string;
27
- url: string;
28
- connections: number;
29
- capacity: number;
30
- status?: string;
31
- }
32
-
33
- interface RoomInfo {
34
- roomId: string;
35
- config: RoomConfig;
36
- shards: ShardInfo[];
37
- metrics: {
38
- totalConnections: number;
39
- totalCapacity: number;
40
- utilizationPercentage: number;
41
- };
42
- }
43
-
44
- interface WorldInfo {
45
- rooms: RoomInfo[];
46
- }
47
-
48
- // Composant qui affiche un loading spinner
49
- const LoadingSpinner = () => (
50
- <div className="loading-spinner">
51
- <div className="spinner"></div>
52
- {/* @ts-ignore */}
53
- <style jsx>{`
54
- .loading-spinner {
55
- display: flex;
56
- justify-content: center;
57
- align-items: center;
58
- height: 100px;
59
- }
60
- .spinner {
61
- border: 4px solid rgba(0, 0, 0, 0.1);
62
- width: 36px;
63
- height: 36px;
64
- border-radius: 50%;
65
- border-left-color: #2563eb;
66
- animation: spin 1s linear infinite;
67
- }
68
- @keyframes spin {
69
- 0% { transform: rotate(0deg); }
70
- 100% { transform: rotate(360deg); }
71
- }
72
- `}</style>
73
- </div>
74
- );
75
-
76
- // Composant pour afficher une barre de progression
77
- const ProgressBar = ({ percentage }: { percentage: number }) => {
78
- const barColor =
79
- percentage < 60 ? '#22c55e' : // vert si <60%
80
- percentage < 80 ? '#f59e0b' : // jaune si <80%
81
- '#ef4444'; // rouge si >=80%
82
-
83
- return (
84
- <div className="progress-container">
85
- <div
86
- className="progress-bar"
87
- style={{ width: `${percentage}%`, backgroundColor: barColor }}
88
- ></div>
89
- <span className="progress-label">{percentage}%</span>
90
- {/* @ts-ignore */}
91
- <style jsx>{`
92
- .progress-container {
93
- width: 100%;
94
- height: 20px;
95
- background-color: #e5e7eb;
96
- border-radius: 10px;
97
- position: relative;
98
- overflow: hidden;
99
- }
100
- .progress-bar {
101
- height: 100%;
102
- border-radius: 10px;
103
- transition: width 0.3s ease;
104
- }
105
- .progress-label {
106
- position: absolute;
107
- top: 50%;
108
- left: 50%;
109
- transform: translate(-50%, -50%);
110
- color: #fff;
111
- font-weight: bold;
112
- text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
113
- }
114
- `}</style>
115
- </div>
116
- );
117
- };
118
-
119
- // Composant pour une carte de salle
120
- const RoomCard = ({ room, onSelect }: { room: RoomInfo; onSelect: () => void }) => {
121
- return (
122
- <div className="room-card" onClick={onSelect}>
123
- <h3>{room.config.name} <span className="room-id">({room.roomId})</span></h3>
124
- <div className="room-stats">
125
- <div className="stat">
126
- <span className="label">Shards:</span>
127
- <span className="value">{room.shards.length}</span>
128
- </div>
129
- <div className="stat">
130
- <span className="label">Utilisateurs:</span>
131
- <span className="value">{room.metrics.totalConnections} / {room.metrics.totalCapacity}</span>
132
- </div>
133
- <div className="stat">
134
- <span className="label">Utilisation:</span>
135
- </div>
136
- <ProgressBar percentage={room.metrics.utilizationPercentage} />
137
- </div>
138
- {/* @ts-ignore */}
139
- <style jsx>{`
140
- .room-card {
141
- background: white;
142
- border-radius: 8px;
143
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
144
- padding: 16px;
145
- margin-bottom: 16px;
146
- cursor: pointer;
147
- transition: all 0.2s;
148
- }
149
- .room-card:hover {
150
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
151
- transform: translateY(-2px);
152
- }
153
- .room-id {
154
- color: #6b7280;
155
- font-size: 0.9em;
156
- }
157
- .room-stats {
158
- margin-top: 12px;
159
- }
160
- .stat {
161
- display: flex;
162
- justify-content: space-between;
163
- margin-bottom: 8px;
164
- }
165
- .label {
166
- font-weight: 500;
167
- color: #374151;
168
- }
169
- .value {
170
- font-weight: 600;
171
- }
172
- `}</style>
173
- </div>
174
- );
175
- };
176
-
177
- // Composant pour une carte de shard
178
- const ShardCard = ({ shard }: { shard: ShardInfo }) => {
179
- const statusColor =
180
- shard.status === 'active' ? '#22c55e' :
181
- shard.status === 'draining' ? '#f59e0b' :
182
- shard.status === 'maintenance' ? '#3b82f6' : '#6b7280';
183
-
184
- const utilizationPercentage = Math.round((shard.connections / shard.capacity) * 100) || 0;
185
-
186
- // Formatage de l'ID du shard pour l'affichage (prendre les derniers caractères ou tout l'ID)
187
- const displayId = shard.id.includes('-') ? shard.id.split('-').pop() : shard.id;
188
-
189
- return (
190
- <div className="shard-card">
191
- <div className="shard-header">
192
- <h4 className="shard-title">Shard: {displayId}</h4>
193
- <div className="status-badge" style={{ backgroundColor: statusColor }}>
194
- {shard.status || 'unknown'}
195
- </div>
196
- </div>
197
-
198
- <div className="shard-url">{shard.url}</div>
199
-
200
- <div className="shard-stats">
201
- <div className="stat">
202
- <span className="label">Connexions:</span>
203
- <span className="value">{shard.connections} / {shard.capacity}</span>
204
- </div>
205
- <div className="stat">
206
- <span className="label">Utilisation:</span>
207
- </div>
208
- <ProgressBar percentage={utilizationPercentage} />
209
- </div>
210
-
211
- {/* @ts-ignore */}
212
- <style jsx>{`
213
- .shard-card {
214
- background: #f9fafb;
215
- border-radius: 6px;
216
- padding: 12px;
217
- margin-bottom: 12px;
218
- border: 1px solid #e5e7eb;
219
- }
220
- .shard-header {
221
- display: flex;
222
- justify-content: space-between;
223
- align-items: center;
224
- margin-bottom: 8px;
225
- }
226
- .shard-title {
227
- margin: 0;
228
- font-size: 1rem;
229
- font-weight: 600;
230
- }
231
- .status-badge {
232
- padding: 4px 8px;
233
- border-radius: 9999px;
234
- font-size: 0.75rem;
235
- color: white;
236
- text-transform: capitalize;
237
- }
238
- .shard-url {
239
- font-family: monospace;
240
- font-size: 0.8rem;
241
- color: #6b7280;
242
- margin-bottom: 8px;
243
- word-break: break-all;
244
- }
245
- .shard-stats {
246
- margin-top: 8px;
247
- }
248
- .stat {
249
- display: flex;
250
- justify-content: space-between;
251
- margin-bottom: 6px;
252
- font-size: 0.9rem;
253
- }
254
- .label {
255
- font-weight: 500;
256
- color: #374151;
257
- }
258
- .value {
259
- font-weight: 600;
260
- }
261
- `}</style>
262
- </div>
263
- );
264
- };
265
-
266
- // Formulaire pour créer une salle
267
- const CreateRoomForm = ({ worldId, onRoomCreated }: { worldId: string; onRoomCreated: () => void }) => {
268
- const [name, setName] = useState('');
269
- const [balancingStrategy, setBalancingStrategy] = useState<'round-robin' | 'least-connections' | 'random'>('round-robin');
270
- const [isPublic, setIsPublic] = useState(true);
271
- const [maxPlayers, setMaxPlayers] = useState(100);
272
- const [minShards, setMinShards] = useState(1);
273
- const [maxShards, setMaxShards] = useState(10);
274
- const [isLoading, setIsLoading] = useState(false);
275
- const [error, setError] = useState('');
276
-
277
- const handleSubmit = async (e: React.FormEvent) => {
278
- e.preventDefault();
279
- setIsLoading(true);
280
- setError('');
281
-
282
- try {
283
- // Utiliser une requête HTTP
284
- const response = await fetch(`/parties/world/world-default/register-room`, {
285
- method: 'POST',
286
- headers: {
287
- 'Content-Type': 'application/json',
288
- },
289
- body: JSON.stringify({
290
- roomId: name,
291
- config: {
292
- name,
293
- balancingStrategy,
294
- public: isPublic,
295
- maxPlayersPerShard: maxPlayers,
296
- minShards,
297
- maxShards
298
- }
299
- })
300
- });
301
-
302
- if (!response.ok) {
303
- const errorData = await response.json();
304
- throw new Error(errorData.error || "Échec de la création de la salle");
305
- }
306
-
307
- onRoomCreated();
308
-
309
- // Reset form
310
- setName('');
311
- setBalancingStrategy('round-robin');
312
- setIsPublic(true);
313
- setMaxPlayers(100);
314
- setMinShards(1);
315
- setMaxShards(10);
316
- } catch (err: any) {
317
- setError(err.message);
318
- } finally {
319
- setIsLoading(false);
320
- }
321
- };
322
-
323
- return (
324
- <div className="create-room-form">
325
- <h3>Créer une nouvelle salle</h3>
326
-
327
- {error && <div className="error-message">{error}</div>}
328
-
329
- <form onSubmit={handleSubmit}>
330
- <div className="form-group">
331
- <label htmlFor="name">Nom de la salle</label>
332
- <input
333
- type="text"
334
- id="name"
335
- value={name}
336
- onChange={(e) => setName(e.target.value)}
337
- required
338
- />
339
- </div>
340
-
341
- <div className="form-group">
342
- <label htmlFor="balancingStrategy">Stratégie d'équilibrage</label>
343
- <select
344
- id="balancingStrategy"
345
- value={balancingStrategy}
346
- onChange={(e) => setBalancingStrategy(e.target.value as any)}
347
- required
348
- >
349
- <option value="round-robin">Round Robin</option>
350
- <option value="least-connections">Moins de connexions</option>
351
- <option value="random">Aléatoire</option>
352
- </select>
353
- </div>
354
-
355
- <div className="form-group">
356
- <label htmlFor="isPublic">Visibilité</label>
357
- <div className="toggle-container">
358
- <input
359
- type="checkbox"
360
- id="isPublic"
361
- checked={isPublic}
362
- onChange={(e) => setIsPublic(e.target.checked)}
363
- />
364
- <label htmlFor="isPublic" className="toggle-label">
365
- {isPublic ? 'Publique' : 'Privée'}
366
- </label>
367
- </div>
368
- </div>
369
-
370
- <div className="form-group">
371
- <label htmlFor="maxPlayers">Joueurs max par shard</label>
372
- <input
373
- type="number"
374
- id="maxPlayers"
375
- value={maxPlayers}
376
- onChange={(e) => setMaxPlayers(parseInt(e.target.value))}
377
- min="1"
378
- required
379
- />
380
- </div>
381
-
382
- <div className="form-group">
383
- <label htmlFor="minShards">Nombre min. de shards</label>
384
- <input
385
- type="number"
386
- id="minShards"
387
- value={minShards}
388
- onChange={(e) => setMinShards(parseInt(e.target.value))}
389
- min="1"
390
- required
391
- />
392
- </div>
393
-
394
- <div className="form-group">
395
- <label htmlFor="maxShards">Nombre max. de shards</label>
396
- <input
397
- type="number"
398
- id="maxShards"
399
- value={maxShards}
400
- onChange={(e) => setMaxShards(parseInt(e.target.value))}
401
- min={minShards}
402
- required
403
- />
404
- </div>
405
-
406
- <button type="submit" className="submit-button" disabled={isLoading}>
407
- {isLoading ? 'Création...' : 'Créer la salle'}
408
- </button>
409
- </form>
410
-
411
- {/* @ts-ignore */}
412
- <style jsx>{`
413
- .create-room-form {
414
- background: white;
415
- border-radius: 8px;
416
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
417
- padding: 16px;
418
- margin-bottom: 24px;
419
- }
420
- .error-message {
421
- background-color: #fee2e2;
422
- border: 1px solid #ef4444;
423
- color: #b91c1c;
424
- padding: 8px 12px;
425
- border-radius: 4px;
426
- margin-bottom: 16px;
427
- }
428
- .form-group {
429
- margin-bottom: 16px;
430
- }
431
- label {
432
- display: block;
433
- margin-bottom: 6px;
434
- font-weight: 500;
435
- color: #374151;
436
- }
437
- input[type="text"],
438
- input[type="number"],
439
- select {
440
- width: 100%;
441
- padding: 8px 12px;
442
- border: 1px solid #d1d5db;
443
- border-radius: 4px;
444
- font-size: 16px;
445
- }
446
- input[type="checkbox"] {
447
- margin-right: 8px;
448
- }
449
- .toggle-container {
450
- display: flex;
451
- align-items: center;
452
- }
453
- .toggle-label {
454
- margin-bottom: 0;
455
- }
456
- .submit-button {
457
- background-color: #2563eb;
458
- color: white;
459
- border: none;
460
- border-radius: 4px;
461
- padding: 8px 16px;
462
- font-size: 16px;
463
- cursor: pointer;
464
- transition: background-color 0.2s;
465
- }
466
- .submit-button:hover {
467
- background-color: #1d4ed8;
468
- }
469
- .submit-button:disabled {
470
- background-color: #93c5fd;
471
- cursor: not-allowed;
472
- }
473
- `}</style>
474
- </div>
475
- );
476
- };
477
-
478
- // Formulaire pour le scaling d'une salle
479
- const ScaleRoomForm = ({ worldId, room, onScaled }: { worldId: string; room: RoomInfo; onScaled: () => void }) => {
480
- const [targetShardCount, setTargetShardCount] = useState(room.shards.length);
481
- const [urlTemplate, setUrlTemplate] = useState('wss://shard-{shardId}.example.com');
482
- const [maxConnections, setMaxConnections] = useState(room.config.maxPlayersPerShard);
483
- const [isLoading, setIsLoading] = useState(false);
484
- const [error, setError] = useState('');
485
-
486
- // Met à jour les valeurs par défaut quand la salle change
487
- useEffect(() => {
488
- setTargetShardCount(room.shards.length);
489
- setMaxConnections(room.config.maxPlayersPerShard);
490
- // Essaie de déduire un modèle d'URL à partir des shards existants
491
- if (room.shards.length > 0) {
492
- const sampleUrl = room.shards[0].url;
493
- setUrlTemplate(sampleUrl.replace(room.shards[0].id, '{shardId}'));
494
- }
495
- }, [room]);
496
-
497
- const handleSubmit = async (e: React.FormEvent) => {
498
- e.preventDefault();
499
- setIsLoading(true);
500
- setError('');
501
-
502
- try {
503
- // Utiliser une requête HTTP
504
- const response = await fetch(`/api/world/scale-room`, {
505
- method: 'POST',
506
- headers: {
507
- 'Content-Type': 'application/json',
508
- },
509
- body: JSON.stringify({
510
- roomId: room.roomId,
511
- targetShardCount,
512
- shardTemplate: {
513
- urlTemplate,
514
- maxConnections
515
- }
516
- })
517
- });
518
-
519
- if (!response.ok) {
520
- const errorData = await response.json();
521
- throw new Error(errorData.error || "Échec de la mise à jour des shards");
522
- }
523
-
524
- onScaled();
525
- } catch (err: any) {
526
- setError(err.message);
527
- } finally {
528
- setIsLoading(false);
529
- }
530
- };
531
-
532
- return (
533
- <div className="scale-room-form">
534
- <h3>Ajuster le nombre de shards</h3>
535
-
536
- {error && <div className="error-message">{error}</div>}
537
-
538
- <form onSubmit={handleSubmit}>
539
- <div className="form-group">
540
- <label htmlFor="targetShardCount">Nombre cible de shards</label>
541
- <input
542
- type="number"
543
- id="targetShardCount"
544
- value={targetShardCount}
545
- onChange={(e) => setTargetShardCount(parseInt(e.target.value))}
546
- min={room.config.minShards}
547
- max={room.config.maxShards || 100}
548
- required
549
- />
550
- <div className="constraints">
551
- Min: {room.config.minShards}, Max: {room.config.maxShards || '∞'}
552
- </div>
553
- </div>
554
-
555
- <div className="form-group">
556
- <label htmlFor="urlTemplate">Modèle d'URL pour nouveaux shards</label>
557
- <input
558
- type="text"
559
- id="urlTemplate"
560
- value={urlTemplate}
561
- onChange={(e) => setUrlTemplate(e.target.value)}
562
- required
563
- />
564
- <div className="hint">Utilisez {'{shardId}'} comme placeholder</div>
565
- </div>
566
-
567
- <div className="form-group">
568
- <label htmlFor="maxConnections">Connexions max par shard</label>
569
- <input
570
- type="number"
571
- id="maxConnections"
572
- value={maxConnections}
573
- onChange={(e) => setMaxConnections(parseInt(e.target.value))}
574
- min="1"
575
- required
576
- />
577
- </div>
578
-
579
- <button type="submit" className="submit-button" disabled={isLoading}>
580
- {isLoading ? 'Mise à jour...' : 'Mettre à jour les shards'}
581
- </button>
582
- </form>
583
-
584
- {/* @ts-ignore */}
585
- <style jsx>{`
586
- .scale-room-form {
587
- background: white;
588
- border-radius: 8px;
589
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
590
- padding: 16px;
591
- margin-bottom: 24px;
592
- }
593
- .error-message {
594
- background-color: #fee2e2;
595
- border: 1px solid #ef4444;
596
- color: #b91c1c;
597
- padding: 8px 12px;
598
- border-radius: 4px;
599
- margin-bottom: 16px;
600
- }
601
- .form-group {
602
- margin-bottom: 16px;
603
- }
604
- label {
605
- display: block;
606
- margin-bottom: 6px;
607
- font-weight: 500;
608
- color: #374151;
609
- }
610
- input[type="text"],
611
- input[type="number"] {
612
- width: 100%;
613
- padding: 8px 12px;
614
- border: 1px solid #d1d5db;
615
- border-radius: 4px;
616
- font-size: 16px;
617
- }
618
- .constraints, .hint {
619
- margin-top: 4px;
620
- font-size: 0.8rem;
621
- color: #6b7280;
622
- }
623
- .submit-button {
624
- background-color: #2563eb;
625
- color: white;
626
- border: none;
627
- border-radius: 4px;
628
- padding: 8px 16px;
629
- font-size: 16px;
630
- cursor: pointer;
631
- transition: background-color 0.2s;
632
- }
633
- .submit-button:hover {
634
- background-color: #1d4ed8;
635
- }
636
- .submit-button:disabled {
637
- background-color: #93c5fd;
638
- cursor: not-allowed;
639
- }
640
- `}</style>
641
- </div>
642
- );
643
- };
644
-
645
- // Fonction pour convertir les données du world en format RoomInfo
646
- const parseWorldInfoFromClient = (worldClient: WorldClient): WorldInfo => {
647
- // Récupérer les données du client
648
- const roomsData = worldClient.rooms();
649
- const shardsData = worldClient.shards();
650
- const roomShardsData = worldClient.roomShards();
651
-
652
- // Transformer les données
653
- const roomsInfo = Object.keys(roomsData).map(roomId => {
654
- const room = roomsData[roomId];
655
- const roomShardIds = roomShardsData[roomId] || [];
656
- const roomShards = roomShardIds
657
- .map(id => shardsData[id])
658
- .filter(Boolean);
659
-
660
- // Calculer les métriques
661
- const totalConnections = roomShards.reduce(
662
- (sum, shard) => sum + (shard ? shard.currentConnections : 0),
663
- 0
664
- );
665
-
666
- const totalCapacity = roomShards.reduce(
667
- (sum, shard) => sum + (shard ? shard.maxConnections : 0),
668
- 0
669
- );
670
-
671
- const utilizationPercentage = totalCapacity > 0
672
- ? Math.round((totalConnections / totalCapacity) * 100)
673
- : 0;
674
-
675
- return {
676
- roomId,
677
- config: {
678
- name: room ? room.name : '',
679
- balancingStrategy: room ? room.balancingStrategy : 'round-robin',
680
- public: room ? room.public : true,
681
- maxPlayersPerShard: room ? room.maxPlayersPerShard : 100,
682
- minShards: room ? room.minShards : 1,
683
- maxShards: room ? room.maxShards : undefined
684
- },
685
- shards: roomShards.map(shard => ({
686
- id: shard ? shard.id || '' : '',
687
- url: shard ? shard.url : '',
688
- connections: shard ? shard.currentConnections : 0,
689
- capacity: shard ? shard.maxConnections : 0,
690
- status: shard ? shard.status : 'unknown'
691
- })),
692
- metrics: {
693
- totalConnections,
694
- totalCapacity,
695
- utilizationPercentage
696
- }
697
- };
698
- });
699
-
700
- return { rooms: roomsInfo };
701
- };
702
-
703
- // Composant principal de l'administration
704
- const WorldAdmin = () => {
705
- const [worldId, setWorldId] = useState('default');
706
- const [worldInfo, setWorldInfo] = useState<WorldInfo | null>(null);
707
- const [isLoading, setIsLoading] = useState(true);
708
- const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
709
- const [error, setError] = useState<string | null>(null);
710
- const [refresh, setRefresh] = useState(0);
711
-
712
- // Références pour le client world et la connexion
713
- const worldClient = useRef<WorldClient>(new WorldClient());
714
- const connection = useRef<any>(null);
715
-
716
- // Se connecter au world via WebSocket
717
- useEffect(() => {
718
- const connectToWorld = async () => {
719
- setIsLoading(true);
720
- setError(null);
721
-
722
- try {
723
- // Réinitialiser le client world
724
- worldClient.current = new WorldClient();
725
-
726
- // Se connecter au world
727
- const conn = await connectionRoom({
728
- host: window.location.origin,
729
- room: `world-default`,
730
- party: 'world',
731
- query: {
732
- 'world-auth-token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30'
733
- }
734
- }, worldClient.current);
735
-
736
- connection.current = conn;
737
-
738
- // Configurer les écouteurs d'événements
739
- conn.on('sync', (data) => {
740
- // Mettre à jour les signaux du client uniquement si les données correspondantes sont définies
741
- if (data.rooms) {
742
- const currentRooms = worldClient.current.rooms();
743
- const updatedRooms = { ...currentRooms };
744
-
745
- // Fusionner les nouvelles données avec les données existantes
746
- Object.entries(data.rooms).forEach(([roomId, roomData]: [string, any]) => {
747
- updatedRooms[roomId] = {
748
- ...(updatedRooms[roomId] || {}),
749
- ...roomData
750
- };
751
- });
752
-
753
- worldClient.current.rooms.set(updatedRooms);
754
- }
755
-
756
- if (data.shards) {
757
- const currentShards = worldClient.current.shards();
758
- const updatedShards = { ...currentShards };
759
-
760
- // Fusionner les nouvelles données avec les données existantes
761
- Object.entries(data.shards).forEach(([shardId, shardData]: [string, any]) => {
762
- updatedShards[shardId] = {
763
- ...(updatedShards[shardId] || {}),
764
- ...shardData,
765
- // Toujours s'assurer que l'ID est défini
766
- id: shardId
767
- };
768
- });
769
-
770
- worldClient.current.shards.set(updatedShards);
771
-
772
- // Mettre à jour roomShards si nécessaire
773
- // Ne reconstruire roomShards que si nous avons reçu des nouveaux shards avec roomId
774
- const shouldUpdateRoomShards = Object.values(data.shards).some((shard: any) => shard.roomId);
775
-
776
- if (shouldUpdateRoomShards) {
777
- const currentRoomShards = worldClient.current.roomShards();
778
- const updatedRoomShards = { ...currentRoomShards };
779
-
780
- // Parcourir tous les shards pour construire la relation room -> shards
781
- Object.entries(updatedShards).forEach(([shardId, shardData]: [string, any]) => {
782
- const roomId = shardData.roomId;
783
- if (roomId) {
784
- if (!updatedRoomShards[roomId]) {
785
- updatedRoomShards[roomId] = [];
786
- }
787
-
788
- // Ajouter le shardId s'il n'est pas déjà présent
789
- if (!updatedRoomShards[roomId].includes(shardId)) {
790
- updatedRoomShards[roomId].push(shardId);
791
- }
792
- }
793
- });
794
-
795
- // Mettre à jour le signal roomShards avec les nouvelles données
796
- worldClient.current.roomShards.set(updatedRoomShards);
797
- }
798
- }
799
-
800
- // Déclencher une mise à jour du composant
801
- setRefresh(prev => prev + 1);
802
- });
803
-
804
- conn.on('error', (err: any) => {
805
- setError(`Erreur de connexion: ${err.message}`);
806
- });
807
-
808
-
809
- setIsLoading(false);
810
- } catch (err: any) {
811
- setError(`Erreur de connexion: ${err.message}`);
812
- setIsLoading(false);
813
- }
814
- };
815
-
816
- connectToWorld();
817
-
818
- return () => {
819
- // Nettoyer la connexion lors du démontage
820
- if (connection.current) {
821
- connection.current.close();
822
- }
823
- };
824
- }, [worldId]);
825
-
826
- // Mettre à jour worldInfo lorsque les données du client changent
827
- useEffect(() => {
828
- // Utiliser les données du client pour générer worldInfo
829
- const info = parseWorldInfoFromClient(worldClient.current);
830
- setWorldInfo(info);
831
-
832
- // Sélectionner la première salle par défaut si aucune n'est sélectionnée
833
- if (!selectedRoomId && info.rooms && info.rooms.length > 0) {
834
- setSelectedRoomId(info.rooms[0].roomId);
835
- } else if (selectedRoomId && !info.rooms.some(room => room.roomId === selectedRoomId)) {
836
- // Si la salle sélectionnée n'existe plus, sélectionner la première salle ou null
837
- setSelectedRoomId(info.rooms.length > 0 ? info.rooms[0].roomId : null);
838
- }
839
- }, [refresh, selectedRoomId]);
840
-
841
- // Salle sélectionnée
842
- const selectedRoom = worldInfo?.rooms.find(room => room.roomId === selectedRoomId);
843
-
844
- // Gestion du rafraîchissement manuel
845
- const handleRefresh = () => {
846
-
847
- };
848
-
849
- // Handler pour la création d'une salle
850
- const handleRoomCreated = () => {
851
- handleRefresh();
852
- };
853
-
854
- // Handler pour le scaling d'une salle
855
- const handleRoomScaled = () => {
856
- handleRefresh();
857
- };
858
-
859
- return (
860
- <div className="world-admin">
861
- <div className="admin-header">
862
- <h1>Administration World</h1>
863
- <div className="world-selector">
864
- <label htmlFor="worldId">ID du World:</label>
865
- <input
866
- type="text"
867
- id="worldId"
868
- value={worldId}
869
- onChange={(e) => setWorldId(e.target.value)}
870
- />
871
- </div>
872
- <button className="refresh-button" onClick={handleRefresh}>
873
- Rafraîchir
874
- </button>
875
- </div>
876
-
877
- {error && (
878
- <div className="error-banner">
879
- {error}
880
- </div>
881
- )}
882
-
883
- {isLoading ? (
884
- <LoadingSpinner />
885
- ) : (
886
- <div className="admin-content">
887
- <div className="rooms-list">
888
- <h2>Salles ({worldInfo?.rooms.length || 0})</h2>
889
-
890
- <CreateRoomForm
891
- worldId={worldId}
892
- onRoomCreated={handleRoomCreated}
893
- />
894
-
895
- {worldInfo?.rooms.map(room => (
896
- <RoomCard
897
- key={room.roomId}
898
- room={room}
899
- onSelect={() => setSelectedRoomId(room.roomId)}
900
- />
901
- ))}
902
-
903
- {worldInfo?.rooms.length === 0 && (
904
- <div className="no-rooms">
905
- Aucune salle enregistrée. Créez-en une avec le formulaire ci-dessus.
906
- </div>
907
- )}
908
- </div>
909
-
910
- <div className="room-details">
911
- {selectedRoom ? (
912
- <>
913
- <div className="room-header">
914
- <h2>{selectedRoom.config.name}</h2>
915
- <div className="room-id-badge">{selectedRoom.roomId}</div>
916
- </div>
917
-
918
- <div className="room-metrics">
919
- <div className="metric-card">
920
- <div className="metric-title">Connexions totales</div>
921
- <div className="metric-value">{selectedRoom.metrics.totalConnections} / {selectedRoom.metrics.totalCapacity}</div>
922
- <ProgressBar percentage={selectedRoom.metrics.utilizationPercentage} />
923
- </div>
924
-
925
- <div className="metric-card">
926
- <div className="metric-title">Nombre de shards</div>
927
- <div className="metric-value">{selectedRoom.shards.length}</div>
928
- <div className="metric-subtitle">
929
- Min: {selectedRoom.config.minShards},
930
- Max: {selectedRoom.config.maxShards || '∞'}
931
- </div>
932
- </div>
933
-
934
- <div className="metric-card">
935
- <div className="metric-title">Stratégie</div>
936
- <div className="metric-value">{selectedRoom.config.balancingStrategy}</div>
937
- <div className="metric-subtitle">
938
- {selectedRoom.config.public ? 'Publique' : 'Privée'}
939
- </div>
940
- </div>
941
- </div>
942
-
943
- <ScaleRoomForm
944
- worldId={worldId}
945
- room={selectedRoom}
946
- onScaled={handleRoomScaled}
947
- />
948
-
949
- <h3>Shards ({selectedRoom.shards.length})</h3>
950
-
951
- <div className="shards-grid">
952
- {selectedRoom.shards.map(shard => (
953
- <ShardCard key={shard.id} shard={shard} />
954
- ))}
955
-
956
- {selectedRoom.shards.length === 0 && (
957
- <div className="no-shards">
958
- Aucun shard pour cette salle. Utilisez le formulaire ci-dessus pour en ajouter.
959
- </div>
960
- )}
961
- </div>
962
- </>
963
- ) : (
964
- <div className="no-room-selected">
965
- Sélectionnez une salle dans la liste pour voir ses détails.
966
- </div>
967
- )}
968
- </div>
969
- </div>
970
- )}
971
-
972
- <Room />
973
-
974
- {/* @ts-ignore */}
975
- <style jsx>{`
976
- .world-admin {
977
- padding: 20px;
978
- max-width: 1200px;
979
- margin: 0 auto;
980
- }
981
- .admin-header {
982
- display: flex;
983
- justify-content: space-between;
984
- align-items: center;
985
- margin-bottom: 24px;
986
- }
987
- .world-selector {
988
- display: flex;
989
- align-items: center;
990
- }
991
- .world-selector label {
992
- margin-right: 8px;
993
- }
994
- .world-selector input {
995
- padding: 6px 12px;
996
- border: 1px solid #d1d5db;
997
- border-radius: 4px;
998
- }
999
- .refresh-button {
1000
- background-color: #f3f4f6;
1001
- border: 1px solid #d1d5db;
1002
- border-radius: 4px;
1003
- padding: 6px 12px;
1004
- cursor: pointer;
1005
- transition: background-color 0.2s;
1006
- }
1007
- .refresh-button:hover {
1008
- background-color: #e5e7eb;
1009
- }
1010
- .error-banner {
1011
- background-color: #fee2e2;
1012
- border: 1px solid #ef4444;
1013
- color: #b91c1c;
1014
- padding: 12px 16px;
1015
- border-radius: 4px;
1016
- margin-bottom: 24px;
1017
- }
1018
- .admin-content {
1019
- display: grid;
1020
- grid-template-columns: 1fr 2fr;
1021
- gap: 24px;
1022
- }
1023
- .rooms-list {
1024
- background: #f9fafb;
1025
- border-radius: 8px;
1026
- padding: 16px;
1027
- }
1028
- .room-details {
1029
- background: #f9fafb;
1030
- border-radius: 8px;
1031
- padding: 16px;
1032
- }
1033
- .room-header {
1034
- display: flex;
1035
- align-items: center;
1036
- margin-bottom: 16px;
1037
- }
1038
- .room-id-badge {
1039
- background: #e5e7eb;
1040
- border-radius: 9999px;
1041
- padding: 4px 12px;
1042
- margin-left: 12px;
1043
- font-size: 0.9rem;
1044
- color: #374151;
1045
- }
1046
- .room-metrics {
1047
- display: grid;
1048
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1049
- gap: 16px;
1050
- margin-bottom: 24px;
1051
- }
1052
- .metric-card {
1053
- background: white;
1054
- border-radius: 8px;
1055
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1056
- padding: 16px;
1057
- }
1058
- .metric-title {
1059
- font-size: 0.9rem;
1060
- color: #6b7280;
1061
- margin-bottom: 8px;
1062
- }
1063
- .metric-value {
1064
- font-size: 1.5rem;
1065
- font-weight: 600;
1066
- margin-bottom: 8px;
1067
- }
1068
- .metric-subtitle {
1069
- font-size: 0.8rem;
1070
- color: #6b7280;
1071
- }
1072
- .shards-grid {
1073
- display: grid;
1074
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1075
- gap: 16px;
1076
- }
1077
- .no-rooms, .no-shards, .no-room-selected {
1078
- background: white;
1079
- border-radius: 8px;
1080
- padding: 32px;
1081
- text-align: center;
1082
- color: #6b7280;
1083
- }
1084
- `}</style>
1085
- </div>
1086
- );
1087
- };
1088
-
1089
- export default WorldAdmin;