@signe/room 1.2.0 → 1.3.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 +9 -15
- package/dist/index.js +144 -243
- 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 +178 -97
|
@@ -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.3.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.3.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,40 +393,42 @@ 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
|
-
|
|
317
|
-
|
|
407
|
+
|
|
318
408
|
// Restore state if exists
|
|
319
|
-
if (existingSession?.
|
|
320
|
-
|
|
409
|
+
if (!existingSession?.publicId) {
|
|
410
|
+
user = isClass(classType) ? new classType() : classType(conn, ctx);
|
|
411
|
+
signal()[publicId] = user;
|
|
412
|
+
const snapshot = createStatesSnapshot(user);
|
|
413
|
+
this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
|
|
321
414
|
}
|
|
322
415
|
|
|
323
|
-
signal()[publicId] = user;
|
|
324
|
-
|
|
325
416
|
// Only store new session if it doesn't exist
|
|
326
417
|
if (!existingSession) {
|
|
327
|
-
await this.saveSession(
|
|
418
|
+
await this.saveSession(conn.id, {
|
|
328
419
|
publicId
|
|
329
420
|
});
|
|
330
421
|
}
|
|
422
|
+
else {
|
|
423
|
+
await this.updateSessionConnection(conn.id, true);
|
|
424
|
+
}
|
|
331
425
|
}
|
|
332
426
|
|
|
333
427
|
// Call the room's onJoin method if it exists
|
|
334
428
|
await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
|
|
335
429
|
|
|
336
430
|
// Store both IDs in connection state
|
|
337
|
-
conn.setState({ publicId
|
|
431
|
+
conn.setState({ publicId });
|
|
338
432
|
|
|
339
433
|
// Send initial sync data with both IDs to the new connection
|
|
340
434
|
conn.send(
|
|
@@ -342,7 +436,6 @@ export class Server implements Party.Server {
|
|
|
342
436
|
type: "sync",
|
|
343
437
|
value: {
|
|
344
438
|
pId: publicId,
|
|
345
|
-
privateId,
|
|
346
439
|
...subRoom.$memoryAll,
|
|
347
440
|
},
|
|
348
441
|
})
|
|
@@ -380,7 +473,6 @@ export class Server implements Party.Server {
|
|
|
380
473
|
return;
|
|
381
474
|
}
|
|
382
475
|
const subRoom = await this.getSubRoom()
|
|
383
|
-
|
|
384
476
|
// Check room guards
|
|
385
477
|
const roomGuards = subRoom.constructor['_roomGuards'] || [];
|
|
386
478
|
for (const guard of roomGuards) {
|
|
@@ -441,78 +533,67 @@ export class Server implements Party.Server {
|
|
|
441
533
|
*/
|
|
442
534
|
async onClose(conn: Party.Connection) {
|
|
443
535
|
const subRoom = await this.getSubRoom()
|
|
536
|
+
|
|
537
|
+
if (!subRoom) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
444
541
|
const signal = this.getUsersProperty(subRoom);
|
|
542
|
+
|
|
445
543
|
if (!conn.state) {
|
|
446
544
|
return;
|
|
447
545
|
}
|
|
448
|
-
|
|
546
|
+
|
|
547
|
+
const privateId = conn.id;
|
|
548
|
+
const { publicId } = conn.state as any;
|
|
449
549
|
const user = signal?.()[publicId];
|
|
450
|
-
|
|
451
|
-
if (!user) return;
|
|
452
550
|
|
|
453
|
-
|
|
454
|
-
if (privateId) {
|
|
455
|
-
await this.saveSession(privateId, {
|
|
456
|
-
publicId,
|
|
457
|
-
state: { ...user }
|
|
458
|
-
});
|
|
459
|
-
}
|
|
551
|
+
if (!user) return;
|
|
460
552
|
|
|
461
|
-
|
|
462
|
-
const existingTimeout = this.timeoutHandles.get(privateId);
|
|
463
|
-
if (existingTimeout) {
|
|
464
|
-
clearTimeout(existingTimeout);
|
|
465
|
-
this.timeoutHandles.delete(privateId);
|
|
466
|
-
}
|
|
553
|
+
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
467
554
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
471
|
-
|
|
472
|
-
// Remove user from signal
|
|
473
|
-
if (signal) {
|
|
474
|
-
delete signal()[publicId];
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Delete session
|
|
478
|
-
if (privateId) {
|
|
479
|
-
await this.deleteSession(privateId);
|
|
480
|
-
}
|
|
555
|
+
// Mark session as disconnected instead of deleting it
|
|
556
|
+
await this.updateSessionConnection(privateId, false);
|
|
481
557
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
558
|
+
// Broadcast user disconnection
|
|
559
|
+
this.room.broadcast(
|
|
560
|
+
JSON.stringify({
|
|
561
|
+
type: "user_disconnected",
|
|
562
|
+
value: { publicId }
|
|
563
|
+
})
|
|
564
|
+
);
|
|
565
|
+
}
|
|
489
566
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
567
|
+
async onAlarm() {
|
|
568
|
+
const subRoom = await this.getSubRoom()
|
|
569
|
+
await awaitReturn(subRoom["onAlarm"]?.(subRoom));
|
|
570
|
+
}
|
|
493
571
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (user.status) {
|
|
499
|
-
user.status.set('offline');
|
|
500
|
-
}
|
|
572
|
+
async onError(connection: Party.Connection, error: Error) {
|
|
573
|
+
const subRoom = await this.getSubRoom()
|
|
574
|
+
await awaitReturn(subRoom["onError"]?.(connection, error));
|
|
575
|
+
}
|
|
501
576
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
}
|
|
509
587
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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;
|
|
516
596
|
}
|
|
597
|
+
return res(response, 200);
|
|
517
598
|
}
|
|
518
599
|
}
|