@pluv/io 0.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.
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +17 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +891 -0
- package/dist/index.mjs +857 -0
- package/package.json +34 -0
- package/src/AbstractPersistance.ts +31 -0
- package/src/AbstractPlatform.ts +40 -0
- package/src/AbstractPubSub.ts +23 -0
- package/src/AbstractWebSocket.ts +117 -0
- package/src/IORoom.ts +630 -0
- package/src/Persistance.ts +77 -0
- package/src/PluvIO.ts +322 -0
- package/src/PubSub.ts +79 -0
- package/src/authorize.ts +81 -0
- package/src/createIO.ts +51 -0
- package/src/index.ts +34 -0
- package/src/types.ts +93 -0
- package/tsconfig.json +5 -0
package/src/IORoom.ts
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import type { YjsDoc } from "@pluv/crdt-yjs";
|
|
2
|
+
import { doc } from "@pluv/crdt-yjs";
|
|
3
|
+
import type {
|
|
4
|
+
BaseIOAuthorize,
|
|
5
|
+
BaseIOEventRecord,
|
|
6
|
+
EventMessage,
|
|
7
|
+
EventRecord,
|
|
8
|
+
InferEventMessage,
|
|
9
|
+
InferIOAuthorize,
|
|
10
|
+
InferIOAuthorizeUser,
|
|
11
|
+
InferIOInput,
|
|
12
|
+
IOAuthorize,
|
|
13
|
+
IOLike,
|
|
14
|
+
JsonObject,
|
|
15
|
+
} from "@pluv/types";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import type { AbstractPlatform, InferWebSocketType } from "./AbstractPlatform";
|
|
18
|
+
import { authorize } from "./authorize";
|
|
19
|
+
import type {
|
|
20
|
+
EventResolverContext,
|
|
21
|
+
InferEventConfig,
|
|
22
|
+
SendMessageOptions,
|
|
23
|
+
WebSocketSession,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
const PING_TIMEOUT_MS = 30_000;
|
|
27
|
+
|
|
28
|
+
type BroadcastMessage<TIO extends IORoom<any, any, any, any, any>> =
|
|
29
|
+
| InferEventMessage<InferIOInput<TIO>>
|
|
30
|
+
| InferEventMessage<BaseIOEventRecord<InferIOAuthorize<TIO>>>;
|
|
31
|
+
|
|
32
|
+
interface BroadcastParams<TIO extends IORoom<any, any, any, any, any>> {
|
|
33
|
+
message: BroadcastMessage<TIO>;
|
|
34
|
+
senderId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface IORoomListeners {
|
|
38
|
+
onDestroy: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface IORoomConfig<
|
|
42
|
+
TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
|
|
43
|
+
TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize,
|
|
44
|
+
TContext extends JsonObject = {},
|
|
45
|
+
TInput extends EventRecord<string, any> = {},
|
|
46
|
+
TOutput extends EventRecord<string, any> = {}
|
|
47
|
+
> {
|
|
48
|
+
authorize?: TAuthorize;
|
|
49
|
+
context: TContext;
|
|
50
|
+
debug: boolean;
|
|
51
|
+
events: InferEventConfig<TContext, TInput, TOutput>;
|
|
52
|
+
onDestroy?: () => void;
|
|
53
|
+
platform: TPlatform;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface SendMessageSender {
|
|
57
|
+
id: string | null;
|
|
58
|
+
user: JsonObject | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface WebsocketRegisterOptions {
|
|
62
|
+
token?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class IORoom<
|
|
66
|
+
TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
|
|
67
|
+
TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize,
|
|
68
|
+
TContext extends JsonObject = {},
|
|
69
|
+
TInput extends EventRecord<string, any> = {},
|
|
70
|
+
TOutput extends EventRecord<string, any> = {}
|
|
71
|
+
> implements IOLike<TAuthorize, TInput, TOutput>
|
|
72
|
+
{
|
|
73
|
+
private readonly _context: TContext;
|
|
74
|
+
private readonly _debug: boolean;
|
|
75
|
+
private readonly _platform: TPlatform;
|
|
76
|
+
|
|
77
|
+
private _doc: YjsDoc<any> = doc();
|
|
78
|
+
private _listeners: IORoomListeners;
|
|
79
|
+
private _sessions = new Map<string, WebSocketSession>();
|
|
80
|
+
private _uninitialize: (() => Promise<void>) | null = null;
|
|
81
|
+
|
|
82
|
+
readonly _authorize: TAuthorize | null = null;
|
|
83
|
+
readonly _events: InferEventConfig<TContext, TInput, TOutput>;
|
|
84
|
+
|
|
85
|
+
readonly room: string;
|
|
86
|
+
|
|
87
|
+
constructor(
|
|
88
|
+
room: string,
|
|
89
|
+
config: IORoomConfig<TPlatform, TAuthorize, TContext, TInput, TOutput>
|
|
90
|
+
) {
|
|
91
|
+
const { authorize, context, debug, events, onDestroy, platform } =
|
|
92
|
+
config;
|
|
93
|
+
|
|
94
|
+
this._context = context;
|
|
95
|
+
this._debug = debug;
|
|
96
|
+
this._events = events;
|
|
97
|
+
this._platform = platform;
|
|
98
|
+
|
|
99
|
+
this.room = room;
|
|
100
|
+
|
|
101
|
+
this._listeners = {
|
|
102
|
+
onDestroy: () => onDestroy?.(),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (authorize) this._authorize = authorize;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public broadcast(message: BroadcastMessage<this>): Promise<void> {
|
|
109
|
+
return Promise.resolve(this._broadcast({ message }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public async register(
|
|
113
|
+
webSocket: InferWebSocketType<TPlatform>,
|
|
114
|
+
options?: WebsocketRegisterOptions
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
if (!this._uninitialize) await this._initialize();
|
|
117
|
+
|
|
118
|
+
const token = options?.token;
|
|
119
|
+
const sessionId = this._platform.randomUUID();
|
|
120
|
+
const pluvWs = this._platform.convertWebSocket(webSocket, {
|
|
121
|
+
room: this.room,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this._logDebug(
|
|
125
|
+
`${chalk.blue(
|
|
126
|
+
`Registering connection for room ${this.room}:`
|
|
127
|
+
)} ${sessionId}`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const uninitializeWs = await pluvWs.initialize();
|
|
131
|
+
|
|
132
|
+
const user = await this._getAuthorizedUser(token);
|
|
133
|
+
|
|
134
|
+
const isUnauthorized = !!this._authorize?.required && !user;
|
|
135
|
+
// There is an attempt for multiple of the same identities. Probably malicious.
|
|
136
|
+
const isTokenInUse =
|
|
137
|
+
!!user &&
|
|
138
|
+
Array.from(this._sessions.values()).some(
|
|
139
|
+
(session) => session.user?.id === user.id
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (isUnauthorized || isTokenInUse) {
|
|
143
|
+
this._logDebug(
|
|
144
|
+
`${chalk.blue(
|
|
145
|
+
"Authorization failed for connection:"
|
|
146
|
+
)} ${sessionId}`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
pluvWs.handleError({ error: new Error("Not authorized") });
|
|
150
|
+
pluvWs.close(3000, "WebSocket unauthorized.");
|
|
151
|
+
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const currentTime = new Date().getTime();
|
|
156
|
+
const session: WebSocketSession = {
|
|
157
|
+
id: sessionId,
|
|
158
|
+
presence: null,
|
|
159
|
+
quit: false,
|
|
160
|
+
room: this.room,
|
|
161
|
+
timers: {
|
|
162
|
+
ping: currentTime,
|
|
163
|
+
},
|
|
164
|
+
webSocket: pluvWs,
|
|
165
|
+
user,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
this._sessions.set(session.id, session);
|
|
169
|
+
|
|
170
|
+
await this._platform.persistance.addUser(
|
|
171
|
+
this.room,
|
|
172
|
+
sessionId,
|
|
173
|
+
user ?? {}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
this._emitRegistered(session);
|
|
177
|
+
this._emitStorageReceived(session);
|
|
178
|
+
this._emitUserJoined(session);
|
|
179
|
+
|
|
180
|
+
const context: EventResolverContext<TContext> = {
|
|
181
|
+
context: this._context,
|
|
182
|
+
doc: this._doc,
|
|
183
|
+
room: this.room,
|
|
184
|
+
session,
|
|
185
|
+
sessions: this._sessions,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
pluvWs.addEventListener("message", (event) => {
|
|
189
|
+
if (!this._uninitialize) return;
|
|
190
|
+
|
|
191
|
+
if (session.quit) {
|
|
192
|
+
session.webSocket.close(1011, "WebSocket broken.");
|
|
193
|
+
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const message = this._parseMessage(event);
|
|
198
|
+
|
|
199
|
+
if (!message) return;
|
|
200
|
+
|
|
201
|
+
const eventConfig = this._getEventConfig(message);
|
|
202
|
+
|
|
203
|
+
if (!eventConfig) return;
|
|
204
|
+
|
|
205
|
+
this._resolveMessage(message, context).then((output) => {
|
|
206
|
+
if (!output) return;
|
|
207
|
+
|
|
208
|
+
const eventType = eventConfig.options?.type ?? "broadcast";
|
|
209
|
+
|
|
210
|
+
switch (eventType) {
|
|
211
|
+
case "self": {
|
|
212
|
+
Object.entries(output).forEach(([type, data]) => {
|
|
213
|
+
this._sendSelfMessage({ data, type }, session);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
case "sync": {
|
|
219
|
+
this._platform.pubSub.publish(this.room, {
|
|
220
|
+
connectionId: session.id,
|
|
221
|
+
options: eventConfig.options,
|
|
222
|
+
room: this.room,
|
|
223
|
+
user: session.user,
|
|
224
|
+
...message,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
Object.entries(output).forEach(([type, data]) => {
|
|
228
|
+
this._sendSelfMessage({ data, type }, session);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
case "broadcast":
|
|
234
|
+
default: {
|
|
235
|
+
Object.entries(output).forEach(([type, data]: any) => {
|
|
236
|
+
this._broadcast({
|
|
237
|
+
message: { data, type },
|
|
238
|
+
senderId: session.id,
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const closeHandler = () => {
|
|
247
|
+
if (!this._uninitialize) return;
|
|
248
|
+
|
|
249
|
+
session.quit = true;
|
|
250
|
+
|
|
251
|
+
this._logDebug(
|
|
252
|
+
`${chalk.blue(
|
|
253
|
+
`(Unregistering connection for room ${this.room}:`
|
|
254
|
+
)} ${sessionId}`
|
|
255
|
+
);
|
|
256
|
+
this._sessions.delete(session.id);
|
|
257
|
+
|
|
258
|
+
this._platform.persistance.deleteUser(this.room, session.id);
|
|
259
|
+
|
|
260
|
+
this._broadcast({
|
|
261
|
+
message: {
|
|
262
|
+
type: "$EXIT",
|
|
263
|
+
data: { sessionId: session.id },
|
|
264
|
+
},
|
|
265
|
+
senderId: session.id,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
uninitializeWs();
|
|
269
|
+
|
|
270
|
+
const size = this._getSessionsSize();
|
|
271
|
+
|
|
272
|
+
this._logDebug(
|
|
273
|
+
`${chalk.blue(
|
|
274
|
+
`Unregistered connection for room ${this.room}:`
|
|
275
|
+
)} ${sessionId}`
|
|
276
|
+
);
|
|
277
|
+
this._logDebug(`${chalk.blue(`Room ${this.room} size:`)} ${size}`);
|
|
278
|
+
|
|
279
|
+
if (size) return;
|
|
280
|
+
|
|
281
|
+
this._uninitialize();
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
pluvWs.addEventListener("close", closeHandler);
|
|
285
|
+
pluvWs.addEventListener("error", closeHandler);
|
|
286
|
+
|
|
287
|
+
this._logDebug(
|
|
288
|
+
`${chalk.blue(
|
|
289
|
+
`Registered connection for room ${this.room}:`
|
|
290
|
+
)} ${sessionId}`
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const size = this._getSessionsSize();
|
|
294
|
+
|
|
295
|
+
this._logDebug(`${chalk.blue(`Room ${this.room} size:`)} ${size}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private _broadcast(params: BroadcastParams<this>): void {
|
|
299
|
+
const { message, senderId } = params;
|
|
300
|
+
|
|
301
|
+
const currentTime = new Date().getTime();
|
|
302
|
+
const sender = senderId ? this._sessions.get(senderId) : null;
|
|
303
|
+
|
|
304
|
+
const quitters = Array.from(this._sessions.values()).filter(
|
|
305
|
+
(session) =>
|
|
306
|
+
session.quit ||
|
|
307
|
+
currentTime - session.timers.ping > PING_TIMEOUT_MS
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
quitters.forEach((session) => {
|
|
311
|
+
this._sessions.delete(session.id);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this._emitQuitters(quitters);
|
|
315
|
+
|
|
316
|
+
this._platform.pubSub.publish(this.room, {
|
|
317
|
+
connectionId: senderId ?? null,
|
|
318
|
+
room: this.room,
|
|
319
|
+
user: sender?.user ?? null,
|
|
320
|
+
...message,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private _emitQuitters(quitters: readonly WebSocketSession[]): void {
|
|
325
|
+
quitters.forEach((quitter) => {
|
|
326
|
+
this._broadcast({
|
|
327
|
+
message: {
|
|
328
|
+
type: "$EXIT",
|
|
329
|
+
data: { sessionId: quitter.id },
|
|
330
|
+
},
|
|
331
|
+
senderId: quitter.id,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private _emitRegistered(session: WebSocketSession): void {
|
|
337
|
+
this._sendSelfMessage(
|
|
338
|
+
{
|
|
339
|
+
type: "$REGISTERED",
|
|
340
|
+
data: { sessionId: session.id },
|
|
341
|
+
},
|
|
342
|
+
session
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private _emitStorageReceived(session: WebSocketSession): void {
|
|
347
|
+
this._platform.persistance.getStorageState(this.room).then((_state) => {
|
|
348
|
+
const state = _state ?? this._doc.encodeStateAsUpdate();
|
|
349
|
+
|
|
350
|
+
this._sendSelfMessage(
|
|
351
|
+
{
|
|
352
|
+
type: "$STORAGE_RECEIVED",
|
|
353
|
+
data: { state },
|
|
354
|
+
},
|
|
355
|
+
session
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private _emitUserJoined(session: WebSocketSession): void {
|
|
361
|
+
const user = session.user as any;
|
|
362
|
+
|
|
363
|
+
if (!user) return;
|
|
364
|
+
|
|
365
|
+
this._broadcast({
|
|
366
|
+
message: {
|
|
367
|
+
type: "$USER_JOINED",
|
|
368
|
+
data: {
|
|
369
|
+
connectionId: session.id,
|
|
370
|
+
user,
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
senderId: session.id,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private async _getAuthorizedUser(
|
|
378
|
+
token: string | undefined
|
|
379
|
+
): Promise<InferIOAuthorizeUser<InferIOAuthorize<this>> | null> {
|
|
380
|
+
if (!this._authorize) return null;
|
|
381
|
+
if (!token) return null;
|
|
382
|
+
|
|
383
|
+
const payload = await authorize({
|
|
384
|
+
platform: this._platform,
|
|
385
|
+
secret: this._authorize.secret,
|
|
386
|
+
}).decode(token);
|
|
387
|
+
|
|
388
|
+
if (!payload) {
|
|
389
|
+
this._logDebug(chalk.blue("Could not decode token:"));
|
|
390
|
+
this._logDebug(token);
|
|
391
|
+
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (payload.room !== this.room) {
|
|
396
|
+
this._logDebug(
|
|
397
|
+
chalk.blue(`Token is not authorized for room ${this.room}:`)
|
|
398
|
+
);
|
|
399
|
+
this._logDebug(chalk.blue("Received:"), payload.room);
|
|
400
|
+
this._logDebug(token);
|
|
401
|
+
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
return this._authorize.user.parse(payload.user) ?? null;
|
|
407
|
+
} catch {
|
|
408
|
+
this._logDebug(`${chalk.blue("Token fails validation:")} ${token}`);
|
|
409
|
+
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private _getSessionsSize(): number {
|
|
415
|
+
const currentTime = new Date().getTime();
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @description Doing this instead of .size because some sessions
|
|
419
|
+
* in the map can be considered as "omitted".
|
|
420
|
+
* @author leedavidcs
|
|
421
|
+
* @date December 21, 2022
|
|
422
|
+
*/
|
|
423
|
+
return Array.from(this._sessions.values()).reduce((count, session) => {
|
|
424
|
+
if (session.quit) return count;
|
|
425
|
+
|
|
426
|
+
return currentTime - session.timers.ping > PING_TIMEOUT_MS
|
|
427
|
+
? count
|
|
428
|
+
: count + 1;
|
|
429
|
+
}, 0);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private _getEventConfig(
|
|
433
|
+
message: EventMessage<string, any>
|
|
434
|
+
): InferEventConfig<TContext, TInput, TOutput>[keyof TInput] | null {
|
|
435
|
+
return this._events[message.type as keyof TInput] ?? null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async _initialize(): Promise<void> {
|
|
439
|
+
this._logDebug(`${chalk.blue(`Initializing room ${this.room}:`)}`);
|
|
440
|
+
|
|
441
|
+
if (!!this._uninitialize) {
|
|
442
|
+
await this._uninitialize();
|
|
443
|
+
|
|
444
|
+
this._uninitialize = null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const pubSubId = await this._platform.pubSub.subscribe(
|
|
448
|
+
this.room,
|
|
449
|
+
({ options = {}, ...message }) => {
|
|
450
|
+
const session = {
|
|
451
|
+
id: message.connectionId,
|
|
452
|
+
user: message.user,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
this._sendMessage(message, session, options);
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
this._uninitialize = async () => {
|
|
460
|
+
this._platform.pubSub.unsubscribe(pubSubId);
|
|
461
|
+
|
|
462
|
+
this._doc.destroy();
|
|
463
|
+
this._doc = doc();
|
|
464
|
+
|
|
465
|
+
this._listeners.onDestroy();
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private _logDebug(...data: any[]): void {
|
|
470
|
+
this._debug && console.log(...data);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private _parseMessage(message: {
|
|
474
|
+
data: string | ArrayBuffer;
|
|
475
|
+
}): EventMessage<string, any> | null {
|
|
476
|
+
const parsed = this._platform.parseData(message.data);
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
if (typeof parsed !== "object") return null;
|
|
480
|
+
if (typeof parsed.type !== "string") return null;
|
|
481
|
+
if (typeof parsed.data !== "object") return null;
|
|
482
|
+
|
|
483
|
+
return parsed as EventMessage<string, any>;
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private async _resolveMessage(
|
|
490
|
+
message: EventMessage<string, any>,
|
|
491
|
+
context: EventResolverContext<TContext>
|
|
492
|
+
): Promise<TOutput | null> {
|
|
493
|
+
const { session } = context;
|
|
494
|
+
|
|
495
|
+
const eventConfig = this._getEventConfig(message);
|
|
496
|
+
|
|
497
|
+
if (!eventConfig) return null;
|
|
498
|
+
|
|
499
|
+
let input: TInput[string];
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* !HACK
|
|
503
|
+
* @description If the config doesn't specify validation, we
|
|
504
|
+
* assume no input is to be expected, but pass through all values
|
|
505
|
+
* regardless. Leave this up to the implementer of the event.
|
|
506
|
+
* @author leedavidcs
|
|
507
|
+
* @date September 25, 2022
|
|
508
|
+
*/
|
|
509
|
+
try {
|
|
510
|
+
input = eventConfig.input
|
|
511
|
+
? (eventConfig.input.parse(message.data) as TInput[string])
|
|
512
|
+
: message.data;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
session?.webSocket.handleError({
|
|
515
|
+
error,
|
|
516
|
+
message: "Invalid input",
|
|
517
|
+
session,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const output = await eventConfig.resolver(input, context);
|
|
524
|
+
|
|
525
|
+
if (!output) return null;
|
|
526
|
+
|
|
527
|
+
return output;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private _sendMessage(
|
|
531
|
+
message: EventMessage<string, any>,
|
|
532
|
+
sender: SendMessageSender | null,
|
|
533
|
+
options: SendMessageOptions = {}
|
|
534
|
+
): void {
|
|
535
|
+
switch (options.type) {
|
|
536
|
+
case "self": {
|
|
537
|
+
this._sendSelfMessage(message, sender);
|
|
538
|
+
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
case "sync": {
|
|
542
|
+
this._sendSyncMessage(message, sender);
|
|
543
|
+
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
case "broadcast":
|
|
547
|
+
default: {
|
|
548
|
+
const sessionIds = options.sessionIds;
|
|
549
|
+
|
|
550
|
+
this._sendBroadcastMessage(message, sender, sessionIds);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private _sendBroadcastMessage(
|
|
556
|
+
message: EventMessage<string, any>,
|
|
557
|
+
sender: SendMessageSender | null,
|
|
558
|
+
sessionIds?: readonly string[]
|
|
559
|
+
): void {
|
|
560
|
+
const senderId = sender?.id ?? null;
|
|
561
|
+
|
|
562
|
+
const sessions =
|
|
563
|
+
sessionIds?.reduce((dict, id) => {
|
|
564
|
+
const _session = this._sessions.get(id);
|
|
565
|
+
|
|
566
|
+
return _session ? dict.set(id, _session) : dict;
|
|
567
|
+
}, new Map<string, WebSocketSession>()) ?? this._sessions;
|
|
568
|
+
|
|
569
|
+
Array.from(sessions.values()).forEach((_session) => {
|
|
570
|
+
_session.webSocket.sendMessage({
|
|
571
|
+
connectionId: senderId,
|
|
572
|
+
room: this.room,
|
|
573
|
+
user: _session.user,
|
|
574
|
+
...message,
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private _sendSelfMessage(
|
|
580
|
+
message: EventMessage<string, any>,
|
|
581
|
+
sender: SendMessageSender | null
|
|
582
|
+
): void {
|
|
583
|
+
const senderId = sender?.id;
|
|
584
|
+
|
|
585
|
+
if (!senderId) return;
|
|
586
|
+
|
|
587
|
+
const session = this._sessions.get(senderId);
|
|
588
|
+
|
|
589
|
+
if (!session) return;
|
|
590
|
+
|
|
591
|
+
session.webSocket.sendMessage({
|
|
592
|
+
connectionId: sender.id,
|
|
593
|
+
room: this.room,
|
|
594
|
+
user: sender.user,
|
|
595
|
+
...message,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private _sendSyncMessage(
|
|
600
|
+
message: EventMessage<string, any>,
|
|
601
|
+
sender: SendMessageSender | null
|
|
602
|
+
): void {
|
|
603
|
+
const senderId = sender?.id;
|
|
604
|
+
|
|
605
|
+
if (!senderId) return;
|
|
606
|
+
|
|
607
|
+
this._resolveMessage(message, {
|
|
608
|
+
context: this._context,
|
|
609
|
+
doc: this._doc,
|
|
610
|
+
room: this.room,
|
|
611
|
+
session: null,
|
|
612
|
+
sessions: this._sessions,
|
|
613
|
+
}).then((output) => {
|
|
614
|
+
if (!output) return;
|
|
615
|
+
|
|
616
|
+
Object.keys(output).forEach((type: string) => {
|
|
617
|
+
const data = output[type] ?? {};
|
|
618
|
+
|
|
619
|
+
this._platform.pubSub.publish(this.room, {
|
|
620
|
+
connectionId: senderId,
|
|
621
|
+
data,
|
|
622
|
+
options: { type: "self" },
|
|
623
|
+
room: this.room,
|
|
624
|
+
type,
|
|
625
|
+
user: sender.user ?? null,
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { JsonObject } from "@pluv/types";
|
|
2
|
+
import { AbstractPersistance } from "./AbstractPersistance";
|
|
3
|
+
|
|
4
|
+
export class Persistance extends AbstractPersistance {
|
|
5
|
+
private _storages = new Map<string, string>();
|
|
6
|
+
private _users = new Map<string, Map<string, JsonObject | null>>();
|
|
7
|
+
|
|
8
|
+
public addUser(
|
|
9
|
+
room: string,
|
|
10
|
+
connectionId: string,
|
|
11
|
+
user: JsonObject | null
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const users =
|
|
14
|
+
this._users.get(room) ?? new Map<string, JsonObject | null>();
|
|
15
|
+
|
|
16
|
+
this._users.set(room, users);
|
|
17
|
+
users.set(connectionId, user);
|
|
18
|
+
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public deleteStorageState(room: string): Promise<void> {
|
|
23
|
+
this._storages.delete(room);
|
|
24
|
+
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async deleteUser(room: string, connectionId: string): Promise<void> {
|
|
29
|
+
const users = this._users.get(room);
|
|
30
|
+
|
|
31
|
+
if (!users) return;
|
|
32
|
+
|
|
33
|
+
users.delete(connectionId);
|
|
34
|
+
|
|
35
|
+
if (!users.size) {
|
|
36
|
+
await this.deleteUsers(room);
|
|
37
|
+
await this.deleteStorageState(room);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public deleteUsers(room: string): Promise<void> {
|
|
44
|
+
this._users.delete(room);
|
|
45
|
+
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public getStorageState(room: string): Promise<string | null> {
|
|
50
|
+
const storage = this._storages.get(room) ?? null;
|
|
51
|
+
|
|
52
|
+
return Promise.resolve(storage);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public getUser(
|
|
56
|
+
room: string,
|
|
57
|
+
connectionId: string
|
|
58
|
+
): Promise<JsonObject | null> {
|
|
59
|
+
const user = this._users.get(room)?.get(connectionId);
|
|
60
|
+
|
|
61
|
+
return Promise.resolve(user ?? null);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public getUsers(room: string): Promise<readonly (JsonObject | null)[]> {
|
|
65
|
+
const users = this._users.get(room);
|
|
66
|
+
|
|
67
|
+
if (!users) return Promise.resolve([]);
|
|
68
|
+
|
|
69
|
+
return Promise.resolve(Array.from(users.values()));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public setStorageState(room: string, state: string): Promise<void> {
|
|
73
|
+
this._storages.set(room, state);
|
|
74
|
+
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
}
|
|
77
|
+
}
|