@signe/room 2.10.0 → 3.0.1
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/CHANGELOG.md +7 -0
- package/dist/chunk-EUXUH3YW.js +15 -0
- package/dist/chunk-EUXUH3YW.js.map +1 -0
- package/dist/cloudflare/index.d.ts +71 -0
- package/dist/cloudflare/index.js +320 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/index.d.ts +87 -188
- package/dist/index.js +860 -114
- package/dist/index.js.map +1 -1
- package/dist/node/index.d.ts +164 -0
- package/dist/node/index.js +786 -0
- package/dist/node/index.js.map +1 -0
- package/dist/party-dNs-hqkq.d.ts +175 -0
- package/examples/cloudflare/README.md +62 -0
- package/examples/cloudflare/node_modules/.bin/tsc +17 -0
- package/examples/cloudflare/node_modules/.bin/tsserver +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler2 +17 -0
- package/examples/cloudflare/package.json +24 -0
- package/examples/cloudflare/public/index.html +443 -0
- package/examples/cloudflare/src/index.ts +28 -0
- package/examples/cloudflare/src/room.ts +44 -0
- package/examples/cloudflare/tsconfig.json +10 -0
- package/examples/cloudflare/wrangler.jsonc +25 -0
- package/examples/node/README.md +57 -0
- package/examples/node/node_modules/.bin/tsc +17 -0
- package/examples/node/node_modules/.bin/tsserver +17 -0
- package/examples/node/node_modules/.bin/tsx +17 -0
- package/examples/node/package.json +23 -0
- package/examples/node/public/index.html +443 -0
- package/examples/node/room.ts +44 -0
- package/examples/node/server.sqlite.ts +52 -0
- package/examples/node/server.ts +51 -0
- package/examples/node/tsconfig.json +10 -0
- package/examples/node-game/README.md +66 -0
- package/examples/node-game/package.json +23 -0
- package/examples/node-game/public/index.html +705 -0
- package/examples/node-game/room.ts +145 -0
- package/examples/node-game/server.sqlite.ts +54 -0
- package/examples/node-game/server.ts +53 -0
- package/examples/node-game/tsconfig.json +10 -0
- package/examples/node-shard/README.md +32 -0
- package/examples/node-shard/dev.ts +39 -0
- package/examples/node-shard/package.json +24 -0
- package/examples/node-shard/public/index.html +777 -0
- package/examples/node-shard/room-server.ts +68 -0
- package/examples/node-shard/room.ts +105 -0
- package/examples/node-shard/shared.ts +6 -0
- package/examples/node-shard/tsconfig.json +14 -0
- package/examples/node-shard/world-server.ts +169 -0
- package/package.json +14 -5
- package/readme.md +418 -4
- package/src/cloudflare/index.ts +474 -0
- package/src/index.ts +2 -2
- package/src/jwt.ts +1 -5
- package/src/mock.ts +29 -7
- package/src/node/index.ts +1112 -0
- package/src/server.ts +781 -60
- package/src/session.guard.ts +6 -2
- package/src/shard.ts +91 -23
- package/src/storage.ts +29 -5
- package/src/testing.ts +4 -3
- package/src/types/party.ts +30 -1
- package/src/world.guard.ts +23 -4
- package/src/world.ts +121 -21
- package/tests/storage-restore.spec.ts +122 -0
- package/examples/game/.vscode/launch.json +0 -11
- package/examples/game/.vscode/settings.json +0 -11
- package/examples/game/README.md +0 -40
- package/examples/game/app/client.tsx +0 -15
- package/examples/game/app/components/Admin.tsx +0 -1089
- package/examples/game/app/components/Room.tsx +0 -162
- package/examples/game/app/styles.css +0 -31
- package/examples/game/package-lock.json +0 -225
- package/examples/game/package.json +0 -20
- package/examples/game/party/game.room.ts +0 -32
- package/examples/game/party/server.ts +0 -10
- package/examples/game/party/shard.ts +0 -5
- package/examples/game/partykit.json +0 -14
- package/examples/game/public/favicon.ico +0 -0
- package/examples/game/public/index.html +0 -27
- package/examples/game/public/normalize.css +0 -351
- package/examples/game/shared/room.schema.ts +0 -14
- package/examples/game/tsconfig.json +0 -109
package/src/world.ts
CHANGED
|
@@ -27,11 +27,14 @@ const RoomConfigSchema = z.object({
|
|
|
27
27
|
const RegisterShardSchema = z.object({
|
|
28
28
|
shardId: z.string(),
|
|
29
29
|
roomId: z.string(),
|
|
30
|
+
worldId: z.string().optional(),
|
|
30
31
|
url: z.string().url(),
|
|
31
32
|
maxConnections: z.number().int().positive(),
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
const UpdateShardStatsSchema = z.object({
|
|
36
|
+
shardId: z.string(),
|
|
37
|
+
worldId: z.string().optional(),
|
|
35
38
|
connections: z.number().int().min(0),
|
|
36
39
|
status: z.enum(['active', 'maintenance', 'draining']).optional(),
|
|
37
40
|
});
|
|
@@ -59,6 +62,7 @@ class RoomConfig {
|
|
|
59
62
|
class ShardInfo {
|
|
60
63
|
@id() id: string;
|
|
61
64
|
@sync() roomId = signal("");
|
|
65
|
+
@sync() worldId = signal("");
|
|
62
66
|
@sync() url = signal("");
|
|
63
67
|
@sync({
|
|
64
68
|
persist: false
|
|
@@ -95,8 +99,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
95
99
|
if (!SHARD_SECRET) {
|
|
96
100
|
throw new Error("SHARD_SECRET env variable is not set");
|
|
97
101
|
}
|
|
98
|
-
|
|
99
|
-
//setTimeout(() => this.cleanupInactiveShards(), 60000);
|
|
102
|
+
this.scheduleInactiveShardCleanup();
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
async onJoin(user: any, conn: Party.Connection, ctx: Party.ConnectionContext) {
|
|
@@ -115,6 +118,15 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
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
|
+
|
|
118
130
|
private cleanupInactiveShards() {
|
|
119
131
|
const now = Date.now();
|
|
120
132
|
const timeout = 5 * 60 * 1000; // 5 minutes timeout
|
|
@@ -130,7 +142,15 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
130
142
|
});
|
|
131
143
|
|
|
132
144
|
// Schedule next cleanup
|
|
133
|
-
|
|
145
|
+
this.scheduleInactiveShardCleanup();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private removeShard(shardId: string) {
|
|
149
|
+
delete this.shards()[shardId];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private shouldCompleteDrain(shard: ShardInfo) {
|
|
153
|
+
return shard.status() === 'draining' && shard.currentConnections() === 0;
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
// Actions
|
|
@@ -139,8 +159,19 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
139
159
|
method: 'POST',
|
|
140
160
|
})
|
|
141
161
|
@Guard([guardManageWorld])
|
|
142
|
-
async registerRoom(req: Party.Request) {
|
|
143
|
-
const
|
|
162
|
+
async registerRoom(req: Party.Request, res?: ServerResponse) {
|
|
163
|
+
const parseResult = RoomConfigSchema.safeParse(await req.json());
|
|
164
|
+
if (!parseResult.success) {
|
|
165
|
+
return res?.badRequest("Invalid room configuration", {
|
|
166
|
+
details: parseResult.error
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
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]) {
|
|
@@ -170,6 +201,8 @@ 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({
|
|
@@ -178,7 +211,14 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
178
211
|
})
|
|
179
212
|
@Guard([guardManageWorld])
|
|
180
213
|
async updateShardStats(req: Party.Request, res: ServerResponse) {
|
|
181
|
-
const
|
|
214
|
+
const parseResult = UpdateShardStatsSchema.safeParse(await req.json());
|
|
215
|
+
if (!parseResult.success) {
|
|
216
|
+
return res.badRequest("Invalid shard stats", {
|
|
217
|
+
details: parseResult.error
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const body = parseResult.data;
|
|
182
222
|
const { shardId, connections, status } = body;
|
|
183
223
|
const shard = this.shards()[shardId];
|
|
184
224
|
|
|
@@ -186,11 +226,19 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
186
226
|
return res.notFound(`Shard ${shardId} not found`);
|
|
187
227
|
}
|
|
188
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
|
+
|
|
189
233
|
shard.currentConnections.set(connections);
|
|
190
234
|
if (status) {
|
|
191
235
|
shard.status.set(status);
|
|
192
236
|
}
|
|
193
237
|
shard.lastHeartbeat.set(Date.now());
|
|
238
|
+
|
|
239
|
+
if (this.shouldCompleteDrain(shard)) {
|
|
240
|
+
this.removeShard(shard.id);
|
|
241
|
+
}
|
|
194
242
|
}
|
|
195
243
|
|
|
196
244
|
@Request({
|
|
@@ -199,7 +247,14 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
199
247
|
})
|
|
200
248
|
@Guard([guardManageWorld])
|
|
201
249
|
async scaleRoom(req: Party.Request, res: ServerResponse) {
|
|
202
|
-
const
|
|
250
|
+
const parseResult = ScaleRoomSchema.safeParse(await req.json());
|
|
251
|
+
if (!parseResult.success) {
|
|
252
|
+
return res.badRequest("Invalid scale room request", {
|
|
253
|
+
details: parseResult.error
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const data = parseResult.data;
|
|
203
258
|
const { targetShardCount, shardTemplate, roomId } = data;
|
|
204
259
|
|
|
205
260
|
// Validate room exists
|
|
@@ -223,8 +278,8 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
223
278
|
|
|
224
279
|
// Handle scaling down
|
|
225
280
|
if (targetShardCount < previousShardCount) {
|
|
226
|
-
// Find candidates
|
|
227
|
-
const
|
|
281
|
+
// Find drain candidates (prioritize already-draining or low-connection shards)
|
|
282
|
+
const shardsToDrain = [...roomShards]
|
|
228
283
|
.sort((a, b) => {
|
|
229
284
|
// Prioritize draining status
|
|
230
285
|
if (a.status() === 'draining' && b.status() !== 'draining') return -1;
|
|
@@ -235,14 +290,12 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
235
290
|
})
|
|
236
291
|
.slice(0, previousShardCount - targetShardCount);
|
|
237
292
|
|
|
238
|
-
//
|
|
239
|
-
const
|
|
240
|
-
shard
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
for (const shard of shardsToRemove) {
|
|
245
|
-
delete this.shards()[shard.id];
|
|
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
|
+
}
|
|
246
299
|
}
|
|
247
300
|
|
|
248
301
|
return;
|
|
@@ -368,11 +421,24 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
368
421
|
}
|
|
369
422
|
}
|
|
370
423
|
|
|
371
|
-
// Get active shards
|
|
372
|
-
|
|
373
|
-
.filter(shard => shard && shard.status() === 'active');
|
|
424
|
+
// Get active shards with available capacity
|
|
425
|
+
let activeShards = this.getAvailableShards(roomShards);
|
|
374
426
|
|
|
375
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
|
+
|
|
376
442
|
return { error: `No active shards available for room ${roomId}` };
|
|
377
443
|
}
|
|
378
444
|
|
|
@@ -412,6 +478,18 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
412
478
|
};
|
|
413
479
|
}
|
|
414
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
|
+
|
|
415
493
|
// Private methods
|
|
416
494
|
private async createShard(
|
|
417
495
|
roomId: string,
|
|
@@ -425,7 +503,8 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
425
503
|
}
|
|
426
504
|
|
|
427
505
|
// Generate shard ID
|
|
428
|
-
const
|
|
506
|
+
const worldId = this.getWorldId();
|
|
507
|
+
const shardId = `${roomId}:${worldId}:${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
|
429
508
|
|
|
430
509
|
// Generate URL from template
|
|
431
510
|
const template = urlTemplate || this.defaultShardUrlTemplate();
|
|
@@ -438,6 +517,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
438
517
|
const newShard = new ShardInfo();
|
|
439
518
|
newShard.id = shardId;
|
|
440
519
|
newShard.roomId.set(roomId);
|
|
520
|
+
newShard.worldId.set(worldId);
|
|
441
521
|
newShard.url.set(url);
|
|
442
522
|
newShard.maxConnections.set(max);
|
|
443
523
|
newShard.currentConnections.set(0);
|
|
@@ -448,4 +528,24 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
448
528
|
this.shards()[shardId] = newShard;
|
|
449
529
|
return newShard;
|
|
450
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
|
+
}
|
|
451
551
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { signal } from "@signe/reactive";
|
|
3
|
+
import { sync, users } from "@signe/sync";
|
|
4
|
+
import { Room, Server, ServerIo } from "../src";
|
|
5
|
+
|
|
6
|
+
class Item {
|
|
7
|
+
id = signal("");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class Player {
|
|
11
|
+
items = signal<any[]>([]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("storage restore hooks", () => {
|
|
15
|
+
it("allows a room to hydrate the persisted root snapshot before load", async () => {
|
|
16
|
+
@Room({ path: "demo" })
|
|
17
|
+
class DemoRoom {
|
|
18
|
+
@sync()
|
|
19
|
+
title = signal("");
|
|
20
|
+
|
|
21
|
+
onStorageRestore({ snapshot }: { snapshot: any }) {
|
|
22
|
+
return {
|
|
23
|
+
...snapshot,
|
|
24
|
+
title: `${snapshot.title}:hydrated`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class DemoServer extends Server {
|
|
30
|
+
rooms = [DemoRoom];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const io = new ServerIo("demo");
|
|
34
|
+
await io.storage.put("state:title", "saved");
|
|
35
|
+
|
|
36
|
+
const server = new DemoServer(io as any);
|
|
37
|
+
await server.onStart();
|
|
38
|
+
|
|
39
|
+
expect((server.subRoom as any).title()).toBe("saved:hydrated");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("allows a room to hydrate persisted user snapshots before load", async () => {
|
|
43
|
+
@Room({ path: "demo" })
|
|
44
|
+
class DemoRoom {
|
|
45
|
+
@users(Player)
|
|
46
|
+
players = signal<Record<string, Player>>({});
|
|
47
|
+
|
|
48
|
+
async onUserStorageRestore({ userSnapshot, user }: { userSnapshot: any; user?: Player }) {
|
|
49
|
+
return {
|
|
50
|
+
...userSnapshot,
|
|
51
|
+
items: userSnapshot.items.map((entry: any) => {
|
|
52
|
+
const item = new Item();
|
|
53
|
+
item.id.set(entry.id);
|
|
54
|
+
return item;
|
|
55
|
+
}),
|
|
56
|
+
usedHelperInstance: user instanceof Player,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class DemoServer extends Server {
|
|
62
|
+
rooms = [DemoRoom];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const io = new ServerIo("demo");
|
|
66
|
+
await io.storage.put("state:players.public-1.items", [{ id: "potion" }]);
|
|
67
|
+
|
|
68
|
+
const server = new DemoServer(io as any);
|
|
69
|
+
await server.onStart();
|
|
70
|
+
|
|
71
|
+
const restoredPlayer = (server.subRoom as any).players()["public-1"];
|
|
72
|
+
expect(restoredPlayer).toBeInstanceOf(Player);
|
|
73
|
+
expect(restoredPlayer.items()[0]).toBeInstanceOf(Item);
|
|
74
|
+
expect(restoredPlayer.items()[0].id()).toBe("potion");
|
|
75
|
+
expect((restoredPlayer as any).usedHelperInstance).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("compacts persisted delete markers when loading room storage", async () => {
|
|
79
|
+
@Room({ path: "demo" })
|
|
80
|
+
class DemoRoom {
|
|
81
|
+
@sync()
|
|
82
|
+
items = signal<Record<string, number>>({});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class DemoServer extends Server {
|
|
86
|
+
rooms = [DemoRoom];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const io = new ServerIo("demo");
|
|
90
|
+
await io.storage.put("state:items.a", "$delete");
|
|
91
|
+
|
|
92
|
+
const server = new DemoServer(io as any);
|
|
93
|
+
await server.onStart();
|
|
94
|
+
|
|
95
|
+
const storageEntries = await io.storage.list({ prefix: "state:" });
|
|
96
|
+
expect(storageEntries.get("state:items.a")).toBeUndefined();
|
|
97
|
+
expect(storageEntries.get("state:.")).toEqual({ items: {} });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("compacts runtime deletes instead of leaving delete markers in storage", async () => {
|
|
101
|
+
@Room({ path: "demo" })
|
|
102
|
+
class DemoRoom {
|
|
103
|
+
@sync()
|
|
104
|
+
items = signal<Record<string, number>>({ a: 1, b: 2 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class DemoServer extends Server {
|
|
108
|
+
rooms = [DemoRoom];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const io = new ServerIo("demo");
|
|
112
|
+
const server = new DemoServer(io as any);
|
|
113
|
+
await server.onStart();
|
|
114
|
+
|
|
115
|
+
delete (server.subRoom as any).items().a;
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
117
|
+
|
|
118
|
+
const storageEntries = await io.storage.list({ prefix: "state:" });
|
|
119
|
+
expect(storageEntries.get("state:items.a")).toBeUndefined();
|
|
120
|
+
expect(storageEntries.get("state:.")).toEqual({ items: { b: 2 } });
|
|
121
|
+
});
|
|
122
|
+
});
|
package/examples/game/README.md
DELETED
|
@@ -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 />);
|