@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
package/src/world.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { signal } from "@signe/reactive";
2
- import { Room, Guard, Request } from "./decorators";
2
+ import { Room, Action, Guard, Request } from "./decorators";
3
3
  import { sync, id, persist } from "@signe/sync";
4
4
  import { z } from "zod";
5
5
  import * as Party from "./types/party";
6
6
  import { guardManageWorld } from "./world.guard";
7
+ import { response } from "./utils";
7
8
  import { RoomInterceptorPacket, RoomOnJoin } from "./interfaces";
8
9
  import { ServerResponse } from "./request/response";
9
10
 
@@ -23,8 +24,17 @@ const RoomConfigSchema = z.object({
23
24
  maxShards: z.number().int().positive().optional(),
24
25
  });
25
26
 
27
+ const RegisterShardSchema = z.object({
28
+ shardId: z.string(),
29
+ roomId: z.string(),
30
+ worldId: z.string().optional(),
31
+ url: z.string().url(),
32
+ maxConnections: z.number().int().positive(),
33
+ });
34
+
26
35
  const UpdateShardStatsSchema = z.object({
27
36
  shardId: z.string(),
37
+ worldId: z.string().optional(),
28
38
  connections: z.number().int().min(0),
29
39
  status: z.enum(['active', 'maintenance', 'draining']).optional(),
30
40
  });
@@ -52,6 +62,7 @@ class RoomConfig {
52
62
  class ShardInfo {
53
63
  @id() id: string;
54
64
  @sync() roomId = signal("");
65
+ @sync() worldId = signal("");
55
66
  @sync() url = signal("");
56
67
  @sync({
57
68
  persist: false
@@ -72,10 +83,10 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
72
83
  // Synchronized state
73
84
  @sync(RoomConfig) rooms = signal<Record<string, RoomConfig>>({});
74
85
  @sync(ShardInfo) shards = signal<Record<string, ShardInfo>>({});
75
-
86
+
76
87
  // Only persisted state (not synced to clients)
77
88
  @persist() rrCounters = signal<Record<string, number>>({});
78
-
89
+
79
90
  // Configuration
80
91
  defaultShardUrlTemplate = signal("{shardId}");
81
92
  defaultMaxConnectionsPerShard = signal(MAX_PLAYERS_PER_SHARD);
@@ -88,8 +99,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
88
99
  if (!SHARD_SECRET) {
89
100
  throw new Error("SHARD_SECRET env variable is not set");
90
101
  }
91
- // TODO
92
- //setTimeout(() => this.cleanupInactiveShards(), 60000);
102
+ this.scheduleInactiveShardCleanup();
93
103
  }
94
104
 
95
105
  async onJoin(user: any, conn: Party.Connection, ctx: Party.ConnectionContext) {
@@ -106,26 +116,43 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
106
116
  }
107
117
  return obj;
108
118
  }
109
-
119
+
110
120
  // Helper methods
121
+ private getWorldId() {
122
+ return this.room.id;
123
+ }
124
+
125
+ private scheduleInactiveShardCleanup() {
126
+ const timeoutId = setTimeout(() => this.cleanupInactiveShards(), 60000);
127
+ timeoutId?.unref?.();
128
+ }
129
+
111
130
  private cleanupInactiveShards() {
112
131
  const now = Date.now();
113
132
  const timeout = 5 * 60 * 1000; // 5 minutes timeout
114
133
  const shardsValue = this.shards();
115
-
134
+
116
135
  let hasChanges = false;
117
136
  Object.values(shardsValue).forEach(shard => {
118
137
  if (now - shard.lastHeartbeat() > timeout) {
119
138
  delete this.shards()[shard.id];
120
-
139
+
121
140
  hasChanges = true;
122
141
  }
123
142
  });
124
-
143
+
125
144
  // Schedule next cleanup
126
- setTimeout(() => this.cleanupInactiveShards(), 60000);
145
+ this.scheduleInactiveShardCleanup();
146
+ }
147
+
148
+ private removeShard(shardId: string) {
149
+ delete this.shards()[shardId];
127
150
  }
128
-
151
+
152
+ private shouldCompleteDrain(shard: ShardInfo) {
153
+ return shard.status() === 'draining' && shard.currentConnections() === 0;
154
+ }
155
+
129
156
  // Actions
130
157
  @Request({
131
158
  path: 'register-room',
@@ -133,16 +160,20 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
133
160
  })
134
161
  @Guard([guardManageWorld])
135
162
  async registerRoom(req: Party.Request, res?: ServerResponse) {
136
- const roomConfigResult = RoomConfigSchema.safeParse(await req.json());
137
- if (!roomConfigResult.success) {
163
+ const parseResult = RoomConfigSchema.safeParse(await req.json());
164
+ if (!parseResult.success) {
138
165
  return res?.badRequest("Invalid room configuration", {
139
- details: roomConfigResult.error
166
+ details: parseResult.error
140
167
  });
141
168
  }
142
169
 
143
- const roomConfig = roomConfigResult.data;
170
+ const roomConfig = parseResult.data;
171
+ if (roomConfig.maxShards !== undefined && roomConfig.minShards > roomConfig.maxShards) {
172
+ return res?.badRequest("minShards cannot be greater than maxShards");
173
+ }
174
+
144
175
  const roomId = roomConfig.name;
145
-
176
+
146
177
  if (!this.rooms()[roomId]) {
147
178
  const newRoom = new RoomConfig();
148
179
  newRoom.id = roomId;
@@ -152,9 +183,9 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
152
183
  newRoom.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
153
184
  newRoom.minShards.set(roomConfig.minShards);
154
185
  newRoom.maxShards.set(roomConfig.maxShards);
155
-
186
+
156
187
  this.rooms()[roomId] = newRoom;
157
-
188
+
158
189
  // Ensure minimum shards are created
159
190
  if (roomConfig.minShards > 0) {
160
191
  for (let i = 0; i < roomConfig.minShards; i++) {
@@ -170,63 +201,73 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
170
201
  room.minShards.set(roomConfig.minShards);
171
202
  room.maxShards.set(roomConfig.maxShards);
172
203
  }
204
+
205
+ await this.ensureMinShards(roomId);
173
206
  }
174
-
207
+
175
208
  @Request({
176
209
  path: 'update-shard',
177
210
  method: 'POST',
178
211
  })
179
212
  @Guard([guardManageWorld])
180
213
  async updateShardStats(req: Party.Request, res: ServerResponse) {
181
- const bodyResult = UpdateShardStatsSchema.safeParse(await req.json());
182
- if (!bodyResult.success) {
183
- return res.badRequest("Invalid shard statistics", {
184
- details: bodyResult.error
214
+ const parseResult = UpdateShardStatsSchema.safeParse(await req.json());
215
+ if (!parseResult.success) {
216
+ return res.badRequest("Invalid shard stats", {
217
+ details: parseResult.error
185
218
  });
186
219
  }
187
220
 
188
- const body = bodyResult.data;
221
+ const body = parseResult.data;
189
222
  const { shardId, connections, status } = body;
190
223
  const shard = this.shards()[shardId];
191
224
 
192
225
  if (!shard) {
193
226
  return res.notFound(`Shard ${shardId} not found`);
194
227
  }
195
-
228
+
229
+ if (body.worldId && body.worldId !== this.getWorldId()) {
230
+ return res.badRequest(`Shard ${shardId} belongs to world ${body.worldId}, not ${this.getWorldId()}`);
231
+ }
232
+
196
233
  shard.currentConnections.set(connections);
197
234
  if (status) {
198
235
  shard.status.set(status);
199
236
  }
200
237
  shard.lastHeartbeat.set(Date.now());
238
+
239
+ if (this.shouldCompleteDrain(shard)) {
240
+ this.removeShard(shard.id);
241
+ }
201
242
  }
202
-
243
+
203
244
  @Request({
204
245
  path: 'scale-room',
205
246
  method: 'POST',
206
247
  })
207
248
  @Guard([guardManageWorld])
208
249
  async scaleRoom(req: Party.Request, res: ServerResponse) {
209
- const dataResult = ScaleRoomSchema.safeParse(await req.json());
210
- if (!dataResult.success) {
211
- return res.badRequest("Invalid scale request", {
212
- details: dataResult.error
250
+ const parseResult = ScaleRoomSchema.safeParse(await req.json());
251
+ if (!parseResult.success) {
252
+ return res.badRequest("Invalid scale room request", {
253
+ details: parseResult.error
213
254
  });
214
255
  }
215
256
 
216
- const data = dataResult.data;
257
+ const data = parseResult.data;
217
258
  const { targetShardCount, shardTemplate, roomId } = data;
218
-
259
+
219
260
  // Validate room exists
220
261
  const room = this.rooms()[roomId];
221
262
  if (!room) {
222
263
  return res.notFound(`Room ${roomId} does not exist`);
223
264
  }
224
-
265
+
225
266
  const roomShards = Object.values(this.shards())
226
267
  .filter(shard => shard.roomId() === roomId);
227
-
268
+
228
269
  const previousShardCount = roomShards.length;
229
-
270
+
230
271
  // Check max shards constraint
231
272
  if (room.maxShards() !== undefined && targetShardCount > room.maxShards()!) {
232
273
  return res.badRequest(`Cannot scale beyond maximum allowed shards (${room.maxShards()})`, {
@@ -234,33 +275,36 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
234
275
  currentShardCount: previousShardCount
235
276
  });
236
277
  }
237
-
278
+
238
279
  // Handle scaling down
239
280
  if (targetShardCount < previousShardCount) {
240
- // Find candidates for removal (prioritize draining or low-connection shards)
241
- const shardsToRemove = [...roomShards]
281
+ // Find drain candidates (prioritize already-draining or low-connection shards)
282
+ const shardsToDrain = [...roomShards]
242
283
  .sort((a, b) => {
243
284
  // Prioritize draining status
244
285
  if (a.status() === 'draining' && b.status() !== 'draining') return -1;
245
286
  if (a.status() !== 'draining' && b.status() === 'draining') return 1;
246
-
287
+
247
288
  // Then by connection count (ascending)
248
289
  return a.currentConnections() - b.currentConnections();
249
290
  })
250
291
  .slice(0, previousShardCount - targetShardCount);
251
-
252
- // Update shards
253
- for (const shard of shardsToRemove) {
254
- delete this.shards()[shard.id];
292
+
293
+ // Complete empty drains immediately, otherwise stop routing new clients there.
294
+ for (const shard of shardsToDrain) {
295
+ shard.status.set('draining');
296
+ if (this.shouldCompleteDrain(shard)) {
297
+ this.removeShard(shard.id);
298
+ }
255
299
  }
256
-
300
+
257
301
  return;
258
302
  }
259
-
303
+
260
304
  // Handle scaling up
261
305
  if (targetShardCount > previousShardCount) {
262
306
  const newShards = [];
263
-
307
+
264
308
  // Create new shards
265
309
  for (let i = 0; i < targetShardCount - previousShardCount; i++) {
266
310
  const newShard = await this.createShard(
@@ -268,7 +312,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
268
312
  shardTemplate?.urlTemplate,
269
313
  shardTemplate?.maxConnections
270
314
  );
271
-
315
+
272
316
  if (newShard) {
273
317
  newShards.push(newShard);
274
318
  }
@@ -284,35 +328,35 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
284
328
  try {
285
329
  // Extract request data
286
330
  let data: { roomId: string; autoCreate?: boolean };
287
-
331
+
288
332
  try {
289
333
  // Handle potential empty body or malformed JSON
290
334
  const body = await req.text();
291
335
  if (!body || body.trim() === '') {
292
336
  return res.badRequest("Request body is empty");
293
337
  }
294
-
338
+
295
339
  data = JSON.parse(body);
296
340
  } catch (parseError) {
297
341
  return res.badRequest("Invalid JSON in request body");
298
342
  }
299
-
343
+
300
344
  // Verify roomId is provided
301
345
  if (!data.roomId) {
302
346
  return res.badRequest("roomId parameter is required");
303
347
  }
304
-
348
+
305
349
  // Determine if auto-creation is enabled (default to true)
306
350
  const autoCreate = data.autoCreate !== undefined ? data.autoCreate : true;
307
-
351
+
308
352
  // Find optimal shard
309
353
  const result = await this.findOptimalShard(data.roomId, autoCreate);
310
-
354
+
311
355
  // Check for errors
312
356
  if ('error' in result) {
313
357
  return res.notFound(result.error);
314
358
  }
315
-
359
+
316
360
  // Return shard information to the client
317
361
  return res.success({
318
362
  success: true,
@@ -324,9 +368,9 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
324
368
  return res.serverError();
325
369
  }
326
370
  }
327
-
371
+
328
372
  private async findOptimalShard(
329
- roomId: string,
373
+ roomId: string,
330
374
  autoCreate: boolean = true
331
375
  ): Promise<{ shardId: string; url: string } | { error: string }> {
332
376
  // Ensure room exists
@@ -343,11 +387,11 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
343
387
  maxShards: undefined
344
388
  })
345
389
  } as Party.Request;
346
-
390
+
347
391
  await this.registerRoom(mockRequest);
348
-
392
+
349
393
  room = this.rooms()[roomId];
350
-
394
+
351
395
  if (!room) {
352
396
  return { error: `Failed to create room ${roomId}` };
353
397
  }
@@ -355,11 +399,11 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
355
399
  return { error: `Room ${roomId} does not exist` };
356
400
  }
357
401
  }
358
-
402
+
359
403
  // Get shards for this room
360
404
  const roomShards = Object.values(this.shards())
361
405
  .filter(shard => shard.roomId() === roomId);
362
-
406
+
363
407
  if (roomShards.length === 0) {
364
408
  if (autoCreate) {
365
409
  // Auto-create a shard
@@ -376,51 +420,76 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
376
420
  return { error: `No shards available for room ${roomId}` };
377
421
  }
378
422
  }
379
-
380
- // Get active shards
381
- const activeShards = roomShards
382
- .filter(shard => shard && shard.status() === 'active');
383
-
423
+
424
+ // Get active shards with available capacity
425
+ let activeShards = this.getAvailableShards(roomShards);
426
+
384
427
  if (activeShards.length === 0) {
428
+ if (autoCreate && this.canCreateShard(room, roomShards.length)) {
429
+ const newShard = await this.createShard(roomId);
430
+ if (newShard) {
431
+ return {
432
+ shardId: newShard.id,
433
+ url: newShard.url()
434
+ };
435
+ }
436
+ }
437
+
438
+ if (roomShards.some(shard => shard.status() === 'active')) {
439
+ return { error: `No shard capacity available for room ${roomId}` };
440
+ }
441
+
385
442
  return { error: `No active shards available for room ${roomId}` };
386
443
  }
387
-
444
+
388
445
  // Apply balancing strategy
389
446
  const balancingStrategy = room.balancingStrategy();
390
447
  let selectedShard: ShardInfo;
391
-
448
+
392
449
  switch (balancingStrategy) {
393
450
  case 'least-connections':
394
451
  // Choose shard with fewest connections
395
452
  selectedShard = activeShards.reduce(
396
- (min, shard) =>
453
+ (min, shard) =>
397
454
  shard.currentConnections() < min.currentConnections() ? shard : min,
398
455
  activeShards[0]
399
456
  );
400
457
  break;
401
-
458
+
402
459
  case 'random':
403
460
  // Choose random shard
404
461
  selectedShard = activeShards[Math.floor(Math.random() * activeShards.length)];
405
462
  break;
406
-
463
+
407
464
  case 'round-robin':
408
465
  default:
409
466
  // Round-robin selection
410
467
  const counter = this.rrCounters()[roomId] || 0;
411
468
  const nextCounter = (counter + 1) % activeShards.length;
412
469
  this.rrCounters()[roomId] = nextCounter;
413
-
470
+
414
471
  selectedShard = activeShards[counter];
415
472
  break;
416
473
  }
417
-
474
+
418
475
  return {
419
476
  shardId: selectedShard.id,
420
477
  url: selectedShard.url()
421
478
  };
422
479
  }
423
-
480
+
481
+ private getAvailableShards(shards: ShardInfo[]) {
482
+ return shards.filter(shard =>
483
+ shard
484
+ && shard.status() === 'active'
485
+ && shard.currentConnections() < shard.maxConnections()
486
+ );
487
+ }
488
+
489
+ private canCreateShard(room: RoomConfig, currentShardCount: number) {
490
+ return room.maxShards() === undefined || currentShardCount < room.maxShards()!;
491
+ }
492
+
424
493
  // Private methods
425
494
  private async createShard(
426
495
  roomId: string,
@@ -432,29 +501,51 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
432
501
  console.error(`Cannot create shard for non-existent room: ${roomId}`);
433
502
  return null;
434
503
  }
435
-
504
+
436
505
  // Generate shard ID
437
- const shardId = `${roomId}:${Date.now()}-${Math.floor(Math.random() * 10000)}`;
438
-
506
+ const worldId = this.getWorldId();
507
+ const shardId = `${roomId}:${worldId}:${Date.now()}-${Math.floor(Math.random() * 10000)}`;
508
+
439
509
  // Generate URL from template
440
510
  const template = urlTemplate || this.defaultShardUrlTemplate();
441
511
  const url = template.replace('{shardId}', shardId).replace('{roomId}', roomId);
442
512
 
443
513
  // Set max connections
444
514
  const max = maxConnections || room.maxPlayersPerShard();
445
-
515
+
446
516
  // Create the shard
447
517
  const newShard = new ShardInfo();
448
518
  newShard.id = shardId;
449
519
  newShard.roomId.set(roomId);
520
+ newShard.worldId.set(worldId);
450
521
  newShard.url.set(url);
451
522
  newShard.maxConnections.set(max);
452
523
  newShard.currentConnections.set(0);
453
524
  newShard.status.set("active");
454
525
  newShard.lastHeartbeat.set(Date.now());
455
-
526
+
456
527
  // Update shards collection
457
528
  this.shards()[shardId] = newShard;
458
529
  return newShard;
459
530
  }
531
+
532
+ private async ensureMinShards(roomId: string) {
533
+ const room = this.rooms()[roomId];
534
+ if (!room) {
535
+ return;
536
+ }
537
+
538
+ const currentShardCount = Object.values(this.shards())
539
+ .filter(shard => shard.roomId() === roomId)
540
+ .length;
541
+ const targetShardCount = room.minShards();
542
+
543
+ if (currentShardCount >= targetShardCount) {
544
+ return;
545
+ }
546
+
547
+ for (let i = currentShardCount; i < targetShardCount; i++) {
548
+ await this.createShard(roomId);
549
+ }
550
+ }
460
551
  }
@@ -1,11 +0,0 @@
1
- {
2
- "configurations": [
3
- {
4
- "type": "node",
5
- "request": "attach",
6
- "name": "PartyKit debugger",
7
- "address": "localhost",
8
- "port": 9229
9
- }
10
- ]
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "files.associations": {
3
- "partykit.json": "jsonc"
4
- },
5
- "json.schemas": [
6
- {
7
- "fileMatch": ["partykit.json"],
8
- "url": "https://www.partykit.io/schema.json"
9
- }
10
- ]
11
- }
@@ -1,40 +0,0 @@
1
- # 🎈 game
2
-
3
- Welcome to the party, pal!
4
-
5
- This is a [Partykit](https://partykit.io) project, which lets you create real-time collaborative applications with minimal coding effort.
6
-
7
- This is the **React starter** which pairs a PartyKit server with a React client.
8
-
9
- Refer to our docs for more information: https://github.com/partykit/partykit/blob/main/README.md. For more help, reach out to us on [Discord](https://discord.gg/g5uqHQJc3z), [GitHub](https://github.com/partykit/partykit), or [Twitter](https://twitter.com/partykit_io).
10
-
11
- ## Usage
12
-
13
- You can start developing by running `npm run dev` and opening [http://localhost:1999](http://localhost:1999) in your browser. When you're ready, you can deploy your application on to the PartyKit cloud with `npm run deploy`.
14
-
15
- ## Finding your way around
16
-
17
- [`party/server.ts`](./party/server.ts) is the server-side code, which is responsible for handling WebSocket events and HTTP requests.
18
-
19
- It implements a simple counter that can be incremented by any connected client. The latest state is broadcast to all connected clients.
20
-
21
- > [!NOTE]
22
- > The full Server API is available at [Party.Server in the PartyKit docs](https://docs.partykit.io/reference/partyserver-api/)
23
-
24
- [`app/client.tsx`](./src/client.ts) is the entrypoint to client-side code.
25
-
26
- [`app/components/Counter.tsx`](./src/components/Counter.tsx) connects to the server, sends `increment` events on the WebSocket, and listens for updates.
27
-
28
- > [!NOTE]
29
- > The client-side reference can be found at [PartySocket in the PartyKit docs](https://docs.partykit.io/reference/partysocket-api/)
30
-
31
- As a client-side React app, the app could be hosted every. During development, for convenience, the server serves the client-side code as well.
32
-
33
- This is achieved with the optional `serve` property in the [`partykit.json`](./partykit.json) config file.
34
-
35
- > [!NOTE]
36
- > Learn about PartyKit config under [Configuration in the PartyKit docs](https://docs.partykit.io/reference/partykit-configuration/)
37
-
38
- ## Next Steps
39
-
40
- Learn about deploying PartyKit applications in the [Deployment guide of the PartyKit docs](https://docs.partykit.io/guides/deploying-your-partykit-server/).
@@ -1,15 +0,0 @@
1
- import { createRoot } from "react-dom/client";
2
- import Admin from "./components/Admin";
3
- import "./styles.css";
4
- import Room from "./components/Room";
5
-
6
-
7
- function App() {
8
- return (
9
- <main>
10
- <Room />
11
- </main>
12
- );
13
- }
14
-
15
- createRoot(document.getElementById("app")!).render(<App />);