@pluv/io 5.0.0 → 5.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/dist/index.mjs DELETED
@@ -1,1421 +0,0 @@
1
- import { makeSubject, subscribe } from "wonka";
2
- import hkdf from "@panva/hkdf";
3
- import { EncryptJWT, jwtDecrypt } from "jose";
4
- import { noop } from "@pluv/crdt";
5
- import colors from "kleur";
6
- //#region src/AbstractPersistence.ts
7
- var AbstractPersistence = class {};
8
- //#endregion
9
- //#region src/Persistence.ts
10
- var Persistence = class extends AbstractPersistence {
11
- _storages = /* @__PURE__ */ new Map();
12
- _users = /* @__PURE__ */ new Map();
13
- addUser(room, connectionId, user) {
14
- const users = this._users.get(room) ?? /* @__PURE__ */ new Map();
15
- this._users.set(room, users);
16
- users.set(connectionId, user);
17
- return Promise.resolve();
18
- }
19
- deleteStorageState(room) {
20
- this._storages.delete(room);
21
- return Promise.resolve();
22
- }
23
- async deleteUser(room, connectionId) {
24
- const users = this._users.get(room);
25
- if (!users) return;
26
- users.delete(connectionId);
27
- if (!users.size) {
28
- await this.deleteUsers(room);
29
- await this.deleteStorageState(room);
30
- }
31
- return Promise.resolve();
32
- }
33
- deleteUsers(room) {
34
- this._users.delete(room);
35
- return Promise.resolve();
36
- }
37
- getSize(room) {
38
- return Promise.resolve(this._users.get(room)?.size ?? 0);
39
- }
40
- getStorageState(room) {
41
- const storage = this._storages.get(room) ?? null;
42
- return Promise.resolve(storage);
43
- }
44
- getUser(room, connectionId) {
45
- const user = this._users.get(room)?.get(connectionId);
46
- return Promise.resolve(user ?? null);
47
- }
48
- async getUsers(room) {
49
- const users = this._users.get(room);
50
- if (!users) return /* @__PURE__ */ new Map();
51
- return users;
52
- }
53
- async getUsersSize(room) {
54
- return this._users.size;
55
- }
56
- initialize(roomContext) {
57
- return this;
58
- }
59
- setStorageState(room, state) {
60
- this._storages.set(room, state);
61
- return Promise.resolve();
62
- }
63
- };
64
- //#endregion
65
- //#region src/AbstractPubSub.ts
66
- var AbstractPubSub = class {};
67
- //#endregion
68
- //#region src/PubSub.ts
69
- var PubSub = class extends AbstractPubSub {
70
- _currentSubscriptionId = 0;
71
- _subjects = /* @__PURE__ */ new Map();
72
- _subscriptionsRefs = /* @__PURE__ */ new Map();
73
- _unsubscriptions = /* @__PURE__ */ new Map();
74
- async publish(roomName, payload) {
75
- const subject = this._subjects.get(roomName);
76
- if (!subject) return;
77
- Promise.resolve(subject.next(payload));
78
- }
79
- subscribe(roomName, onMessage) {
80
- const id = this._currentSubscriptionId++;
81
- const refs = this._subscriptionsRefs.get(roomName) ?? [];
82
- const subject = makeSubject();
83
- this._subjects.set(roomName, subject);
84
- this._subscriptionsRefs.set(roomName, [...refs, id]);
85
- const unsubscribe = subscribe(onMessage)(subject.source).unsubscribe;
86
- this._unsubscriptions.set(id, [roomName, unsubscribe]);
87
- return Promise.resolve(id);
88
- }
89
- unsubscribe(subscriptionId) {
90
- const [roomName, unsubscribe] = this._unsubscriptions.get(subscriptionId) ?? [];
91
- if (!roomName || !unsubscribe) return;
92
- const refs = this._subscriptionsRefs.get(roomName);
93
- if (!refs) return;
94
- if (refs.length === 1) {
95
- this._subjects.delete(roomName);
96
- this._subscriptionsRefs.delete(roomName);
97
- this._unsubscriptions.delete(subscriptionId);
98
- return;
99
- }
100
- const index = refs.indexOf(subscriptionId);
101
- this._subscriptionsRefs.set(roomName, index === -1 ? refs : [...refs.slice(0, index), ...refs.slice(index + 1)]);
102
- unsubscribe();
103
- this._unsubscriptions.delete(subscriptionId);
104
- }
105
- };
106
- //#endregion
107
- //#region src/AbstractPlatform.ts
108
- var AbstractPlatform = class {
109
- _initContext;
110
- _roomContext;
111
- _initialized = false;
112
- persistence;
113
- pubSub;
114
- _createToken;
115
- _fetch;
116
- constructor(config = {}) {
117
- const { roomContext, persistence, pubSub } = config;
118
- this._meta = config._meta;
119
- if (!!roomContext) this._roomContext = roomContext;
120
- this.persistence = persistence ?? new Persistence();
121
- this.pubSub = pubSub ?? new PubSub();
122
- }
123
- validateConfig(config) {}
124
- _initialize() {
125
- if (this._initialized) throw new Error("Platform is already initialized");
126
- this._initialized = true;
127
- return this;
128
- }
129
- };
130
- //#endregion
131
- //#region src/AbstractWebSocket.ts
132
- var AbstractWebSocket = class {
133
- /** The connection is not yet open. */
134
- CONNECTING = 0;
135
- /** The connection is open and ready to communicate. */
136
- OPEN = 1;
137
- /** The connection is in the process of closing. */
138
- CLOSING = 2;
139
- /** The connection is closed. */
140
- CLOSED = 3;
141
- persistence;
142
- room;
143
- webSocket;
144
- _platform;
145
- constructor(webSocket, config) {
146
- const { persistence, platform, room } = config;
147
- this.persistence = persistence;
148
- this._platform = platform;
149
- this.room = room;
150
- this.webSocket = webSocket;
151
- }
152
- handleError(params) {
153
- const { error, message: _message, room, session } = params;
154
- const message = error instanceof Error ? error.message : "Unexpected error";
155
- const stack = error instanceof Error ? error.stack ?? null : null;
156
- const connectionId = session?.id ?? null;
157
- return this.sendMessage({
158
- connectionId,
159
- data: {
160
- message,
161
- stack
162
- },
163
- room,
164
- type: "$error",
165
- user: session?.user ?? null
166
- });
167
- }
168
- sendMessage(data) {
169
- return this.send(JSON.stringify(data));
170
- }
171
- };
172
- //#endregion
173
- //#region src/authorize.ts
174
- const DEFAULT_MAX_AGE_MS = 6e4;
175
- const getEncryptionKey = async (secret) => {
176
- return await hkdf("sha256", secret, "", "Pluv.io Generated Encryption Key", 32);
177
- };
178
- const now = () => Date.now() / 1e3 | 0;
179
- const authorize = (params) => {
180
- const { platform, secret } = params;
181
- const encode = async (encodeParams) => {
182
- const { maxAge = DEFAULT_MAX_AGE_MS, room, user } = encodeParams;
183
- const encryptionSecret = await getEncryptionKey(secret);
184
- return (await new EncryptJWT({
185
- room,
186
- sub: room ? `${room}|${user.id}` : user.id,
187
- user
188
- }).setProtectedHeader({
189
- alg: "dir",
190
- enc: "A256GCM"
191
- }).setIssuedAt().setExpirationTime(now() + maxAge).setJti(platform.randomUUID()).encrypt(encryptionSecret)).toString();
192
- };
193
- const decode = async (jwt) => {
194
- const { payload } = await jwtDecrypt(jwt, await getEncryptionKey(secret), { clockTolerance: 15 });
195
- return payload ?? null;
196
- };
197
- return {
198
- decode,
199
- encode
200
- };
201
- };
202
- //#endregion
203
- //#region src/PluvProcedure.ts
204
- var PluvProcedure = class PluvProcedure {
205
- _broadcast = null;
206
- _input = null;
207
- _self = null;
208
- _sync = null;
209
- get config() {
210
- return {
211
- broadcast: this._broadcast?.bind(this) ?? null,
212
- input: this._input ?? null,
213
- resolver: this._resolver(),
214
- self: this._self?.bind(this) ?? null,
215
- sync: this._sync?.bind(this) ?? null
216
- };
217
- }
218
- constructor(config = {}) {
219
- const { broadcast, input, self, sync } = config;
220
- this._broadcast = broadcast ?? null;
221
- this._input = input ?? null;
222
- this._self = self ?? null;
223
- this._sync = sync ?? null;
224
- }
225
- broadcast(resolver) {
226
- const { broadcast } = this.config;
227
- if (!!broadcast) throw new Error("Broadcast was already defined for this procedure");
228
- return new PluvProcedure({
229
- ...this.config,
230
- broadcast: resolver
231
- });
232
- }
233
- input(input) {
234
- return new PluvProcedure({ input });
235
- }
236
- self(resolver) {
237
- const { self } = this.config;
238
- if (!!self) throw new Error("Self was already defined for this procedure");
239
- return new PluvProcedure({
240
- ...this.config,
241
- self: resolver
242
- });
243
- }
244
- sync(resolver) {
245
- const { sync } = this.config;
246
- if (!!sync) throw new Error("Sync was already defined for this procedure");
247
- return new PluvProcedure({
248
- ...this.config,
249
- sync: resolver
250
- });
251
- }
252
- _resolver() {
253
- return (data, context) => {
254
- return {
255
- ...this._broadcast?.(data, context),
256
- ...this._self?.(data, context),
257
- ...this._sync?.(data, context)
258
- };
259
- };
260
- }
261
- };
262
- //#endregion
263
- //#region src/PluvRouter.ts
264
- var PluvRouter = class PluvRouter {
265
- _defs = { events: {} };
266
- constructor(events) {
267
- const invalidName = Object.keys(events).find((name) => !this._isValidEventName(name));
268
- if (typeof invalidName === "string") throw new Error(`Invalid event name. Event names must be formatted as valid JavaScript variable names: "${invalidName}"`);
269
- this._defs = { events };
270
- }
271
- static merge(...routers) {
272
- return new PluvRouter(Object.assign(Object.create(null), ...routers.map((router) => router._defs.events)));
273
- }
274
- _isValidEventName(name) {
275
- return /^[a-z_$][a-z0-9_$]*$/gi.test(name);
276
- }
277
- };
278
- const MAX_STORAGE_SIZE_BYTES = 31457280;
279
- //#endregion
280
- //#region src/utils/oneLine.ts
281
- const oneLine = (strings, ...values) => {
282
- return strings.map((str) => str.replace(/\s+/g, " ")).reduce((acc, str, i) => acc + str + (i < values.length ? values[i] : ""), "").replace(/\s+/g, " ").trim();
283
- };
284
- //#endregion
285
- //#region src/utils/pickBy.ts
286
- const pickBy = (obj, by) => {
287
- const result = {};
288
- const keys = Object.keys(obj);
289
- for (let i = 0; i < keys.length; ++i) {
290
- const key = keys[i];
291
- const value = obj[key];
292
- if (by(value)) result[key] = value;
293
- }
294
- return result;
295
- };
296
- //#endregion
297
- //#region src/IORoom.ts
298
- var IORoom = class {
299
- id;
300
- _doc;
301
- _lastGarbageCollectMs = -1 * 60001;
302
- _uninitialize = null;
303
- _authorize = null;
304
- _context;
305
- _crdt;
306
- _debug;
307
- _docFactory;
308
- _getInitialStorage;
309
- _listeners;
310
- _platform;
311
- _roomContext;
312
- _router;
313
- _sessions = /* @__PURE__ */ new Map();
314
- _userSessionss = /* @__PURE__ */ new Map();
315
- _wasDocEmptyOnInit = true;
316
- _storageInitializedViaSession = false;
317
- /**
318
- * @ignore
319
- * @readonly
320
- * @deprecated Internal use only. Changes to this will never be marked as breaking.
321
- */
322
- get _defs() {
323
- return {
324
- authorize: this._authorize,
325
- context: this._context,
326
- crdt: this._crdt,
327
- events: this._router._defs.events,
328
- platform: this._platform
329
- };
330
- }
331
- get broadcast() {
332
- const _broadcast = (event, data) => {
333
- const message = {
334
- type: event,
335
- data
336
- };
337
- return Promise.resolve(this._broadcast({ message }));
338
- };
339
- return new Proxy(_broadcast, { get(fn, prop) {
340
- return (data) => fn(prop, data);
341
- } });
342
- }
343
- get _initialized() {
344
- return Promise.resolve(!!this._uninitialize?.then((result) => !!result));
345
- }
346
- constructor(id, config) {
347
- const { _meta, authorize, context, crdt = noop, debug, getInitialStorage, onRoomDestroyed, onStorageDestroyed, onMessage, onUserConnected, onUserDisconnected, platform, roomContext, router } = config;
348
- this.id = id;
349
- this._context = context;
350
- this._crdt = crdt;
351
- this._debug = debug;
352
- this._docFactory = crdt.doc(() => ({}));
353
- this._getInitialStorage = getInitialStorage;
354
- this._roomContext = roomContext;
355
- this._router = router;
356
- this._platform = platform.initialize({
357
- ...!!_meta ? { _meta } : {},
358
- roomContext
359
- });
360
- this._listeners = {
361
- onRoomDestroyed: (event) => onRoomDestroyed?.(event),
362
- onStorageDestroyed: (event) => onStorageDestroyed?.(event),
363
- onMessage: (event) => onMessage?.(event),
364
- onUserConnected: (event) => onUserConnected?.(event),
365
- onUserDisconnected: (event) => onUserDisconnected?.(event)
366
- };
367
- if (authorize) this._authorize = authorize;
368
- this._platform.getWebSockets().forEach((webSocket) => {
369
- const deserialized = this._platform.getSerializedState(webSocket);
370
- const sessionId = this._platform.getSessionId(webSocket);
371
- if (!deserialized) return;
372
- if (typeof sessionId !== "string") return;
373
- const pluvWs = this._platform.convertWebSocket(webSocket, { room: this.id });
374
- this._sessions.set(sessionId, pluvWs);
375
- const userId = pluvWs.session.user?.id;
376
- if (!!userId) this._addUserSession(userId, sessionId);
377
- });
378
- const { doc, uninitialize } = this._initialize();
379
- this._doc = doc;
380
- this._uninitialize = uninitialize;
381
- }
382
- /**
383
- * @description Closes a connection with the specified sessionId
384
- * @param sessionId The session id of the connection to close
385
- */
386
- async evict(sessionId) {
387
- const session = this._sessions.get(sessionId);
388
- if (session) session.state.quit = true;
389
- await this._emitQuitters();
390
- }
391
- /**
392
- * @description Closes all connections to this room, effectively destroying
393
- * the room
394
- */
395
- async evictAll() {
396
- Array.from(this._sessions.values()).forEach((session) => {
397
- session.state.quit = true;
398
- });
399
- await this._emitQuitters();
400
- }
401
- /**
402
- * @description The IORoom will garbage collect occasionally as connections ping/pong the
403
- * server
404
- */
405
- async garbageCollect() {
406
- this._lastGarbageCollectMs = Date.now();
407
- this._emitQuitters();
408
- this._emitSyncState();
409
- }
410
- getSize() {
411
- const currentTime = (/* @__PURE__ */ new Date()).getTime();
412
- /**
413
- * @description Doing this instead of .size because some sessions
414
- * in the map can be considered as "omitted".
415
- * @date December 21, 2022
416
- */
417
- return Array.from(this._sessions.values()).reduce((count, pluvWs) => {
418
- if (pluvWs.state.quit) return count;
419
- return currentTime - (this._platform.getLastPing(pluvWs) ?? pluvWs.state.timers.ping) > 3e4 ? count : count + 1;
420
- }, 0);
421
- }
422
- onClose(webSocket) {
423
- this._ensureDetached();
424
- const wsSession = this._getAbstractWs(webSocket);
425
- if (!wsSession) return async () => void 0;
426
- return this._onClose(wsSession);
427
- }
428
- onError(webSocket) {
429
- this._ensureDetached();
430
- const wsSession = this._getAbstractWs(webSocket);
431
- if (!wsSession) return async () => void 0;
432
- return this._onClose(wsSession);
433
- }
434
- onMessage(webSocket) {
435
- this._ensureDetached();
436
- const wsSession = this._getAbstractWs(webSocket);
437
- if (!wsSession) return async () => void 0;
438
- return this._onMessage(wsSession);
439
- }
440
- async register(webSocket, ...options) {
441
- const _options = options[0] ?? {};
442
- const token = _options.token ?? null;
443
- /**
444
- * TODO
445
- * @description Update the sessionId strategy so that the userId can be used to retrieve
446
- * the session in O(1) time if authorized.
447
- * @date April 15, 2025
448
- */
449
- const sessionId = this._platform.getSessionId(webSocket);
450
- if (typeof sessionId === "string" && this._sessions.has(sessionId)) return;
451
- if (!await this._initialized) {
452
- this._initialize();
453
- await this._initialized;
454
- }
455
- const user = await this._getAuthorizedUser(token, _options);
456
- const isUnauthorized = !!this._getIOAuthorize(_options) && !user;
457
- const pluvWs = this._platform.convertWebSocket(webSocket, { room: this.id });
458
- if (isUnauthorized) {
459
- this._logDebug(`${colors.blue("Authorization failed for connection")}`);
460
- pluvWs.handleError({
461
- error: /* @__PURE__ */ new Error("Not authorized"),
462
- room: this.id
463
- });
464
- pluvWs.close(3e3, "WebSocket unauthorized.");
465
- return;
466
- }
467
- if (!!user) {
468
- const latest = this._getLatestPresence(user.id);
469
- const prevState = pluvWs.state;
470
- pluvWs.user = user;
471
- this._platform.setSerializedState(pluvWs, {
472
- ...prevState,
473
- presence: latest.presence,
474
- timers: {
475
- ...prevState.timers,
476
- presence: latest.timer
477
- }
478
- });
479
- this._addUserSession(user.id, pluvWs.sessionId);
480
- }
481
- this._logDebug(`${colors.blue(`Registering connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
482
- await this._platform.acceptWebSocket(pluvWs);
483
- this._sessions.set(pluvWs.sessionId, pluvWs);
484
- await this._platform.persistence.addUser(this.id, pluvWs.sessionId, user ?? {});
485
- if (this._platform._config.registrationMode === "attached") {
486
- const onClose = this._onClose(pluvWs).bind(this);
487
- const onMessage = this._onMessage(pluvWs).bind(this);
488
- pluvWs.addEventListener("close", onClose);
489
- pluvWs.addEventListener("error", onClose);
490
- pluvWs.addEventListener("message", onMessage);
491
- }
492
- await this._emitRegistered(pluvWs);
493
- const size = this.getSize();
494
- this._logDebug(oneLine`
495
- ${colors.blue(`Registered connection for room ${this.id}:`)}
496
- ${pluvWs.sessionId}
497
- `);
498
- this._logDebug(`${colors.blue(`Room ${this.id} size:`)} ${size}`);
499
- }
500
- _addUserSession(userId, sessionId) {
501
- const updated = (this._userSessionss.get(userId) ?? /* @__PURE__ */ new Set()).add(sessionId);
502
- this._userSessionss.set(userId, updated);
503
- return updated;
504
- }
505
- async _broadcast(params) {
506
- const { message, senderId } = params;
507
- const user = ((senderId ? this._sessions.get(senderId) ?? null : null)?.session ?? null)?.user ?? null;
508
- if (typeof senderId !== "string") return;
509
- this._platform.pubSub.publish(this.id, {
510
- connectionId: senderId ?? null,
511
- room: this.id,
512
- user,
513
- ...message
514
- });
515
- }
516
- async _closeWebSockets(webSockets) {
517
- const closeWebSocket = async (webSocket) => {
518
- webSocket.state = {
519
- ...webSocket.state,
520
- quit: true
521
- };
522
- const sessionId = webSocket.sessionId;
523
- this._logDebug(`${colors.blue(`Unregistering connection for room ${this.id}:`)} ${sessionId}`);
524
- this._sessions.delete(sessionId);
525
- await this._platform.persistence.deleteUser(this.id, sessionId).catch(() => null);
526
- await this._broadcast({
527
- message: {
528
- type: "$exit",
529
- data: { sessionId }
530
- },
531
- senderId: sessionId
532
- });
533
- const user = webSocket.session.user;
534
- if (!!user) this._removeUserSession(user.id, sessionId);
535
- try {
536
- const [doc, context] = await Promise.all([this._doc, this._getContext()]);
537
- const encodedState = doc.getEncodedState();
538
- await Promise.resolve(this._listeners.onUserDisconnected({
539
- context,
540
- encodedState,
541
- platform: this._platform,
542
- room: this.id,
543
- user
544
- }));
545
- } catch (error) {
546
- console.error(error);
547
- }
548
- this._logDebug(`${colors.blue(`Unregistered connection for room ${this.id}:`)} ${webSocket.sessionId}`);
549
- };
550
- const promises = webSockets.map(async (webSocket) => {
551
- await closeWebSocket(webSocket).catch(() => null);
552
- });
553
- await Promise.all(promises).catch(() => null);
554
- const size = this.getSize();
555
- this._logDebug(`${colors.blue(`Room ${this.id} size:`)} ${size}`);
556
- if (!!size) return;
557
- if (!this._uninitialize) return;
558
- await (await this._uninitialize).bind(this)();
559
- }
560
- async _emitQuitters() {
561
- const currentTime = (/* @__PURE__ */ new Date()).getTime();
562
- const quitters = Array.from(this._sessions.values()).filter((pluvWs) => {
563
- return pluvWs.state.quit || currentTime - pluvWs.state.timers.ping > 3e4;
564
- });
565
- this._closeWebSockets(quitters);
566
- }
567
- async _emitRegistered(pluvWs) {
568
- const session = pluvWs.session;
569
- const sessionId = session.id;
570
- const presence = session.presence;
571
- const user = session.user;
572
- const [doc, context] = await Promise.all([this._doc, this._getContext()]);
573
- const encodedState = doc.isEmpty() ? null : doc.getEncodedState();
574
- await this._sendSelfMessage({
575
- type: "$registered",
576
- data: {
577
- presence,
578
- sessionId,
579
- state: encodedState,
580
- timers: { presence: session.timers.presence }
581
- }
582
- }, {
583
- sessionId,
584
- user
585
- });
586
- try {
587
- await Promise.resolve(this._listeners.onUserConnected({
588
- context,
589
- encodedState,
590
- platform: this._platform,
591
- room: this.id,
592
- user,
593
- webSocket: pluvWs.webSocket
594
- }));
595
- } catch (err) {
596
- console.error(err);
597
- }
598
- }
599
- async _emitSyncState() {
600
- const connectionIds = await this._platform.persistence.getUsers(this.id).then((map) => Object.keys(map));
601
- await this._broadcast({ message: {
602
- type: "$syncStateReceived",
603
- data: { connectionIds }
604
- } });
605
- }
606
- _ensureDetached() {
607
- if (this._platform._config.registrationMode === "detached") return;
608
- throw new Error("Platform must use detached mode");
609
- }
610
- _getAbstractWs(webSocket) {
611
- if (webSocket instanceof AbstractWebSocket) return webSocket;
612
- const sessionId = this._platform.getSessionId(webSocket);
613
- if (typeof sessionId === "string") {
614
- const session = this._sessions.get(sessionId) ?? null;
615
- if (session) return session;
616
- }
617
- return Array.from(this._sessions.values()).find((pluvWs) => pluvWs.webSocket === webSocket) ?? null;
618
- }
619
- /**
620
- * TODO
621
- * @description Revisit any cast types
622
- * @date July 17, 2025
623
- */
624
- async _getAuthorizedUser(token, options) {
625
- const ioAuthorize = this._getIOAuthorize(options);
626
- if (!ioAuthorize) return null;
627
- if (!token) return null;
628
- if (!ioAuthorize.secret) throw new Error("`authorize` was specified without a valid secret");
629
- const payload = await authorize({
630
- platform: this._platform,
631
- secret: ioAuthorize.secret
632
- }).decode(token);
633
- if (!payload) {
634
- this._logDebug(colors.blue("Could not decode token:"));
635
- this._logDebug(token);
636
- return null;
637
- }
638
- if (payload.room !== this.id) {
639
- this._logDebug(colors.blue(`Token is not authorized for room ${this.id}:`));
640
- this._logDebug(colors.blue("Received:"), payload.room);
641
- this._logDebug(token);
642
- return null;
643
- }
644
- try {
645
- return ioAuthorize.user.parse(payload.user) ?? null;
646
- } catch {
647
- this._logDebug(`${colors.blue("Token fails validation:")} ${token}`);
648
- return null;
649
- }
650
- }
651
- async _getContext() {
652
- return typeof this._context === "function" ? await Promise.resolve(this._context(this._roomContext)) : await Promise.resolve(this._context);
653
- }
654
- async _getInitialDoc() {
655
- const doc = this._docFactory.getEmpty();
656
- const [encodedState, context] = await Promise.all([this._platform.persistence.getStorageState(this.id), this._getContext()]);
657
- if (!encodedState) {
658
- const loadedState = await this._getInitialStorage({
659
- context,
660
- room: this.id
661
- });
662
- if (!!loadedState && !this._docFactory.isEmpty(loadedState)) doc.applyEncodedState({ update: loadedState });
663
- }
664
- if (typeof encodedState === "string") {
665
- doc.applyEncodedState({ update: encodedState });
666
- if (!doc.isEmpty()) this._storageInitializedViaSession = true;
667
- }
668
- return doc;
669
- }
670
- _getIOAuthorize(options) {
671
- if (typeof this._authorize === "function") return this._authorize(options);
672
- return this._authorize;
673
- }
674
- _getLatestPresence(userId) {
675
- if (!this._authorize) return {
676
- timer: null,
677
- presence: null
678
- };
679
- const sessionIds = Array.from(this._userSessionss.get(userId)?.values() ?? []);
680
- if (!sessionIds.length) return {
681
- timer: null,
682
- presence: null
683
- };
684
- return sessionIds.reduce((state, sessionId) => {
685
- const pluvWs = this._sessions.get(sessionId) ?? null;
686
- if (!pluvWs) return state;
687
- const session = pluvWs.session;
688
- const presence = session.presence;
689
- const timer = session.timers.presence;
690
- if (session.user?.id !== userId) return state;
691
- if (typeof state.timer !== "number") return {
692
- presence,
693
- timer
694
- };
695
- if (typeof timer !== "number") return state;
696
- return timer > state.timer ? {
697
- presence,
698
- timer
699
- } : state;
700
- }, {
701
- presence: null,
702
- timer: null
703
- });
704
- }
705
- _getProcedure(message) {
706
- return this._router._defs.events[message.type] ?? null;
707
- }
708
- _getProcedureInputs(message) {
709
- const procedure = this._getProcedure(message);
710
- if (!procedure) return message.data;
711
- return procedure.config.input ? procedure.config.input.parse(message.data) : message.data;
712
- }
713
- _getSession(webSocket) {
714
- const pluvWs = this._getAbstractWs(webSocket);
715
- if (!pluvWs) throw new Error("Session could not be found");
716
- return pluvWs.session;
717
- }
718
- _getSessions() {
719
- return Array.from(this._sessions.values()).map((pluvWs) => pluvWs.session);
720
- }
721
- _initialize() {
722
- const promise = (async () => {
723
- this._logDebug(`${colors.blue(`Initializing room: ${this.id}`)}`);
724
- if (!!this._uninitialize) {
725
- await (await this._uninitialize).bind(this)();
726
- this._uninitialize = null;
727
- }
728
- const pubSubId = await this._platform.pubSub.subscribe(this.id, async ({ options = {}, ...message }) => {
729
- await promise;
730
- const sender = {
731
- sessionId: message.connectionId,
732
- user: message.user
733
- };
734
- switch (options.type) {
735
- case "self":
736
- await this._sendSelfMessage(message, sender);
737
- return;
738
- case "sync":
739
- await this._sendSyncMessage(message, sender);
740
- return;
741
- default: {
742
- const sessionIds = options.sessionIds;
743
- await this._sendBroadcastMessage(message, sender, sessionIds);
744
- }
745
- }
746
- });
747
- const doc = await this._getInitialDoc();
748
- this._wasDocEmptyOnInit = doc.isEmpty();
749
- const uninitialize = async () => {
750
- this._platform.pubSub.unsubscribe(pubSubId);
751
- const [doc, context] = await Promise.all([this._doc, this._getContext()]);
752
- const encodedState = doc.getEncodedState();
753
- doc.destroy();
754
- this._doc = Promise.resolve(this._docFactory.getEmpty());
755
- await Promise.resolve(this._listeners.onRoomDestroyed({
756
- ..."_meta" in this._platform && !!this._platform._meta ? { _meta: this._platform._meta } : {},
757
- context,
758
- platform: this._platform,
759
- room: this.id
760
- }));
761
- if (this._storageInitializedViaSession) {
762
- await Promise.resolve(this._listeners.onStorageDestroyed({
763
- ..."_meta" in this._platform && !!this._platform._meta ? { _meta: this._platform._meta } : {},
764
- context,
765
- encodedState,
766
- platform: this._platform,
767
- room: this.id
768
- }));
769
- this._storageInitializedViaSession = false;
770
- }
771
- this._uninitialize = null;
772
- };
773
- return {
774
- doc,
775
- uninitialize
776
- };
777
- })();
778
- this._doc = promise.then((result) => result.doc);
779
- this._uninitialize = promise.then((result) => result.uninitialize);
780
- return {
781
- doc: this._doc,
782
- uninitialize: this._uninitialize
783
- };
784
- }
785
- _logDebug(...data) {
786
- if (this._debug) console.log(...data);
787
- }
788
- _onClose(webSocket) {
789
- return async () => {
790
- if (!await this._initialized) return;
791
- await this._closeWebSockets([webSocket]);
792
- };
793
- }
794
- _onMessage(webSocket) {
795
- return async (event) => {
796
- if (!await this._initialized) return;
797
- const pluvWs = this._getAbstractWs(webSocket);
798
- if (!pluvWs) throw new Error("Could not get session");
799
- const session = this._getSession(pluvWs);
800
- const sessions = this._getSessions();
801
- const setPresence = (params) => {
802
- this._setPresence.bind(this)(params);
803
- };
804
- const [doc, context] = await Promise.all([this._doc, this._getContext()]);
805
- const eventContext = {
806
- context,
807
- doc,
808
- garbageCollect: async () => {
809
- await this.garbageCollect();
810
- },
811
- platform: this._platform,
812
- get presence() {
813
- return session.presence;
814
- },
815
- set presence(presence) {
816
- const sessionId = this.session?.id;
817
- if (!sessionId) return;
818
- setPresence({
819
- presence,
820
- sessionId,
821
- timer: this.time
822
- });
823
- },
824
- room: this.id,
825
- session,
826
- sessions,
827
- time: (/* @__PURE__ */ new Date()).getTime()
828
- };
829
- if (pluvWs.state.quit) {
830
- await this._closeWebSockets([pluvWs]).catch(() => null);
831
- pluvWs.close(1011, "WebSocket broken.");
832
- return;
833
- }
834
- const message = this._parseMessage(event);
835
- if (!message) return;
836
- const procedure = this._getProcedure(message);
837
- this._listeners.onMessage({
838
- ..."_meta" in this._platform && !!this._platform._meta ? { _meta: this._platform._meta } : {},
839
- context,
840
- encodedState: doc.getEncodedState(),
841
- message,
842
- platform: this._platform,
843
- room: this.id,
844
- user: session.user,
845
- webSocket: session.webSocket.webSocket
846
- });
847
- const sessionId = session.id;
848
- const user = session.user;
849
- if (!procedure) {
850
- await this._broadcast({
851
- message,
852
- senderId: sessionId
853
- });
854
- return;
855
- }
856
- let inputs;
857
- try {
858
- inputs = this._getProcedureInputs(message);
859
- } catch (error) {
860
- pluvWs.handleError({
861
- error,
862
- message: "Invalid input",
863
- room: this.id,
864
- session
865
- });
866
- return;
867
- }
868
- await Promise.all([
869
- procedure.config.broadcast?.(inputs, eventContext),
870
- procedure.config.self?.(inputs, eventContext),
871
- procedure.config.sync?.(inputs, eventContext)
872
- ]).then(async ([broadcast, self, sync]) => {
873
- const handleBroadcast = async () => {
874
- if (!broadcast) return;
875
- const messages = Object.entries(broadcast).map(async ([type, data]) => {
876
- await this._broadcast({
877
- message: {
878
- data,
879
- type
880
- },
881
- senderId: sessionId
882
- });
883
- });
884
- await Promise.all(messages);
885
- };
886
- const handleSelf = async () => {
887
- if (!self) return;
888
- const messages = Object.entries(self).map(async ([type, data]) => {
889
- await this._sendSelfMessage({
890
- data,
891
- type
892
- }, {
893
- sessionId,
894
- user
895
- });
896
- });
897
- await Promise.all(messages);
898
- };
899
- const handleSync = async () => {
900
- if (!sync) return;
901
- await this._platform.pubSub.publish(this.id, {
902
- connectionId: session.id,
903
- options: { type: "sync" },
904
- room: this.id,
905
- user,
906
- ...message
907
- });
908
- const messages = Object.entries(sync).map(async ([type, data]) => {
909
- await this._sendSelfMessage({
910
- data,
911
- type
912
- }, {
913
- sessionId,
914
- user
915
- });
916
- });
917
- await Promise.all(messages);
918
- };
919
- await Promise.all([
920
- handleBroadcast(),
921
- handleSelf(),
922
- handleSync()
923
- ]);
924
- });
925
- await this._checkStorageInitializedViaSession();
926
- };
927
- }
928
- _parseMessage(message) {
929
- try {
930
- const parsed = this._platform.parseData(message.data);
931
- if (typeof parsed !== "object") return null;
932
- if (typeof parsed.type !== "string") return null;
933
- if (typeof parsed.data !== "object") return null;
934
- return parsed;
935
- } catch {
936
- return null;
937
- }
938
- }
939
- _setPresence(params) {
940
- const { presence, sessionId, timer: _timer } = params;
941
- const timer = _timer ?? (/* @__PURE__ */ new Date()).getTime();
942
- const pluvWs = this._sessions.get(sessionId) ?? null;
943
- if (!pluvWs) return;
944
- const user = pluvWs.session.user;
945
- (user ? /* @__PURE__ */ new Set([...this._userSessionss.get(user.id) ?? [], sessionId]) : /* @__PURE__ */ new Set([sessionId])).forEach((sId) => {
946
- const session = this._sessions.get(sId)?.session;
947
- if (!session) return;
948
- const prevState = session.webSocket.state;
949
- this._platform.setSerializedState(session.webSocket, {
950
- ...prevState,
951
- presence,
952
- timers: {
953
- ...prevState.timers,
954
- presence: timer
955
- }
956
- });
957
- });
958
- }
959
- _removeUserSession(userId, sessionId) {
960
- const set = this._userSessionss.get(userId);
961
- if (!set) return null;
962
- set.delete(sessionId);
963
- if (!!set.size) return set;
964
- this._userSessionss.delete(userId);
965
- return set;
966
- }
967
- async _checkStorageInitializedViaSession() {
968
- if (this._storageInitializedViaSession) return;
969
- const doc = await this._doc;
970
- if (this._wasDocEmptyOnInit && !doc.isEmpty() && this._sessions.size > 0) this._storageInitializedViaSession = true;
971
- }
972
- async _sendMessage(pluvWs, message) {
973
- if (!await this._initialized) return;
974
- await Promise.resolve(pluvWs.sendMessage(message));
975
- }
976
- async _sendBroadcastMessage(message, sender, sessionIds) {
977
- const { data, type } = message;
978
- const connectionId = sender?.sessionId ?? null;
979
- const room = this.id;
980
- if (typeof connectionId !== "string") return;
981
- const webSockets = sessionIds?.reduce((dict, id) => {
982
- const pluvWs = this._sessions.get(id);
983
- return pluvWs ? dict.set(id, pluvWs) : dict;
984
- }, /* @__PURE__ */ new Map()) ?? this._sessions;
985
- const promises = Array.from(webSockets.values()).map(async (pluvWs) => {
986
- const user = pluvWs.session.user ?? null;
987
- this._sendMessage(pluvWs, {
988
- connectionId,
989
- data,
990
- room,
991
- type,
992
- user
993
- });
994
- });
995
- await Promise.all(promises);
996
- }
997
- async _sendSelfMessage(message, sender) {
998
- const senderId = sender?.sessionId ?? null;
999
- if (typeof senderId !== "string") return;
1000
- const pluvWs = this._sessions.get(senderId);
1001
- if (!pluvWs) return;
1002
- /**
1003
- * @description Note that this will not fire for Cloudflare's hibernatable websockets
1004
- * because the server will auto-respond with the $pong event without calling any
1005
- * functions.
1006
- * @date April 18, 2025
1007
- */
1008
- if (message.type === "$pong") {
1009
- if (Date.now() - this._lastGarbageCollectMs > 6e4) await this.garbageCollect();
1010
- }
1011
- const user = pluvWs.session.user ?? null;
1012
- this._sendMessage(pluvWs, {
1013
- connectionId: senderId,
1014
- room: this.id,
1015
- user,
1016
- ...message
1017
- });
1018
- }
1019
- async _sendSyncMessage(message, sender) {
1020
- if (!sender) return;
1021
- const connectionId = sender.sessionId;
1022
- if (typeof connectionId !== "string") return;
1023
- const [doc, context] = await Promise.all([this._doc, this._getContext()]);
1024
- const resolverCtx = {
1025
- context,
1026
- doc,
1027
- garbageCollect: async () => {
1028
- await this.garbageCollect();
1029
- },
1030
- platform: this._platform,
1031
- get presence() {
1032
- return null;
1033
- },
1034
- set presence(presence) {},
1035
- room: this.id,
1036
- session: null,
1037
- sessions: this._getSessions(),
1038
- time: (/* @__PURE__ */ new Date()).getTime()
1039
- };
1040
- const resolver = this._getProcedure(message)?.config.sync;
1041
- if (!resolver) return;
1042
- let inputs;
1043
- try {
1044
- inputs = this._getProcedureInputs(message);
1045
- } catch {
1046
- return;
1047
- }
1048
- Promise.resolve(resolver(inputs, resolverCtx)).then((output) => {
1049
- if (!output) return;
1050
- Object.keys(output).forEach((type) => {
1051
- const data = output[type] ?? {};
1052
- this._platform.pubSub.publish(this.id, {
1053
- connectionId,
1054
- data,
1055
- options: { type: "self" },
1056
- room: this.id,
1057
- type,
1058
- user: sender.user ?? null
1059
- });
1060
- });
1061
- });
1062
- }
1063
- };
1064
- //#endregion
1065
- //#region src/version.generated.ts
1066
- const version = "5.0.0";
1067
- //#endregion
1068
- //#region src/PluvServer.ts
1069
- var PluvServer = class {
1070
- version = version;
1071
- _config;
1072
- _docFactory;
1073
- get fetch() {
1074
- return (...args) => {
1075
- const platform = this._config.platform();
1076
- platform.validateConfig(this._config);
1077
- if (!platform._fetch) throw new Error(`\`${platform._name}\` does not support \`fetch\``);
1078
- return platform._fetch(...args);
1079
- };
1080
- }
1081
- /**
1082
- * @ignore
1083
- * @readonly
1084
- * @deprecated Internal use only. Changes to this will never be marked as breaking.
1085
- */
1086
- get _defs() {
1087
- return {
1088
- authorize: this._config.authorize,
1089
- context: this._config.context,
1090
- crdt: this._config.crdt,
1091
- events: this._router._defs.events,
1092
- platform: this._config.platform()
1093
- };
1094
- }
1095
- get _baseRouter() {
1096
- const listeners = this._getListeners();
1097
- return new PluvRouter({
1098
- $getOthers: this._procedure.sync((data, { room, session, sessions }) => {
1099
- const currentTime = Date.now();
1100
- return { $othersReceived: { others: sessions.filter((wsSession) => {
1101
- if (wsSession.id === session?.id) return false;
1102
- if (wsSession.quit) return false;
1103
- if (currentTime - wsSession.timers.ping > 3e4) return false;
1104
- return true;
1105
- }).reduce((acc, { id, presence, timers, user }) => ({
1106
- ...acc,
1107
- [id]: {
1108
- connectionId: id,
1109
- presence,
1110
- room,
1111
- timers: { presence: timers.presence },
1112
- user
1113
- }
1114
- }), {}) } };
1115
- }),
1116
- $initializeSession: this._procedure.broadcast((data, { session }) => {
1117
- const presence = data?.presence;
1118
- const timers = session.timers;
1119
- if (!session) return {};
1120
- session.presence = presence;
1121
- return { $userJoined: {
1122
- connectionId: session.id,
1123
- user: session.user,
1124
- presence,
1125
- timers: { presence: timers.presence }
1126
- } };
1127
- }).self(async (data, { context, doc, platform, room, session }) => {
1128
- const oldState = await platform.persistence.getStorageState(room);
1129
- /**
1130
- * !HACK
1131
- * @description This is the frontend's initialStorage. We only want to
1132
- * apply this if the server's storage is empty (i.e. no initial storage
1133
- * has been applied)
1134
- */
1135
- const update = data?.update;
1136
- if (!oldState) {
1137
- const loadedState = await this._getInitialStorage({
1138
- context,
1139
- room
1140
- });
1141
- if (!!loadedState && !this._docFactory.isEmpty(loadedState)) doc.applyEncodedState({ update: loadedState });
1142
- }
1143
- /**
1144
- * @description Storage was already initialized. Don't overwrite the current
1145
- * storage state with the incoming initial storage. Return what the current state
1146
- * is without changes.
1147
- * @date May 7, 2025
1148
- */
1149
- if (!doc.isEmpty()) return { $storageReceived: {
1150
- changeKind: "unchanged",
1151
- state: doc.getEncodedState()
1152
- } };
1153
- /**
1154
- * @description Storage was never initialized, and there is an incoming
1155
- * initialStorage from the client. Apply the initialStorage and persist the state
1156
- * as an update.
1157
- * @date May 7, 2025
1158
- */
1159
- if (!!update) {
1160
- const encodedState = doc.applyEncodedState({ update }).getEncodedState();
1161
- const storageSize = new TextEncoder().encode(encodedState).length;
1162
- if (!!this._config.limits.storageMaxSize && storageSize > this._config.limits.storageMaxSize) throw new Error("Storage has exceeded the size limit");
1163
- await platform.persistence.setStorageState(room, encodedState).catch((error) => {
1164
- this._logDebug(error);
1165
- });
1166
- listeners.onStorageUpdated({
1167
- context,
1168
- encodedState,
1169
- platform,
1170
- room,
1171
- user: session?.user,
1172
- webSocket: session?.webSocket.webSocket
1173
- });
1174
- return { $storageReceived: {
1175
- changeKind: "initialized",
1176
- state: encodedState
1177
- } };
1178
- }
1179
- return { $storageReceived: {
1180
- changeKind: "empty",
1181
- state: doc.getEncodedState()
1182
- } };
1183
- }),
1184
- $ping: this._procedure.self((data, { platform, session }) => {
1185
- if (!session) return {};
1186
- const currentTime = (/* @__PURE__ */ new Date()).getTime();
1187
- const prevState = session.webSocket.state;
1188
- platform.setSerializedState(session.webSocket, {
1189
- ...prevState,
1190
- timers: {
1191
- ...prevState.timers,
1192
- ping: currentTime
1193
- }
1194
- });
1195
- return { $pong: {} };
1196
- }),
1197
- $updatePresence: this._procedure.broadcast((data, context) => {
1198
- const presence = data?.presence;
1199
- const { session } = context;
1200
- if (!session) return {};
1201
- const cleanedPatch = pickBy(presence ?? {}, (value) => typeof value !== "undefined");
1202
- const updated = Object.assign(Object.create(null), session.presence, cleanedPatch);
1203
- const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
1204
- if (!!this._config.limits.presenceMaxSize && bytes > this._config.limits.presenceMaxSize) throw new Error(oneLine`
1205
- Large presence. Presence must be at most
1206
- ${this._config.limits.presenceMaxSize.toLocaleString()} bytes.
1207
- Current size: ${bytes.toLocaleString()} bytes
1208
- `);
1209
- context.presence = updated;
1210
- return { $presenceUpdated: {
1211
- presence: updated,
1212
- timers: { presence: session.timers.presence }
1213
- } };
1214
- }),
1215
- $updateStorage: this._procedure.broadcast((data, { context, doc, platform, room }) => {
1216
- const origin = data?.origin;
1217
- const update = data?.update ?? null;
1218
- if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
1219
- const encodedState = (update === null ? doc : doc.applyEncodedState({ update })).getEncodedState();
1220
- const storageSize = new TextEncoder().encode(encodedState).length;
1221
- if (!!this._config.limits.storageMaxSize && storageSize > this._config.limits.storageMaxSize) throw new Error(oneLine`
1222
- Large Storage. Storage must be at most
1223
- ${this._config.limits.storageMaxSize.toLocaleString()} bytes.
1224
- Current size: ${storageSize.toLocaleString()} bytes
1225
- `);
1226
- platform.persistence.setStorageState(room, encodedState).then(() => {
1227
- listeners.onStorageUpdated({
1228
- context,
1229
- encodedState,
1230
- platform,
1231
- room
1232
- });
1233
- });
1234
- return { $storageUpdated: { state: encodedState } };
1235
- })
1236
- });
1237
- }
1238
- get _procedure() {
1239
- return new PluvProcedure();
1240
- }
1241
- get _router() {
1242
- return this._config.router ? PluvRouter.merge(this._baseRouter, this._config.router) : this._baseRouter;
1243
- }
1244
- constructor(options) {
1245
- this._config = {
1246
- crdt: noop,
1247
- debug: false,
1248
- router: new PluvRouter({}),
1249
- ...options
1250
- };
1251
- const { onRoomDestroyed, onRoomMessage, onStorageDestroyed, onStorageUpdated, onUserConnected, onUserDisconnected } = options;
1252
- this._docFactory = this._config.crdt.doc(() => ({}));
1253
- this._listeners = {
1254
- onRoomDestroyed: (event) => onRoomDestroyed?.(event),
1255
- onRoomMessage: (event) => onRoomMessage?.(event),
1256
- onStorageDestroyed: (event) => onStorageDestroyed?.(event),
1257
- onStorageUpdated: (event) => onStorageUpdated?.(event),
1258
- onUserConnected: (event) => onUserConnected?.(event),
1259
- onUserDisconnected: (event) => onUserDisconnected?.(event)
1260
- };
1261
- }
1262
- createRoom(room, ...options) {
1263
- const { _meta, debug, ...platformRoomContext } = options[0] ?? {};
1264
- const platform = this._config.platform();
1265
- platform.validateConfig(this._config);
1266
- if (platform._config.handleMode !== "io") throw new Error(`\`createRoom\` is unsupported for \`${platform._name}\``);
1267
- if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room)) throw new Error("Unsupported room name");
1268
- const roomContext = platformRoomContext;
1269
- const listeners = this._getListeners();
1270
- const logDebug = this._logDebug.bind(this);
1271
- const newRoom = new IORoom(room, {
1272
- ...!!_meta ? { _meta } : {},
1273
- authorize: this._config.authorize ?? void 0,
1274
- context: this._config.context,
1275
- crdt: this._config.crdt,
1276
- debug: debug ?? this._config.debug,
1277
- getInitialStorage: this._getInitialStorage,
1278
- async onRoomDestroyed(event) {
1279
- logDebug(`${colors.blue("Deleting empty room:")} ${room}`);
1280
- await Promise.resolve(listeners.onRoomDestroyed(event));
1281
- logDebug(`${colors.blue("Deleted room:")} ${room}`);
1282
- },
1283
- async onStorageDestroyed(event) {
1284
- logDebug(`${colors.blue("Destroying storage for room:")} ${room}`);
1285
- await Promise.resolve(listeners.onStorageDestroyed(event));
1286
- await event.platform.persistence.deleteStorageState(room);
1287
- logDebug(`${colors.blue("Destroyed storage for room:")} ${room}`);
1288
- },
1289
- async onMessage(event) {
1290
- await Promise.resolve(listeners.onRoomMessage(event));
1291
- },
1292
- async onUserConnected(event) {
1293
- await Promise.resolve(listeners.onUserConnected(event));
1294
- },
1295
- async onUserDisconnected(event) {
1296
- await Promise.resolve(listeners.onUserDisconnected(event));
1297
- },
1298
- platform,
1299
- roomContext,
1300
- router: this._router
1301
- });
1302
- this._logDebug(`${colors.blue("Created room:")} ${room}`);
1303
- return newRoom;
1304
- }
1305
- async createToken(params) {
1306
- return await this._config.io.createToken(params);
1307
- }
1308
- _getInitialStorage = (...args) => {
1309
- return (typeof this._config.getInitialStorage !== "string" ? this._config.getInitialStorage ?? (() => null) : (() => null))(...args);
1310
- };
1311
- _getListeners() {
1312
- return this._listeners;
1313
- }
1314
- _logDebug(...data) {
1315
- if (this._config.debug) console.log(...data);
1316
- }
1317
- };
1318
- //#endregion
1319
- //#region src/PluvIO.ts
1320
- var PluvIO = class {
1321
- version = version;
1322
- _authorize = null;
1323
- _context = {};
1324
- _crdt;
1325
- _debug;
1326
- _limits;
1327
- _platform;
1328
- get procedure() {
1329
- return new PluvProcedure();
1330
- }
1331
- constructor(options) {
1332
- const { authorize, context, crdt = noop, debug = false, limits, platform } = options;
1333
- this._crdt = crdt;
1334
- this._debug = debug;
1335
- this._limits = {
1336
- presenceMaxSize: 512,
1337
- storageMaxSize: MAX_STORAGE_SIZE_BYTES,
1338
- userIdMaxLength: 128,
1339
- userMaxSize: 512,
1340
- ...limits
1341
- };
1342
- this._platform = platform;
1343
- if (authorize) this._authorize = authorize;
1344
- if (context) this._context = context;
1345
- }
1346
- async createToken(params) {
1347
- if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
1348
- const ioAuthorize = this._getIOAuthorize(params);
1349
- const user = params.user;
1350
- const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
1351
- if (!!this._limits.userIdMaxLength && user.id.length > this._limits.userIdMaxLength) throw new Error(oneLine`
1352
- createToken was called with a long user id. User ID must be at
1353
- most 128 characters. Current length: ${user.id.length.toLocaleString()}
1354
- `);
1355
- const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
1356
- if (!!this._limits.userMaxSize && bytes > this._limits.userMaxSize) throw new Error(oneLine`
1357
- createToken called with large payload. User must be at most 512
1358
- bytes. Current size: ${bytes.toLocaleString()}
1359
- `);
1360
- const platform = this._platform();
1361
- /**
1362
- * !HACK
1363
- * @description Allow the platform to overwrite this behavior as needed. This is introduced
1364
- * to support platformPluv
1365
- * @date December 15, 2024
1366
- */
1367
- if (platform._createToken) return await platform._createToken({
1368
- ...params,
1369
- authorize: ioAuthorize
1370
- });
1371
- const secret = ioAuthorize?.secret ?? null;
1372
- if (!secret) throw new Error("`authorize` was specified without a valid secret");
1373
- return await authorize({
1374
- platform,
1375
- secret
1376
- }).encode(params);
1377
- }
1378
- mergeRouters(...routers) {
1379
- return PluvRouter.merge(...routers);
1380
- }
1381
- router(events) {
1382
- const invalidName = Object.keys(events).find((name) => name.includes("$"));
1383
- if (typeof invalidName === "string") throw new Error(oneLine`
1384
- Invalid event name. Event names must not contain $: "${invalidName}"
1385
- `);
1386
- return new PluvRouter(events);
1387
- }
1388
- server(...config) {
1389
- return new PluvServer({
1390
- ...config[0] ?? {},
1391
- authorize: this._authorize ?? void 0,
1392
- context: this._context,
1393
- crdt: this._crdt,
1394
- debug: this._debug,
1395
- io: this,
1396
- limits: this._limits,
1397
- platform: this._platform
1398
- });
1399
- }
1400
- _getIOAuthorize(options) {
1401
- if (typeof this._authorize === "function") return this._authorize(options);
1402
- return this._authorize;
1403
- }
1404
- };
1405
- //#endregion
1406
- //#region src/createIO.ts
1407
- const createIO = (params) => {
1408
- const { authorize, context, crdt, debug, limits, platform } = params;
1409
- return new PluvIO({
1410
- authorize,
1411
- context,
1412
- crdt,
1413
- debug,
1414
- limits,
1415
- platform
1416
- });
1417
- };
1418
- //#endregion
1419
- export { AbstractPersistence, AbstractPlatform, AbstractPubSub, AbstractWebSocket, PluvIO, PluvProcedure, PluvRouter, PluvServer, authorize, createIO };
1420
-
1421
- //# sourceMappingURL=index.mjs.map