@pluv/io 5.0.1 → 5.0.3

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