@signe/room 1.2.1 → 1.4.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 +8 -15
- package/dist/index.js +140 -239
- package/dist/index.js.map +1 -1
- package/examples/game/app/client.tsx +0 -26
- package/examples/game/app/components/Counter.tsx +47 -9
- package/examples/game/package-lock.json +225 -0
- package/examples/game/package.json +1 -1
- package/examples/game/party/game.room.ts +13 -5
- package/examples/game/party/server.ts +1 -4
- package/examples/game/shared/room.schema.ts +9 -1
- package/package.json +2 -2
- package/src/decorators.ts +2 -2
- package/src/mock.ts +5 -1
- package/src/server.ts +173 -94
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { id, users } from '../../../../sync/src/decorators';
|
|
1
2
|
import { signal } from '../../../../reactive';
|
|
2
3
|
import { sync } from '../../../../sync';
|
|
3
4
|
|
|
5
|
+
class User {
|
|
6
|
+
@id() id = signal('')
|
|
7
|
+
@sync() name = signal('')
|
|
8
|
+
@sync() score = signal(0)
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export class RoomSchema {
|
|
5
|
-
@
|
|
12
|
+
@users(User) users = signal({})
|
|
13
|
+
@sync() count = signal(0)
|
|
6
14
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signe/room",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"keywords": [],
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dset": "^3.1.3",
|
|
18
18
|
"partysocket": "^1.0.1",
|
|
19
19
|
"zod": "^3.23.8",
|
|
20
|
-
"@signe/sync": "1.
|
|
20
|
+
"@signe/sync": "1.4.0"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
package/src/decorators.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface RoomOptions {
|
|
|
22
22
|
throttleSync?: number;
|
|
23
23
|
hibernate?: boolean;
|
|
24
24
|
guards?: RoomGuardFn[];
|
|
25
|
-
|
|
25
|
+
sessionExpiryTime?: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function Room(options: RoomOptions) {
|
|
@@ -31,7 +31,7 @@ export function Room(options: RoomOptions) {
|
|
|
31
31
|
target.maxUsers = options.maxUsers;
|
|
32
32
|
target.throttleStorage = options.throttleStorage;
|
|
33
33
|
target.throttleSync = options.throttleSync;
|
|
34
|
-
target.
|
|
34
|
+
target.sessionExpiryTime = options.sessionExpiryTime ?? 5 * 60 * 1000;
|
|
35
35
|
if (options.guards) {
|
|
36
36
|
target['_roomGuards'] = options.guards;
|
|
37
37
|
}
|
package/src/mock.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateShortUUID } from "../../sync/src/utils";
|
|
2
2
|
import { Storage } from "./storage";
|
|
3
3
|
|
|
4
|
-
class MockPartySocket {
|
|
4
|
+
export class MockPartySocket {
|
|
5
5
|
private events: Map<string, Function> = new Map();
|
|
6
6
|
id = generateShortUUID()
|
|
7
7
|
|
|
@@ -38,6 +38,10 @@ class MockPartyRoom {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
getConnections() {
|
|
42
|
+
return this.clients;
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
clear() {
|
|
42
46
|
this.clients.clear();
|
|
43
47
|
}
|
package/src/server.ts
CHANGED
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
getByPath,
|
|
6
6
|
load,
|
|
7
7
|
syncClass,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
DELETE_TOKEN,
|
|
9
|
+
generateShortUUID
|
|
10
|
+
} from "@signe/sync";
|
|
10
11
|
import type * as Party from "./types/party";
|
|
11
12
|
import {
|
|
12
13
|
awaitReturn,
|
|
@@ -50,13 +51,6 @@ type CreateRoomOptions = {
|
|
|
50
51
|
export class Server implements Party.Server {
|
|
51
52
|
subRoom = null;
|
|
52
53
|
rooms: any[] = [];
|
|
53
|
-
private timeoutHandles: Map<string, any> = new Map();
|
|
54
|
-
|
|
55
|
-
static async onBeforeConnect(request: Party.Request, lobby: Party.Lobby) {
|
|
56
|
-
const token = new URL(request.url).searchParams.get("token") ?? "";
|
|
57
|
-
request.headers.set("X-User-ID", token);
|
|
58
|
-
return request;
|
|
59
|
-
}
|
|
60
54
|
|
|
61
55
|
/**
|
|
62
56
|
* @constructor
|
|
@@ -107,13 +101,72 @@ export class Server implements Party.Server {
|
|
|
107
101
|
}
|
|
108
102
|
}
|
|
109
103
|
|
|
104
|
+
private async garbageCollector(options: { sessionExpiryTime: number }) {
|
|
105
|
+
const subRoom = await this.getSubRoom();
|
|
106
|
+
if (!subRoom) return;
|
|
107
|
+
|
|
108
|
+
// Get active connections
|
|
109
|
+
const activeConnections = [...this.room.getConnections()];
|
|
110
|
+
const activePrivateIds = new Set(activeConnections.map(conn => conn.id));
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Get all sessions from storage
|
|
114
|
+
const sessions = await this.room.storage.list();
|
|
115
|
+
const users = this.getUsersProperty(subRoom);
|
|
116
|
+
const usersPropName = this.getUsersPropName(subRoom);
|
|
117
|
+
|
|
118
|
+
// Store valid publicIds from sessions
|
|
119
|
+
const validPublicIds = new Set<string>();
|
|
120
|
+
const expiredPublicIds = new Set<string>();
|
|
121
|
+
const SESSION_EXPIRY_TIME = options.sessionExpiryTime
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
|
|
124
|
+
for (const [key, session] of sessions) {
|
|
125
|
+
// Only process session entries
|
|
126
|
+
if (!key.startsWith('session:')) continue;
|
|
127
|
+
|
|
128
|
+
const privateId = key.replace('session:', '');
|
|
129
|
+
const typedSession = session as {publicId: string, created: number, connected: boolean};
|
|
130
|
+
|
|
131
|
+
// Check if session should be deleted based on:
|
|
132
|
+
// 1. Connection is not active
|
|
133
|
+
// 2. Session is marked as disconnected
|
|
134
|
+
// 3. Session is older than expiry time
|
|
135
|
+
if (!activePrivateIds.has(privateId) &&
|
|
136
|
+
!typedSession.connected &&
|
|
137
|
+
(now - typedSession.created) > SESSION_EXPIRY_TIME) {
|
|
138
|
+
// Delete expired session
|
|
139
|
+
await this.deleteSession(privateId);
|
|
140
|
+
expiredPublicIds.add(typedSession.publicId);
|
|
141
|
+
} else if (typedSession && typedSession.publicId) {
|
|
142
|
+
// Keep track of valid publicIds from active or recent sessions
|
|
143
|
+
validPublicIds.add(typedSession.publicId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Clean up users only if ALL their sessions are expired
|
|
148
|
+
if (users && usersPropName) {
|
|
149
|
+
const currentUsers = users();
|
|
150
|
+
for (const publicId in currentUsers) {
|
|
151
|
+
// Only delete user if they have an expired session and no valid sessions
|
|
152
|
+
if (expiredPublicIds.has(publicId) && !validPublicIds.has(publicId)) {
|
|
153
|
+
delete currentUsers[publicId];
|
|
154
|
+
await this.room.storage.delete(`${usersPropName}.${publicId}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error in garbage collector:', error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
110
164
|
/**
|
|
111
165
|
* @method createRoom
|
|
112
166
|
* @private
|
|
113
167
|
* @async
|
|
114
168
|
* @param {CreateRoomOptions} [options={}] - Options for creating the room.
|
|
115
169
|
* @returns {Promise<Object>} The created room instance.
|
|
116
|
-
* @throws {Error} If no matching room is found.
|
|
117
170
|
*
|
|
118
171
|
* @example
|
|
119
172
|
* ```typescript
|
|
@@ -127,6 +180,7 @@ export class Server implements Party.Server {
|
|
|
127
180
|
private async createRoom(options: CreateRoomOptions = {}) {
|
|
128
181
|
let instance
|
|
129
182
|
let init = true
|
|
183
|
+
let initPersist = true
|
|
130
184
|
|
|
131
185
|
// Find the appropriate room based on the current room ID
|
|
132
186
|
for (let room of this.rooms) {
|
|
@@ -138,7 +192,7 @@ export class Server implements Party.Server {
|
|
|
138
192
|
}
|
|
139
193
|
|
|
140
194
|
if (!instance) {
|
|
141
|
-
|
|
195
|
+
return null;
|
|
142
196
|
}
|
|
143
197
|
|
|
144
198
|
// Load the room's memory from storage
|
|
@@ -148,16 +202,17 @@ export class Server implements Party.Server {
|
|
|
148
202
|
const memory = await this.room.storage.list();
|
|
149
203
|
const tmpObject: any = root || {};
|
|
150
204
|
for (let [key, value] of memory) {
|
|
205
|
+
if (key.startsWith('session:')) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
151
208
|
if (key == ".") {
|
|
152
209
|
continue;
|
|
153
210
|
}
|
|
154
211
|
dset(tmpObject, key, value);
|
|
155
212
|
}
|
|
156
|
-
load(instance, tmpObject);
|
|
213
|
+
load(instance, tmpObject, true);
|
|
157
214
|
};
|
|
158
215
|
|
|
159
|
-
await loadMemory();
|
|
160
|
-
|
|
161
216
|
instance.$memoryAll = {}
|
|
162
217
|
|
|
163
218
|
// Sync callback: Broadcast changes to all clients
|
|
@@ -180,12 +235,20 @@ export class Server implements Party.Server {
|
|
|
180
235
|
}
|
|
181
236
|
|
|
182
237
|
// Persist callback: Save changes to storage
|
|
183
|
-
const persistCb = async (values) => {
|
|
184
|
-
|
|
238
|
+
const persistCb = async (values: Map<string, any>) => {
|
|
239
|
+
if (initPersist) {
|
|
240
|
+
values.clear();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
for (let [path, value] of values) {
|
|
185
244
|
const _instance =
|
|
186
245
|
path == "." ? instance : getByPath(instance, path);
|
|
187
|
-
const itemValue = createStatesSnapshot(_instance);
|
|
188
|
-
|
|
246
|
+
const itemValue = createStatesSnapshot(_instance);
|
|
247
|
+
if (value == DELETE_TOKEN) {
|
|
248
|
+
await this.room.storage.delete(path);
|
|
249
|
+
} else {
|
|
250
|
+
await this.room.storage.put(path, itemValue);
|
|
251
|
+
}
|
|
189
252
|
}
|
|
190
253
|
values.clear();
|
|
191
254
|
}
|
|
@@ -196,6 +259,10 @@ export class Server implements Party.Server {
|
|
|
196
259
|
onPersist: throttle(persistCb, instance["throttleStorage"] ?? 2000),
|
|
197
260
|
});
|
|
198
261
|
|
|
262
|
+
await loadMemory();
|
|
263
|
+
|
|
264
|
+
initPersist = false
|
|
265
|
+
|
|
199
266
|
return instance
|
|
200
267
|
}
|
|
201
268
|
|
|
@@ -215,8 +282,8 @@ export class Server implements Party.Server {
|
|
|
215
282
|
* }
|
|
216
283
|
* ```
|
|
217
284
|
*/
|
|
218
|
-
private async getSubRoom(options = {}) {
|
|
219
|
-
let subRoom
|
|
285
|
+
private async getSubRoom(options = {}): Promise<any | null> {
|
|
286
|
+
let subRoom // instance of the room or null
|
|
220
287
|
if (this.isHibernate) {
|
|
221
288
|
subRoom = await this.createRoom(options)
|
|
222
289
|
}
|
|
@@ -251,18 +318,35 @@ export class Server implements Party.Server {
|
|
|
251
318
|
return null;
|
|
252
319
|
}
|
|
253
320
|
|
|
254
|
-
private
|
|
321
|
+
private getUsersPropName(subRoom) {
|
|
322
|
+
const meta = subRoom.constructor["_propertyMetadata"];
|
|
323
|
+
return meta?.get("users")
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async getSession(privateId: string): Promise<{publicId: string, state?: any, created?: number, connected?: boolean} | null> {
|
|
255
327
|
if (!privateId) return null;
|
|
256
328
|
try {
|
|
257
329
|
const session = await this.room.storage.get(`session:${privateId}`);
|
|
258
|
-
return session as {publicId: string, state?: any} | null;
|
|
330
|
+
return session as {publicId: string, state?: any, created: number, connected: boolean} | null;
|
|
259
331
|
} catch (e) {
|
|
260
332
|
return null;
|
|
261
333
|
}
|
|
262
334
|
}
|
|
263
335
|
|
|
264
|
-
private async saveSession(privateId: string, data: {publicId: string, state?: any}) {
|
|
265
|
-
|
|
336
|
+
private async saveSession(privateId: string, data: {publicId: string, state?: any, created?: number, connected?: boolean}) {
|
|
337
|
+
const sessionData = {
|
|
338
|
+
...data,
|
|
339
|
+
created: data.created || Date.now(),
|
|
340
|
+
connected: data.connected !== undefined ? data.connected : true
|
|
341
|
+
};
|
|
342
|
+
await this.room.storage.put(`session:${privateId}`, sessionData);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async updateSessionConnection(privateId: string, connected: boolean) {
|
|
346
|
+
const session = await this.getSession(privateId);
|
|
347
|
+
if (session) {
|
|
348
|
+
await this.saveSession(privateId, { ...session, connected });
|
|
349
|
+
}
|
|
266
350
|
}
|
|
267
351
|
|
|
268
352
|
private async deleteSession(privateId: string) {
|
|
@@ -290,6 +374,14 @@ export class Server implements Party.Server {
|
|
|
290
374
|
getMemoryAll: true,
|
|
291
375
|
})
|
|
292
376
|
|
|
377
|
+
if (!subRoom) {
|
|
378
|
+
conn.close();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const sessionExpiryTime = subRoom.constructor.sessionExpiryTime;
|
|
383
|
+
await this.garbageCollector({ sessionExpiryTime });
|
|
384
|
+
|
|
293
385
|
// Check room guards
|
|
294
386
|
const roomGuards = subRoom.constructor['_roomGuards'] || [];
|
|
295
387
|
for (const guard of roomGuards) {
|
|
@@ -301,16 +393,15 @@ export class Server implements Party.Server {
|
|
|
301
393
|
}
|
|
302
394
|
|
|
303
395
|
// Check for existing session
|
|
304
|
-
const
|
|
305
|
-
const existingSession = providedPrivateId ? await this.getSession(providedPrivateId) : null;
|
|
396
|
+
const existingSession = await this.getSession(conn.id)
|
|
306
397
|
|
|
307
398
|
// Generate IDs
|
|
308
399
|
const publicId = existingSession?.publicId || generateShortUUID();
|
|
309
|
-
const privateId = existingSession ? providedPrivateId : generateShortUUID();
|
|
310
400
|
|
|
311
401
|
let user = null;
|
|
312
402
|
const signal = this.getUsersProperty(subRoom);
|
|
313
|
-
|
|
403
|
+
const usersPropName = this.getUsersPropName(subRoom);
|
|
404
|
+
|
|
314
405
|
if (signal) {
|
|
315
406
|
const { classType } = signal.options;
|
|
316
407
|
|
|
@@ -318,21 +409,26 @@ export class Server implements Party.Server {
|
|
|
318
409
|
if (!existingSession?.publicId) {
|
|
319
410
|
user = isClass(classType) ? new classType() : classType(conn, ctx);
|
|
320
411
|
signal()[publicId] = user;
|
|
412
|
+
const snapshot = createStatesSnapshot(user);
|
|
413
|
+
this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
|
|
321
414
|
}
|
|
322
415
|
|
|
323
416
|
// Only store new session if it doesn't exist
|
|
324
417
|
if (!existingSession) {
|
|
325
|
-
await this.saveSession(
|
|
418
|
+
await this.saveSession(conn.id, {
|
|
326
419
|
publicId
|
|
327
420
|
});
|
|
328
421
|
}
|
|
422
|
+
else {
|
|
423
|
+
await this.updateSessionConnection(conn.id, true);
|
|
424
|
+
}
|
|
329
425
|
}
|
|
330
426
|
|
|
331
427
|
// Call the room's onJoin method if it exists
|
|
332
428
|
await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
|
|
333
429
|
|
|
334
430
|
// Store both IDs in connection state
|
|
335
|
-
conn.setState({ publicId
|
|
431
|
+
conn.setState({ publicId });
|
|
336
432
|
|
|
337
433
|
// Send initial sync data with both IDs to the new connection
|
|
338
434
|
conn.send(
|
|
@@ -340,7 +436,6 @@ export class Server implements Party.Server {
|
|
|
340
436
|
type: "sync",
|
|
341
437
|
value: {
|
|
342
438
|
pId: publicId,
|
|
343
|
-
privateId,
|
|
344
439
|
...subRoom.$memoryAll,
|
|
345
440
|
},
|
|
346
441
|
})
|
|
@@ -378,7 +473,6 @@ export class Server implements Party.Server {
|
|
|
378
473
|
return;
|
|
379
474
|
}
|
|
380
475
|
const subRoom = await this.getSubRoom()
|
|
381
|
-
|
|
382
476
|
// Check room guards
|
|
383
477
|
const roomGuards = subRoom.constructor['_roomGuards'] || [];
|
|
384
478
|
for (const guard of roomGuards) {
|
|
@@ -439,82 +533,67 @@ export class Server implements Party.Server {
|
|
|
439
533
|
*/
|
|
440
534
|
async onClose(conn: Party.Connection) {
|
|
441
535
|
const subRoom = await this.getSubRoom()
|
|
442
|
-
|
|
443
|
-
if (!
|
|
536
|
+
|
|
537
|
+
if (!subRoom) {
|
|
444
538
|
return;
|
|
445
539
|
}
|
|
446
|
-
const { publicId, privateId } = conn.state as any;
|
|
447
|
-
const user = signal?.()[publicId];
|
|
448
|
-
|
|
449
|
-
if (!user) return;
|
|
450
540
|
|
|
451
|
-
|
|
452
|
-
if (privateId) {
|
|
453
|
-
await this.saveSession(privateId, {
|
|
454
|
-
publicId
|
|
455
|
-
});
|
|
456
|
-
}
|
|
541
|
+
const signal = this.getUsersProperty(subRoom);
|
|
457
542
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (existingTimeout) {
|
|
461
|
-
clearTimeout(existingTimeout);
|
|
462
|
-
this.timeoutHandles.delete(privateId);
|
|
543
|
+
if (!conn.state) {
|
|
544
|
+
return;
|
|
463
545
|
}
|
|
464
546
|
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// Remove user from signal
|
|
470
|
-
if (signal) {
|
|
471
|
-
delete signal()[publicId];
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Delete session
|
|
475
|
-
if (privateId) {
|
|
476
|
-
await this.deleteSession(privateId);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Broadcast user disconnection
|
|
480
|
-
this.room.broadcast(
|
|
481
|
-
JSON.stringify({
|
|
482
|
-
type: "user_disconnected",
|
|
483
|
-
value: { publicId }
|
|
484
|
-
})
|
|
485
|
-
);
|
|
547
|
+
const privateId = conn.id;
|
|
548
|
+
const { publicId } = conn.state as any;
|
|
549
|
+
const user = signal?.()[publicId];
|
|
486
550
|
|
|
487
|
-
|
|
488
|
-
this.timeoutHandles.delete(privateId);
|
|
489
|
-
};
|
|
551
|
+
if (!user) return;
|
|
490
552
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (disconnectTimeout > 0) {
|
|
494
|
-
// Set temporary offline status
|
|
495
|
-
if (user.status) {
|
|
496
|
-
user.status.set('offline');
|
|
497
|
-
}
|
|
553
|
+
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
498
554
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
JSON.stringify({
|
|
502
|
-
type: "user_offline",
|
|
503
|
-
value: { publicId }
|
|
504
|
-
})
|
|
505
|
-
);
|
|
555
|
+
// Mark session as disconnected instead of deleting it
|
|
556
|
+
await this.updateSessionConnection(privateId, false);
|
|
506
557
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
558
|
+
// Broadcast user disconnection
|
|
559
|
+
this.room.broadcast(
|
|
560
|
+
JSON.stringify({
|
|
561
|
+
type: "user_disconnected",
|
|
562
|
+
value: { publicId }
|
|
563
|
+
})
|
|
564
|
+
);
|
|
514
565
|
}
|
|
515
566
|
|
|
516
567
|
async onAlarm() {
|
|
517
568
|
const subRoom = await this.getSubRoom()
|
|
518
569
|
await awaitReturn(subRoom["onAlarm"]?.(subRoom));
|
|
519
570
|
}
|
|
571
|
+
|
|
572
|
+
async onError(connection: Party.Connection, error: Error) {
|
|
573
|
+
const subRoom = await this.getSubRoom()
|
|
574
|
+
await awaitReturn(subRoom["onError"]?.(connection, error));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async onRequest(req: Party.Request) {
|
|
578
|
+
const subRoom = await this.getSubRoom()
|
|
579
|
+
const res = (body: any, status: number) => {
|
|
580
|
+
return new Response(JSON.stringify(body), { status });
|
|
581
|
+
}
|
|
582
|
+
if (!subRoom) {
|
|
583
|
+
return res({
|
|
584
|
+
error: "Not found"
|
|
585
|
+
}, 404);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const response = await awaitReturn(subRoom["onRequest"]?.(req, this.room));
|
|
589
|
+
if (!response) {
|
|
590
|
+
return res({
|
|
591
|
+
error: "Not found"
|
|
592
|
+
}, 404);
|
|
593
|
+
}
|
|
594
|
+
if (response instanceof Response) {
|
|
595
|
+
return response;
|
|
596
|
+
}
|
|
597
|
+
return res(response, 200);
|
|
598
|
+
}
|
|
520
599
|
}
|