@signe/room 1.4.1 → 2.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.
- package/dist/index.d.ts +258 -22
- package/dist/index.js +1447 -60
- package/dist/index.js.map +1 -1
- package/examples/game/app/client.tsx +2 -2
- package/examples/game/app/components/Admin.tsx +1089 -0
- package/examples/game/app/components/Room.tsx +158 -0
- package/examples/game/party/server.ts +3 -2
- package/examples/game/party/shard.ts +5 -0
- package/examples/game/partykit.json +5 -1
- package/package.json +2 -2
- package/readme.md +226 -2
- package/src/decorators.ts +34 -2
- package/src/index.ts +4 -1
- package/src/interfaces.ts +13 -0
- package/src/jwt.ts +217 -0
- package/src/mock.ts +39 -3
- package/src/server.ts +595 -79
- package/src/shard.ts +244 -0
- package/src/testing.ts +47 -6
- package/src/utils.ts +7 -0
- package/src/world.guard.ts +28 -0
- package/src/world.ts +448 -0
- package/examples/game/app/components/Counter.tsx +0 -82
package/src/world.ts
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { signal } from "@signe/reactive";
|
|
2
|
+
import { Room, Action, Guard, Request } from "./decorators";
|
|
3
|
+
import { sync, id, persist } from "@signe/sync";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import * as Party from "./types/party";
|
|
6
|
+
import { guardManageWorld } from "./world.guard";
|
|
7
|
+
import { response } from "./utils";
|
|
8
|
+
import { RoomInterceptorPacket, RoomOnJoin } from "./interfaces";
|
|
9
|
+
|
|
10
|
+
// Types definitions
|
|
11
|
+
type BalancingStrategy = 'round-robin' | 'least-connections' | 'random';
|
|
12
|
+
type ShardStatus = 'active' | 'maintenance' | 'draining';
|
|
13
|
+
|
|
14
|
+
// Schema validations
|
|
15
|
+
const RoomConfigSchema = z.object({
|
|
16
|
+
name: z.string(),
|
|
17
|
+
balancingStrategy: z.enum(['round-robin', 'least-connections', 'random']),
|
|
18
|
+
public: z.boolean(),
|
|
19
|
+
maxPlayersPerShard: z.number().int().positive(),
|
|
20
|
+
minShards: z.number().int().min(0),
|
|
21
|
+
maxShards: z.number().int().positive().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const RegisterShardSchema = z.object({
|
|
25
|
+
shardId: z.string(),
|
|
26
|
+
roomId: z.string(),
|
|
27
|
+
url: z.string().url(),
|
|
28
|
+
maxConnections: z.number().int().positive(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const UpdateShardStatsSchema = z.object({
|
|
32
|
+
connections: z.number().int().min(0),
|
|
33
|
+
status: z.enum(['active', 'maintenance', 'draining']).optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const ScaleRoomSchema = z.object({
|
|
37
|
+
roomId: z.string(),
|
|
38
|
+
targetShardCount: z.number().int().positive(),
|
|
39
|
+
shardTemplate: z.object({
|
|
40
|
+
urlTemplate: z.string(),
|
|
41
|
+
maxConnections: z.number().int().positive(),
|
|
42
|
+
}).optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Model classes
|
|
46
|
+
class RoomConfig {
|
|
47
|
+
@id() id: string;
|
|
48
|
+
@sync() name = signal("");
|
|
49
|
+
@sync() balancingStrategy = signal<BalancingStrategy>("round-robin");
|
|
50
|
+
@sync() public = signal(true);
|
|
51
|
+
@sync() maxPlayersPerShard = signal(100);
|
|
52
|
+
@sync() minShards = signal(1);
|
|
53
|
+
@sync() maxShards = signal<number | undefined>(undefined);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class ShardInfo {
|
|
57
|
+
@id() id: string;
|
|
58
|
+
@sync() roomId = signal("");
|
|
59
|
+
@sync() url = signal("");
|
|
60
|
+
@sync({
|
|
61
|
+
persist: false
|
|
62
|
+
}) currentConnections = signal(0);
|
|
63
|
+
@sync() maxConnections = signal(100);
|
|
64
|
+
@sync() status = signal<ShardStatus>("active");
|
|
65
|
+
@sync() lastHeartbeat = signal(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// World room implementation
|
|
69
|
+
@Room({
|
|
70
|
+
path: "world-{worldId}",
|
|
71
|
+
maxUsers: 100, // Limit for admin connections
|
|
72
|
+
throttleStorage: 2000, // Throttle storage updates (ms)
|
|
73
|
+
throttleSync: 500, // Throttle sync updates (ms)
|
|
74
|
+
})
|
|
75
|
+
export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
76
|
+
// Synchronized state
|
|
77
|
+
@sync(RoomConfig) rooms = signal<Record<string, RoomConfig>>({});
|
|
78
|
+
@sync(ShardInfo) shards = signal<Record<string, ShardInfo>>({});
|
|
79
|
+
|
|
80
|
+
// Only persisted state (not synced to clients)
|
|
81
|
+
@persist() rrCounters = signal<Record<string, number>>({});
|
|
82
|
+
|
|
83
|
+
// Configuration
|
|
84
|
+
defaultShardUrlTemplate = signal("{shardId}");
|
|
85
|
+
defaultMaxConnectionsPerShard = signal(100);
|
|
86
|
+
|
|
87
|
+
constructor(private room: Party.Room) {
|
|
88
|
+
const { AUTH_JWT_SECRET, SHARD_SECRET } = this.room.env;
|
|
89
|
+
if (!AUTH_JWT_SECRET) {
|
|
90
|
+
throw new Error("AUTH_JWT_SECRET env variable is not set");
|
|
91
|
+
}
|
|
92
|
+
if (!SHARD_SECRET) {
|
|
93
|
+
throw new Error("SHARD_SECRET env variable is not set");
|
|
94
|
+
}
|
|
95
|
+
setTimeout(() => this.cleanupInactiveShards(), 60000);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async onJoin(user: any, conn: Party.Connection, ctx: Party.ConnectionContext) {
|
|
99
|
+
const canConnect = await guardManageWorld(user, ctx.request, this.room);
|
|
100
|
+
conn.setState({
|
|
101
|
+
...conn.state,
|
|
102
|
+
isAdmin: canConnect
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interceptorPacket(_, obj: any, conn: Party.Connection) {
|
|
107
|
+
if (!conn.state['isAdmin']) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return obj;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Helper methods
|
|
114
|
+
private cleanupInactiveShards() {
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const timeout = 5 * 60 * 1000; // 5 minutes timeout
|
|
117
|
+
const shardsValue = this.shards();
|
|
118
|
+
|
|
119
|
+
let hasChanges = false;
|
|
120
|
+
Object.values(shardsValue).forEach(shard => {
|
|
121
|
+
if (now - shard.lastHeartbeat() > timeout) {
|
|
122
|
+
delete this.shards()[shard.id];
|
|
123
|
+
|
|
124
|
+
hasChanges = true;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Schedule next cleanup
|
|
129
|
+
setTimeout(() => this.cleanupInactiveShards(), 60000);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Actions
|
|
133
|
+
@Request({
|
|
134
|
+
path: 'register-room',
|
|
135
|
+
method: 'POST',
|
|
136
|
+
})
|
|
137
|
+
@Guard([guardManageWorld])
|
|
138
|
+
async registerRoom(req: Party.Request) {
|
|
139
|
+
const roomConfig: z.infer<typeof RoomConfigSchema> = await req.json();
|
|
140
|
+
const roomId = roomConfig.name;
|
|
141
|
+
|
|
142
|
+
if (!this.rooms()[roomId]) {
|
|
143
|
+
const newRoom = new RoomConfig();
|
|
144
|
+
newRoom.id = roomId;
|
|
145
|
+
newRoom.name.set(roomConfig.name);
|
|
146
|
+
newRoom.balancingStrategy.set(roomConfig.balancingStrategy);
|
|
147
|
+
newRoom.public.set(roomConfig.public);
|
|
148
|
+
newRoom.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
|
|
149
|
+
newRoom.minShards.set(roomConfig.minShards);
|
|
150
|
+
newRoom.maxShards.set(roomConfig.maxShards);
|
|
151
|
+
|
|
152
|
+
this.rooms()[roomId] = newRoom;
|
|
153
|
+
|
|
154
|
+
// Ensure minimum shards are created
|
|
155
|
+
if (roomConfig.minShards > 0) {
|
|
156
|
+
for (let i = 0; i < roomConfig.minShards; i++) {
|
|
157
|
+
await this.createShard(roomId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// Update existing room
|
|
162
|
+
const room = this.rooms()[roomId];
|
|
163
|
+
room.balancingStrategy.set(roomConfig.balancingStrategy);
|
|
164
|
+
room.public.set(roomConfig.public);
|
|
165
|
+
room.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
|
|
166
|
+
room.minShards.set(roomConfig.minShards);
|
|
167
|
+
room.maxShards.set(roomConfig.maxShards);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Request({
|
|
172
|
+
path: 'update-shard',
|
|
173
|
+
method: 'POST',
|
|
174
|
+
})
|
|
175
|
+
@Guard([guardManageWorld])
|
|
176
|
+
async updateShardStats(req: Party.Request) {
|
|
177
|
+
const body: { shardId: string; connections: number; status: ShardStatus } = await req.json();
|
|
178
|
+
const { shardId, connections, status } = body;
|
|
179
|
+
const shard = this.shards()[shardId];
|
|
180
|
+
|
|
181
|
+
if (!shard) {
|
|
182
|
+
return { error: `Shard ${shardId} not found` };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
shard.currentConnections.set(connections);
|
|
186
|
+
if (status) {
|
|
187
|
+
shard.status.set(status);
|
|
188
|
+
}
|
|
189
|
+
shard.lastHeartbeat.set(Date.now());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@Request({
|
|
193
|
+
path: 'scale-room',
|
|
194
|
+
method: 'POST',
|
|
195
|
+
})
|
|
196
|
+
@Guard([guardManageWorld])
|
|
197
|
+
async scaleRoom(req: Party.Request) {
|
|
198
|
+
const data: z.infer<typeof ScaleRoomSchema> = await req.json();
|
|
199
|
+
const { targetShardCount, shardTemplate, roomId } = data;
|
|
200
|
+
|
|
201
|
+
// Validate room exists
|
|
202
|
+
const room = this.rooms()[roomId];
|
|
203
|
+
if (!room) {
|
|
204
|
+
return { error: `Room ${roomId} does not exist` };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const roomShards = Object.values(this.shards())
|
|
208
|
+
.filter(shard => shard.roomId() === roomId);
|
|
209
|
+
|
|
210
|
+
const previousShardCount = roomShards.length;
|
|
211
|
+
|
|
212
|
+
// Check max shards constraint
|
|
213
|
+
if (room.maxShards() !== undefined && targetShardCount > room.maxShards()!) {
|
|
214
|
+
return {
|
|
215
|
+
error: `Cannot scale beyond maximum allowed shards (${room.maxShards()})`,
|
|
216
|
+
roomId,
|
|
217
|
+
currentShardCount: previousShardCount
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Handle scaling down
|
|
222
|
+
if (targetShardCount < previousShardCount) {
|
|
223
|
+
// Find candidates for removal (prioritize draining or low-connection shards)
|
|
224
|
+
const shardsToRemove = [...roomShards]
|
|
225
|
+
.sort((a, b) => {
|
|
226
|
+
// Prioritize draining status
|
|
227
|
+
if (a.status() === 'draining' && b.status() !== 'draining') return -1;
|
|
228
|
+
if (a.status() !== 'draining' && b.status() === 'draining') return 1;
|
|
229
|
+
|
|
230
|
+
// Then by connection count (ascending)
|
|
231
|
+
return a.currentConnections() - b.currentConnections();
|
|
232
|
+
})
|
|
233
|
+
.slice(0, previousShardCount - targetShardCount);
|
|
234
|
+
|
|
235
|
+
// Remove the selected shards
|
|
236
|
+
const shardsToKeep = roomShards.filter(
|
|
237
|
+
shard => !shardsToRemove.some(s => s.id === shard.id)
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Update shards
|
|
241
|
+
for (const shard of shardsToRemove) {
|
|
242
|
+
delete this.shards()[shard.id];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Handle scaling up
|
|
249
|
+
if (targetShardCount > previousShardCount) {
|
|
250
|
+
const newShards = [];
|
|
251
|
+
|
|
252
|
+
// Create new shards
|
|
253
|
+
for (let i = 0; i < targetShardCount - previousShardCount; i++) {
|
|
254
|
+
const newShard = await this.createShard(
|
|
255
|
+
roomId,
|
|
256
|
+
shardTemplate?.urlTemplate,
|
|
257
|
+
shardTemplate?.maxConnections
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (newShard) {
|
|
261
|
+
newShards.push(newShard);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@Request({
|
|
268
|
+
path: 'connect',
|
|
269
|
+
method: 'POST',
|
|
270
|
+
})
|
|
271
|
+
async connect(req: Party.Request) {
|
|
272
|
+
try {
|
|
273
|
+
// Extract request data
|
|
274
|
+
let data: { roomId: string; autoCreate?: boolean };
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Handle potential empty body or malformed JSON
|
|
278
|
+
const body = await req.text();
|
|
279
|
+
if (!body || body.trim() === '') {
|
|
280
|
+
return response(400, { error: "Request body is empty" });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
data = JSON.parse(body);
|
|
284
|
+
} catch (parseError) {
|
|
285
|
+
return response(400, { error: "Invalid JSON in request body" });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Verify roomId is provided
|
|
289
|
+
if (!data.roomId) {
|
|
290
|
+
return response(400, { error: "roomId parameter is required" });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Determine if auto-creation is enabled (default to true)
|
|
294
|
+
const autoCreate = data.autoCreate !== undefined ? data.autoCreate : true;
|
|
295
|
+
|
|
296
|
+
// Find optimal shard
|
|
297
|
+
const result = await this.findOptimalShard(data.roomId, autoCreate);
|
|
298
|
+
|
|
299
|
+
// Check for errors
|
|
300
|
+
if ('error' in result) {
|
|
301
|
+
return response(404, { error: result.error });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Return shard information to the client
|
|
305
|
+
return response(200, {
|
|
306
|
+
success: true,
|
|
307
|
+
shardId: result.shardId,
|
|
308
|
+
url: result.url
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error('Error connecting to shard:', error);
|
|
312
|
+
return response(500, { error: "Internal server error", details: error instanceof Error ? error.message : String(error) });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async findOptimalShard(
|
|
317
|
+
roomId: string,
|
|
318
|
+
autoCreate: boolean = true
|
|
319
|
+
): Promise<{ shardId: string; url: string } | { error: string }> {
|
|
320
|
+
// Ensure room exists
|
|
321
|
+
let room = this.rooms()[roomId];
|
|
322
|
+
if (!room) {
|
|
323
|
+
if (autoCreate) {
|
|
324
|
+
const mockRequest = {
|
|
325
|
+
json: async () => ({
|
|
326
|
+
name: roomId,
|
|
327
|
+
balancingStrategy: 'round-robin',
|
|
328
|
+
public: true,
|
|
329
|
+
maxPlayersPerShard: this.defaultMaxConnectionsPerShard(),
|
|
330
|
+
minShards: 1,
|
|
331
|
+
maxShards: undefined
|
|
332
|
+
})
|
|
333
|
+
} as Party.Request;
|
|
334
|
+
|
|
335
|
+
await this.registerRoom(mockRequest);
|
|
336
|
+
|
|
337
|
+
room = this.rooms()[roomId];
|
|
338
|
+
|
|
339
|
+
if (!room) {
|
|
340
|
+
return { error: `Failed to create room ${roomId}` };
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
return { error: `Room ${roomId} does not exist` };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Get shards for this room
|
|
348
|
+
const roomShards = Object.values(this.shards())
|
|
349
|
+
.filter(shard => shard.roomId() === roomId);
|
|
350
|
+
|
|
351
|
+
if (roomShards.length === 0) {
|
|
352
|
+
if (autoCreate) {
|
|
353
|
+
// Auto-create a shard
|
|
354
|
+
const newShard = await this.createShard(roomId);
|
|
355
|
+
if (newShard) {
|
|
356
|
+
return {
|
|
357
|
+
shardId: newShard.id,
|
|
358
|
+
url: newShard.url()
|
|
359
|
+
};
|
|
360
|
+
} else {
|
|
361
|
+
return { error: `Failed to create shard for room ${roomId}` };
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
return { error: `No shards available for room ${roomId}` };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Get active shards
|
|
369
|
+
const activeShards = roomShards
|
|
370
|
+
.filter(shard => shard && shard.status() === 'active');
|
|
371
|
+
|
|
372
|
+
if (activeShards.length === 0) {
|
|
373
|
+
return { error: `No active shards available for room ${roomId}` };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Apply balancing strategy
|
|
377
|
+
const balancingStrategy = room.balancingStrategy();
|
|
378
|
+
let selectedShard: ShardInfo;
|
|
379
|
+
|
|
380
|
+
switch (balancingStrategy) {
|
|
381
|
+
case 'least-connections':
|
|
382
|
+
// Choose shard with fewest connections
|
|
383
|
+
selectedShard = activeShards.reduce(
|
|
384
|
+
(min, shard) =>
|
|
385
|
+
shard.currentConnections() < min.currentConnections() ? shard : min,
|
|
386
|
+
activeShards[0]
|
|
387
|
+
);
|
|
388
|
+
break;
|
|
389
|
+
|
|
390
|
+
case 'random':
|
|
391
|
+
// Choose random shard
|
|
392
|
+
selectedShard = activeShards[Math.floor(Math.random() * activeShards.length)];
|
|
393
|
+
break;
|
|
394
|
+
|
|
395
|
+
case 'round-robin':
|
|
396
|
+
default:
|
|
397
|
+
// Round-robin selection
|
|
398
|
+
const counter = this.rrCounters()[roomId] || 0;
|
|
399
|
+
const nextCounter = (counter + 1) % activeShards.length;
|
|
400
|
+
this.rrCounters()[roomId] = nextCounter;
|
|
401
|
+
|
|
402
|
+
selectedShard = activeShards[counter];
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
shardId: selectedShard.id,
|
|
408
|
+
url: selectedShard.url()
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Private methods
|
|
413
|
+
private async createShard(
|
|
414
|
+
roomId: string,
|
|
415
|
+
urlTemplate?: string,
|
|
416
|
+
maxConnections?: number
|
|
417
|
+
): Promise<ShardInfo | null> {
|
|
418
|
+
const room = this.rooms()[roomId];
|
|
419
|
+
if (!room) {
|
|
420
|
+
console.error(`Cannot create shard for non-existent room: ${roomId}`);
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Generate shard ID
|
|
425
|
+
const shardId = `${roomId}:${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
|
426
|
+
|
|
427
|
+
// Generate URL from template
|
|
428
|
+
const template = urlTemplate || this.defaultShardUrlTemplate();
|
|
429
|
+
const url = template.replace('{shardId}', shardId).replace('{roomId}', roomId);
|
|
430
|
+
|
|
431
|
+
// Set max connections
|
|
432
|
+
const max = maxConnections || room.maxPlayersPerShard();
|
|
433
|
+
|
|
434
|
+
// Create the shard
|
|
435
|
+
const newShard = new ShardInfo();
|
|
436
|
+
newShard.id = shardId;
|
|
437
|
+
newShard.roomId.set(roomId);
|
|
438
|
+
newShard.url.set(url);
|
|
439
|
+
newShard.maxConnections.set(max);
|
|
440
|
+
newShard.currentConnections.set(0);
|
|
441
|
+
newShard.status.set("active");
|
|
442
|
+
newShard.lastHeartbeat.set(Date.now());
|
|
443
|
+
|
|
444
|
+
// Update shards collection
|
|
445
|
+
this.shards()[shardId] = newShard;
|
|
446
|
+
return newShard;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { effect } from '../../../../../reactive';
|
|
3
|
-
import { connection } from '../../../../../sync/src/client';
|
|
4
|
-
import { RoomSchema } from "../../shared/room.schema";
|
|
5
|
-
|
|
6
|
-
let val = localStorage.getItem('test')
|
|
7
|
-
|
|
8
|
-
if (!val) {
|
|
9
|
-
val = ''+Math.random()
|
|
10
|
-
localStorage.setItem('test', val)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default function Counter() {
|
|
14
|
-
const [refresh, setRefresh] = useState(0);
|
|
15
|
-
const [count, setCount] = useState(0);
|
|
16
|
-
const [users, setUsers] = useState<any[]>([]);
|
|
17
|
-
let socket = useRef<any>(null);
|
|
18
|
-
let room = useRef<any>(null);
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
room.current = new RoomSchema();
|
|
22
|
-
socket.current = connection({
|
|
23
|
-
host: location.hostname == 'localhost' ? 'localhost:1999' : 'https://signe.rsamaium.partykit.dev',
|
|
24
|
-
room: 'game',
|
|
25
|
-
id: val as string
|
|
26
|
-
}, room.current);
|
|
27
|
-
|
|
28
|
-
socket.current.on('user_disconnected', (data: any) => {
|
|
29
|
-
console.log(data)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
// Subscribe to changes
|
|
33
|
-
effect(() => {
|
|
34
|
-
if (room.current) {
|
|
35
|
-
setCount(room.current.count());
|
|
36
|
-
setUsers(Object.values(room.current.users()));
|
|
37
|
-
setRefresh(refresh + 1);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
const increment = () => {
|
|
43
|
-
room.current.count.update((count: number) => count + 1);
|
|
44
|
-
socket.current.emit('increment')
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const styles = {
|
|
48
|
-
backgroundColor: "#ff0f0f",
|
|
49
|
-
borderRadius: "9999px",
|
|
50
|
-
border: "none",
|
|
51
|
-
color: "white",
|
|
52
|
-
fontSize: "0.95rem",
|
|
53
|
-
cursor: "pointer",
|
|
54
|
-
padding: "1rem 3rem",
|
|
55
|
-
margin: "1rem 0rem",
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const getStorage = async () => {
|
|
59
|
-
const response = await fetch('/party/game').then(res => res.json())
|
|
60
|
-
console.log(JSON.stringify(response, null, 2))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<>
|
|
65
|
-
{
|
|
66
|
-
room.current && (
|
|
67
|
-
<div key={refresh}>
|
|
68
|
-
<button style={styles} onClick={increment}>
|
|
69
|
-
Increment me! {count !== null && <>Count: {count}</>}
|
|
70
|
-
</button>
|
|
71
|
-
<button onClick={getStorage}>get storage</button>
|
|
72
|
-
<ul>
|
|
73
|
-
{users.map((user: any) => (
|
|
74
|
-
<li key={user.id()}>{user.id()} : {user.score()}</li>
|
|
75
|
-
))}
|
|
76
|
-
</ul>
|
|
77
|
-
</div>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
</>
|
|
81
|
-
);
|
|
82
|
-
}
|