@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
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DurableObjectNamespace,
|
|
3
|
+
DurableObjectState,
|
|
4
|
+
WebSocket as CloudflareWebSocket,
|
|
5
|
+
} from "@cloudflare/workers-types";
|
|
6
|
+
import type * as Party from "../types/party";
|
|
7
|
+
|
|
8
|
+
export type CloudflareRoomServerConstructor<TServer extends Party.Server = Party.Server> = {
|
|
9
|
+
new (room: Party.Room): TServer;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type CloudflareRoomWorkerOptions = {
|
|
13
|
+
binding: string;
|
|
14
|
+
partiesPath?: string;
|
|
15
|
+
env?: Record<string, unknown>;
|
|
16
|
+
rooms?: Record<string, CloudflareRoomServerConstructor>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CloudflareRoomEnv = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
type ParsedPartyPath = {
|
|
22
|
+
namespace: string;
|
|
23
|
+
roomId: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CloudflareRoomRecord = {
|
|
27
|
+
room: CloudflareRoom;
|
|
28
|
+
server: Party.Server;
|
|
29
|
+
started: Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type CloudflareRuntimeConfig = Required<Pick<CloudflareRoomWorkerOptions, "binding" | "partiesPath">> & {
|
|
33
|
+
ServerClass: CloudflareRoomServerConstructor;
|
|
34
|
+
env: Record<string, unknown>;
|
|
35
|
+
rooms: Record<string, CloudflareRoomServerConstructor>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DEFAULT_PARTIES_PATH = "/parties/main";
|
|
39
|
+
const WEBSOCKET_OPEN = 1;
|
|
40
|
+
|
|
41
|
+
let runtimeConfig: CloudflareRuntimeConfig | undefined;
|
|
42
|
+
|
|
43
|
+
export function createCloudflareRoomWorker<TServer extends Party.Server>(
|
|
44
|
+
ServerClass: CloudflareRoomServerConstructor<TServer>,
|
|
45
|
+
options: CloudflareRoomWorkerOptions
|
|
46
|
+
) {
|
|
47
|
+
runtimeConfig = createRuntimeConfig(ServerClass, options);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
async fetch(
|
|
51
|
+
request: Request,
|
|
52
|
+
env: CloudflareRoomEnv,
|
|
53
|
+
ctx: unknown
|
|
54
|
+
): Promise<Response> {
|
|
55
|
+
return dispatchCloudflareRoomRequest(request, env, ctx);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function dispatchCloudflareRoomRequest(
|
|
61
|
+
request: Request,
|
|
62
|
+
env: CloudflareRoomEnv,
|
|
63
|
+
_ctx?: unknown
|
|
64
|
+
): Promise<Response> {
|
|
65
|
+
const config = getRuntimeConfig();
|
|
66
|
+
const parsed = parsePartyRequest(request.url, config.partiesPath);
|
|
67
|
+
|
|
68
|
+
if (!parsed) {
|
|
69
|
+
return new Response("Not Found", { status: 404 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const namespace = getNamespace(env, config.binding);
|
|
73
|
+
const stub = namespace.get(namespace.idFromName(parsed.roomId));
|
|
74
|
+
return fetchDurableObjectStub(stub, request);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class SigneRoomDurableObject {
|
|
78
|
+
private recordPromise?: Promise<CloudflareRoomRecord>;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly ctx: DurableObjectState,
|
|
82
|
+
private readonly env: CloudflareRoomEnv
|
|
83
|
+
) {}
|
|
84
|
+
|
|
85
|
+
async fetch(request: Request): Promise<Response> {
|
|
86
|
+
const config = getRuntimeConfig();
|
|
87
|
+
const parsed = parsePartyRequest(request.url, config.partiesPath);
|
|
88
|
+
|
|
89
|
+
if (!parsed) {
|
|
90
|
+
return new Response("Not Found", { status: 404 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isWebSocketUpgrade(request)) {
|
|
94
|
+
return this.acceptWebSocket(request, parsed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const record = await this.getRecord(parsed);
|
|
98
|
+
return record.server.onRequest?.(request as unknown as Party.Request)
|
|
99
|
+
?? new Response("Not Found", { status: 404 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async alarm(): Promise<void> {
|
|
103
|
+
const record = await this.recordPromise;
|
|
104
|
+
await record?.server.onAlarm?.();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async acceptWebSocket(
|
|
108
|
+
request: Request,
|
|
109
|
+
parsed: ParsedPartyPath
|
|
110
|
+
): Promise<Response> {
|
|
111
|
+
const pair = new WebSocketPair();
|
|
112
|
+
const [client, server] = Object.values(pair) as [
|
|
113
|
+
CloudflareWebSocket,
|
|
114
|
+
CloudflareWebSocket,
|
|
115
|
+
];
|
|
116
|
+
const record = await this.getRecord(parsed);
|
|
117
|
+
const connection = new CloudflareConnection(
|
|
118
|
+
server,
|
|
119
|
+
request.url,
|
|
120
|
+
getConnectionIdFromUrl(request.url)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
server.accept();
|
|
124
|
+
|
|
125
|
+
await record.server.onConnect?.(connection as unknown as Party.Connection, {
|
|
126
|
+
request: request as unknown as Party.Request,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
server.addEventListener("message", (event) => {
|
|
130
|
+
void record.server.onMessage?.(
|
|
131
|
+
normalizeWebSocketMessage(event.data),
|
|
132
|
+
connection as unknown as Party.Connection
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
server.addEventListener("close", () => {
|
|
136
|
+
record.room.deleteConnection(connection.id, connection);
|
|
137
|
+
void record.server.onClose?.(connection as unknown as Party.Connection);
|
|
138
|
+
});
|
|
139
|
+
server.addEventListener("error", (event) => {
|
|
140
|
+
const errorData = (event as { error?: unknown; message?: string }).error;
|
|
141
|
+
const error = errorData instanceof Error
|
|
142
|
+
? errorData
|
|
143
|
+
: new Error((event as { message?: string }).message ?? "Cloudflare WebSocket error");
|
|
144
|
+
void record.server.onError?.(connection as unknown as Party.Connection, error);
|
|
145
|
+
});
|
|
146
|
+
record.room.addConnection(connection);
|
|
147
|
+
|
|
148
|
+
return new Response(null, {
|
|
149
|
+
status: 101,
|
|
150
|
+
webSocket: client,
|
|
151
|
+
} as ResponseInit & { webSocket: CloudflareWebSocket });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async getRecord(parsed: ParsedPartyPath): Promise<CloudflareRoomRecord> {
|
|
155
|
+
if (!this.recordPromise) {
|
|
156
|
+
this.recordPromise = this.createRecord(parsed);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return this.recordPromise;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async createRecord(parsed: ParsedPartyPath): Promise<CloudflareRoomRecord> {
|
|
163
|
+
const config = getRuntimeConfig();
|
|
164
|
+
const ServerClass = config.rooms[parsed.namespace] ?? config.ServerClass;
|
|
165
|
+
const room = new CloudflareRoom({
|
|
166
|
+
id: parsed.roomId,
|
|
167
|
+
name: parsed.namespace,
|
|
168
|
+
env: {
|
|
169
|
+
...config.env,
|
|
170
|
+
...this.env,
|
|
171
|
+
},
|
|
172
|
+
state: this.ctx,
|
|
173
|
+
binding: config.binding,
|
|
174
|
+
partiesPath: config.partiesPath,
|
|
175
|
+
});
|
|
176
|
+
const server = new ServerClass(room as Party.Room);
|
|
177
|
+
const record: CloudflareRoomRecord = {
|
|
178
|
+
room,
|
|
179
|
+
server,
|
|
180
|
+
started: Promise.resolve(server.onStart?.()).then(() => undefined),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await record.started;
|
|
184
|
+
return record;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export class CloudflareRoom implements Party.Room {
|
|
189
|
+
readonly id: string;
|
|
190
|
+
readonly internalID: string;
|
|
191
|
+
readonly name: string;
|
|
192
|
+
readonly env: Record<string, unknown>;
|
|
193
|
+
readonly storage: Party.Storage;
|
|
194
|
+
readonly context: Party.Context;
|
|
195
|
+
readonly connections = new Map<string, Party.Connection>();
|
|
196
|
+
readonly parties: Party.Context["parties"];
|
|
197
|
+
readonly analytics = {} as Party.Room["analytics"];
|
|
198
|
+
|
|
199
|
+
constructor(options: {
|
|
200
|
+
id: string;
|
|
201
|
+
name: string;
|
|
202
|
+
env: Record<string, unknown>;
|
|
203
|
+
state: DurableObjectState;
|
|
204
|
+
binding: string;
|
|
205
|
+
partiesPath: string;
|
|
206
|
+
}) {
|
|
207
|
+
this.id = options.id;
|
|
208
|
+
this.internalID = `${options.name}:${options.id}`;
|
|
209
|
+
this.name = options.name;
|
|
210
|
+
this.env = options.env;
|
|
211
|
+
this.storage = options.state.storage as Party.Storage;
|
|
212
|
+
this.parties = createPartiesContext(options.env, options.binding, options.partiesPath);
|
|
213
|
+
this.context = {
|
|
214
|
+
parties: this.parties,
|
|
215
|
+
ai: {},
|
|
216
|
+
vectorize: {},
|
|
217
|
+
analytics: this.analytics,
|
|
218
|
+
assets: {
|
|
219
|
+
fetch: async () => null,
|
|
220
|
+
},
|
|
221
|
+
bindings: {
|
|
222
|
+
r2: {},
|
|
223
|
+
kv: {},
|
|
224
|
+
},
|
|
225
|
+
} as Party.Context;
|
|
226
|
+
this.blockConcurrencyWhile = options.state.blockConcurrencyWhile.bind(options.state);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
blockConcurrencyWhile: Party.Room["blockConcurrencyWhile"];
|
|
230
|
+
|
|
231
|
+
broadcast(msg: string | ArrayBuffer | ArrayBufferView, without: string[] = []) {
|
|
232
|
+
for (const connection of this.connections.values()) {
|
|
233
|
+
if (!without.includes(connection.id)) {
|
|
234
|
+
connection.send(msg);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getConnection<TState = unknown>(id: string): Party.Connection<TState> | undefined {
|
|
240
|
+
let connection: Party.Connection | undefined;
|
|
241
|
+
for (const current of this.connections.values()) {
|
|
242
|
+
if (current.id === id || current.sessionId === id) {
|
|
243
|
+
connection = current;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return connection as Party.Connection<TState> | undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getConnections<TState = unknown>(): Iterable<Party.Connection<TState>> {
|
|
250
|
+
return Array.from(this.connections.values()) as Party.Connection<TState>[];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
addConnection(connection: CloudflareConnection) {
|
|
254
|
+
this.connections.set(connection.id, connection as unknown as Party.Connection);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
deleteConnection(id: string, connection?: CloudflareConnection) {
|
|
258
|
+
if (connection) {
|
|
259
|
+
this.connections.delete(connection.id);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const [connectionKey, current] of this.connections) {
|
|
264
|
+
if (current.id === id || current.sessionId === id) {
|
|
265
|
+
this.connections.delete(connectionKey);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export class CloudflareConnection<TState = unknown> {
|
|
272
|
+
readonly id = createConnectionId();
|
|
273
|
+
readonly sessionId: string;
|
|
274
|
+
readonly socket: this = this;
|
|
275
|
+
readonly uri: string;
|
|
276
|
+
state: Party.ConnectionState<TState> | TState | null = null;
|
|
277
|
+
private attachment: unknown = null;
|
|
278
|
+
|
|
279
|
+
constructor(
|
|
280
|
+
private readonly webSocket: CloudflareWebSocket,
|
|
281
|
+
uri: string,
|
|
282
|
+
sessionId?: string
|
|
283
|
+
) {
|
|
284
|
+
this.sessionId = sessionId || this.id;
|
|
285
|
+
this.uri = uri;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
send(data: string | ArrayBuffer | ArrayBufferView) {
|
|
289
|
+
if (
|
|
290
|
+
this.webSocket.readyState === undefined ||
|
|
291
|
+
this.webSocket.readyState === WEBSOCKET_OPEN
|
|
292
|
+
) {
|
|
293
|
+
this.webSocket.send(data);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
close(code?: number, reason?: string) {
|
|
298
|
+
this.webSocket.close(code, reason);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
setState(state: TState | Party.ConnectionSetStateFn<TState> | null) {
|
|
302
|
+
this.state = typeof state === "function"
|
|
303
|
+
? (state as Party.ConnectionSetStateFn<TState>)(this.state as Party.ConnectionState<TState>)
|
|
304
|
+
: state;
|
|
305
|
+
return this.state as Party.ConnectionState<TState>;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
serializeAttachment<T = unknown>(attachment: T): void {
|
|
309
|
+
this.attachment = attachment;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
deserializeAttachment<T = unknown>(): T | null {
|
|
313
|
+
return this.attachment as T | null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function createRuntimeConfig<TServer extends Party.Server>(
|
|
318
|
+
ServerClass: CloudflareRoomServerConstructor<TServer>,
|
|
319
|
+
options: CloudflareRoomWorkerOptions
|
|
320
|
+
): CloudflareRuntimeConfig {
|
|
321
|
+
return {
|
|
322
|
+
ServerClass: ServerClass as CloudflareRoomServerConstructor,
|
|
323
|
+
binding: options.binding,
|
|
324
|
+
partiesPath: normalizePath(options.partiesPath ?? DEFAULT_PARTIES_PATH),
|
|
325
|
+
env: options.env ?? {},
|
|
326
|
+
rooms: {
|
|
327
|
+
main: ServerClass as CloudflareRoomServerConstructor,
|
|
328
|
+
...(options.rooms ?? {}),
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getRuntimeConfig(): CloudflareRuntimeConfig {
|
|
334
|
+
if (!runtimeConfig) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
"createCloudflareRoomWorker() must be called before using SigneRoomDurableObject."
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return runtimeConfig;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function createPartiesContext(
|
|
344
|
+
env: Record<string, unknown>,
|
|
345
|
+
binding: string,
|
|
346
|
+
partiesPath: string
|
|
347
|
+
): Party.Context["parties"] {
|
|
348
|
+
return new Proxy({}, {
|
|
349
|
+
get(_target, namespace: string) {
|
|
350
|
+
return {
|
|
351
|
+
get(roomId: string) {
|
|
352
|
+
return {
|
|
353
|
+
connect: () => {
|
|
354
|
+
throw new Error("Party stub connect() is not implemented by @signe/room/cloudflare");
|
|
355
|
+
},
|
|
356
|
+
socket: async () => {
|
|
357
|
+
throw new Error("Party stub socket() is not implemented by @signe/room/cloudflare");
|
|
358
|
+
},
|
|
359
|
+
fetch(pathOrInit?: string | RequestInit | Request, init?: RequestInit) {
|
|
360
|
+
const namespaceBinding = getNamespace(env, binding);
|
|
361
|
+
const stub = namespaceBinding.get(namespaceBinding.idFromName(roomId));
|
|
362
|
+
if (pathOrInit instanceof Request) {
|
|
363
|
+
return fetchDurableObjectStub(stub, pathOrInit);
|
|
364
|
+
}
|
|
365
|
+
const path = typeof pathOrInit === "string" ? pathOrInit : "/";
|
|
366
|
+
const requestInit = typeof pathOrInit === "string" ? init : pathOrInit;
|
|
367
|
+
return fetchDurableObjectStub(
|
|
368
|
+
stub,
|
|
369
|
+
toLocalUrl(`${getNamespacePath(partiesPath, namespace, roomId)}${normalizeStubPath(path)}`),
|
|
370
|
+
requestInit as RequestInit | undefined
|
|
371
|
+
);
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
}) as Party.Context["parties"];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function getNamespace(env: Record<string, unknown>, binding: string) {
|
|
381
|
+
const namespace = env[binding] as DurableObjectNamespace | undefined;
|
|
382
|
+
|
|
383
|
+
if (!namespace) {
|
|
384
|
+
throw new Error(`Missing Durable Object binding: ${binding}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return namespace;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function fetchDurableObjectStub(
|
|
391
|
+
stub: unknown,
|
|
392
|
+
input: Request | string | URL,
|
|
393
|
+
init?: RequestInit
|
|
394
|
+
): Promise<Response> {
|
|
395
|
+
return (stub as {
|
|
396
|
+
fetch(request: Request | string | URL, init?: RequestInit): Promise<Response>;
|
|
397
|
+
}).fetch(input, init);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parsePartyRequest(url: string, partiesPath: string): ParsedPartyPath | null {
|
|
401
|
+
const requestUrl = new URL(url);
|
|
402
|
+
const segments = trimSlashes(requestUrl.pathname).split("/");
|
|
403
|
+
const configuredSegments = trimSlashes(partiesPath).split("/");
|
|
404
|
+
const baseSegments = configuredSegments.slice(0, -1);
|
|
405
|
+
|
|
406
|
+
if (segments.length < baseSegments.length + 2) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for (let index = 0; index < baseSegments.length; index++) {
|
|
411
|
+
if (segments[index] !== baseSegments[index]) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
namespace: decodeURIComponent(segments[baseSegments.length]),
|
|
418
|
+
roomId: decodeURIComponent(segments[baseSegments.length + 1]),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function getNamespacePath(partiesPath: string, namespace: string, roomId: string) {
|
|
423
|
+
const baseSegments = trimSlashes(partiesPath).split("/").slice(0, -1);
|
|
424
|
+
return `/${[...baseSegments, namespace, encodeURIComponent(roomId)].join("/")}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isWebSocketUpgrade(request: Request) {
|
|
428
|
+
return request.headers.get("Upgrade")?.toLowerCase() === "websocket";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function normalizeWebSocketMessage(data: unknown): string | ArrayBuffer | ArrayBufferView {
|
|
432
|
+
if (typeof data === "string" || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
433
|
+
return data;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return String(data);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function normalizePath(path: string) {
|
|
440
|
+
return `/${trimSlashes(path)}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function toLocalUrl(path: string) {
|
|
444
|
+
return path.startsWith("http://") || path.startsWith("https://")
|
|
445
|
+
? path
|
|
446
|
+
: `http://localhost${path.startsWith("/") ? path : `/${path}`}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function normalizeStubPath(path: string) {
|
|
450
|
+
if (!path || path === "/") {
|
|
451
|
+
return "";
|
|
452
|
+
}
|
|
453
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function getConnectionIdFromUrl(url: string) {
|
|
457
|
+
const requestedId = new URL(url).searchParams.get("id")?.trim();
|
|
458
|
+
return requestedId || undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function trimSlashes(value: string) {
|
|
462
|
+
return value.replace(/^\/+|\/+$/g, "");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function createConnectionId() {
|
|
466
|
+
return Math.random().toString(36).slice(2, 12);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
declare const WebSocketPair: {
|
|
470
|
+
new (): {
|
|
471
|
+
0: CloudflareWebSocket;
|
|
472
|
+
1: CloudflareWebSocket;
|
|
473
|
+
};
|
|
474
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * from './decorators';
|
|
2
2
|
export { ClientIo, MockConnection, ServerIo } from './mock';
|
|
3
|
-
export { Server } from './server';
|
|
3
|
+
export { Server, type StorageRestoreContext, type UserStorageRestoreContext } from './server';
|
|
4
4
|
export * from './testing';
|
|
5
5
|
export * from './shard';
|
|
6
6
|
export * from './world';
|
|
7
7
|
export * from './interfaces';
|
|
8
8
|
export * from './request/response';
|
|
9
|
-
export { requireSession, createRequireSessionGuard } from './session.guard';
|
|
9
|
+
export { requireSession, createRequireSessionGuard } from './session.guard';
|
package/src/jwt.ts
CHANGED
|
@@ -132,9 +132,7 @@ export class JWTAuth {
|
|
|
132
132
|
};
|
|
133
133
|
|
|
134
134
|
// Encode header and payload
|
|
135
|
-
// @ts-expect-error - TS doesn't have a built-in TextEncoder
|
|
136
135
|
const encodedHeader: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(header)));
|
|
137
|
-
// @ts-expect-error - TS doesn't have a built-in TextEncoder
|
|
138
136
|
const encodedPayload: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)));
|
|
139
137
|
|
|
140
138
|
// Create signature base
|
|
@@ -174,9 +172,7 @@ export class JWTAuth {
|
|
|
174
172
|
|
|
175
173
|
// Decode header and payload
|
|
176
174
|
try {
|
|
177
|
-
// @ts-expect-error - TS doesn't have a built-in TextDecoder
|
|
178
175
|
const header: JWTHeader = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
|
|
179
|
-
// @ts-expect-error - TS doesn't have a built-in TextDecoder
|
|
180
176
|
const payload: JWTPayload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
|
|
181
177
|
|
|
182
178
|
// Check algorithm
|
|
@@ -214,4 +210,4 @@ export class JWTAuth {
|
|
|
214
210
|
throw new Error('Token verification failed: Unknown error');
|
|
215
211
|
}
|
|
216
212
|
}
|
|
217
|
-
}
|
|
213
|
+
}
|
package/src/mock.ts
CHANGED
|
@@ -8,9 +8,9 @@ export class MockPartyClient {
|
|
|
8
8
|
id : string
|
|
9
9
|
conn: MockConnection;
|
|
10
10
|
|
|
11
|
-
constructor(public server: Server,
|
|
12
|
-
this.id =
|
|
13
|
-
this.conn = new MockConnection(this)
|
|
11
|
+
constructor(public server: Server, sessionId?: string) {
|
|
12
|
+
this.id = generateShortUUID()
|
|
13
|
+
this.conn = new MockConnection(this, sessionId || this.id)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
addEventListener(event, cb) {
|
|
@@ -49,8 +49,8 @@ export class MockPartyClient {
|
|
|
49
49
|
class MockLobby {
|
|
50
50
|
constructor(public server: Server, public lobbyId: string) {}
|
|
51
51
|
|
|
52
|
-
socket(
|
|
53
|
-
return new MockPartyClient(this.server)
|
|
52
|
+
socket(init?: { id?: string }) {
|
|
53
|
+
return new MockPartyClient(this.server, init?.id)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async connection(idOrOptions?: string | { id?: string, query?: Record<string, string>, headers?: Record<string, string> }, maybeOptions?: { query?: Record<string, string>, headers?: Record<string, string> }) {
|
|
@@ -142,7 +142,13 @@ class MockPartyRoom {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
getConnection(id: string) {
|
|
145
|
-
|
|
145
|
+
let connection: MockConnection | undefined;
|
|
146
|
+
for (const client of this.clients.values()) {
|
|
147
|
+
if (client.conn.id === id || client.conn.sessionId === id) {
|
|
148
|
+
connection = client.conn;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return connection;
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
getConnections() {
|
|
@@ -152,15 +158,30 @@ class MockPartyRoom {
|
|
|
152
158
|
clear() {
|
|
153
159
|
this.clients.clear();
|
|
154
160
|
}
|
|
161
|
+
|
|
162
|
+
deleteConnection(id: string, connection?: MockConnection) {
|
|
163
|
+
if (connection) {
|
|
164
|
+
this.clients.delete(connection.id);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const [connectionKey, client] of this.clients) {
|
|
169
|
+
if (client.conn.id === id || client.conn.sessionId === id) {
|
|
170
|
+
this.clients.delete(connectionKey);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
155
174
|
}
|
|
156
175
|
|
|
157
176
|
export class MockConnection {
|
|
158
177
|
server: Server;
|
|
159
178
|
id: string;
|
|
179
|
+
sessionId: string;
|
|
160
180
|
|
|
161
|
-
constructor(public client: MockPartyClient) {
|
|
181
|
+
constructor(public client: MockPartyClient, sessionId: string) {
|
|
162
182
|
this.server = client.server
|
|
163
183
|
this.id = client.id
|
|
184
|
+
this.sessionId = sessionId
|
|
164
185
|
}
|
|
165
186
|
|
|
166
187
|
state: any = {};
|
|
@@ -174,6 +195,7 @@ export class MockConnection {
|
|
|
174
195
|
}
|
|
175
196
|
|
|
176
197
|
close() {
|
|
198
|
+
(this.server.room as any).deleteConnection?.(this.id, this);
|
|
177
199
|
this.server.onClose(this as any)
|
|
178
200
|
}
|
|
179
201
|
}
|