@pluv/io 5.0.1 → 5.0.2

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