@pluv/io 0.40.2 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -244,28 +244,12 @@ var AbstractWebSocket = class {
244
244
  this.CLOSING = 2;
245
245
  /** The connection is closed. */
246
246
  this.CLOSED = 3;
247
- this._user = null;
248
247
  const { persistence, platform, room } = config;
249
248
  this.persistence = persistence;
250
249
  this._platform = platform;
251
250
  this.room = room;
252
251
  this.webSocket = webSocket;
253
252
  }
254
- getSession() {
255
- return __async(this, null, function* () {
256
- var _a;
257
- const sessionId = this.sessionId;
258
- const state = this.state;
259
- const room = state.room;
260
- const user = (_a = this._user) != null ? _a : yield this.persistence.getUser(room, sessionId);
261
- this._user = user;
262
- return __spreadProps(__spreadValues({}, state), {
263
- id: sessionId,
264
- user,
265
- webSocket: this
266
- });
267
- });
268
- }
269
253
  handleError(params) {
270
254
  var _a, _b, _c;
271
255
  const { error, message: _message, room, session } = params;
@@ -403,14 +387,19 @@ var import_crdt = require("@pluv/crdt");
403
387
  var import_kleur = __toESM(require("kleur"));
404
388
 
405
389
  // src/constants.ts
390
+ var GARBAGE_COLLECT_INTERVAL_MS = 6e4;
391
+ var MAX_PRESENCE_SIZE_BYTES = 512;
392
+ var MAX_USER_SIZE_BYTES = 512;
406
393
  var PING_TIMEOUT_MS = 3e4;
407
394
 
408
395
  // src/IORoom.ts
409
396
  var IORoom = class {
410
397
  constructor(id, config) {
398
+ this._lastGarbageCollectMs = -1 * (GARBAGE_COLLECT_INTERVAL_MS + 1);
411
399
  this._uninitialize = null;
412
400
  this._authorize = null;
413
401
  this._sessions = /* @__PURE__ */ new Map();
402
+ this._userSessionss = /* @__PURE__ */ new Map();
414
403
  const {
415
404
  _meta,
416
405
  authorize: authorize2,
@@ -440,12 +429,15 @@ var IORoom = class {
440
429
  if (authorize2) this._authorize = authorize2;
441
430
  const webSockets = this._platform.getWebSockets();
442
431
  webSockets.forEach((webSocket) => {
432
+ var _a;
443
433
  const deserialized = this._platform.getSerializedState(webSocket);
444
434
  const sessionId = this._platform.getSessionId(webSocket);
445
435
  if (!deserialized) return;
446
436
  if (typeof sessionId !== "string") return;
447
437
  const pluvWs = this._platform.convertWebSocket(webSocket, { room: this.id });
448
438
  this._sessions.set(sessionId, pluvWs);
439
+ const userId = (_a = pluvWs.session.user) == null ? void 0 : _a.id;
440
+ if (!!userId) this._addUserSession(userId, sessionId);
449
441
  });
450
442
  const { doc, uninitialize } = this._initialize();
451
443
  this._doc = doc;
@@ -503,6 +495,16 @@ var IORoom = class {
503
495
  yield this._emitQuitters();
504
496
  });
505
497
  }
498
+ /**
499
+ * @description The IORoom will garbage collect occasionally as connections ping/pong the
500
+ * server
501
+ */
502
+ garbageCollect() {
503
+ return __async(this, null, function* () {
504
+ this._lastGarbageCollectMs = Date.now();
505
+ this._emitQuitters();
506
+ });
507
+ }
506
508
  getSize() {
507
509
  const currentTime = (/* @__PURE__ */ new Date()).getTime();
508
510
  return Array.from(this._sessions.values()).reduce((count, pluvWs) => {
@@ -551,19 +553,25 @@ var IORoom = class {
551
553
  const user = yield this._getAuthorizedUser(token, _options);
552
554
  const ioAuthorize = this._getIOAuthorize(_options);
553
555
  const isUnauthorized = !!ioAuthorize && !user;
554
- const isTokenInUse = !!user && (yield this._getSessions().then((sessions) => sessions.some((session) => {
555
- var _a2;
556
- return ((_a2 = session.user) == null ? void 0 : _a2.id) === user.id;
557
- })));
558
556
  const pluvWs = this._platform.convertWebSocket(webSocket, { room: this.id });
559
- this._logDebug(`${import_kleur.default.blue(`Registering connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
560
- yield this._platform.acceptWebSocket(pluvWs);
561
- if (isUnauthorized || isTokenInUse) {
562
- this._logDebug(`${import_kleur.default.blue("Authorization failed for connection:")} ${pluvWs.sessionId}`);
557
+ if (isUnauthorized) {
558
+ this._logDebug(`${import_kleur.default.blue("Authorization failed for connection")}`);
563
559
  pluvWs.handleError({ error: new Error("Not authorized"), room: this.id });
564
560
  pluvWs.close(3e3, "WebSocket unauthorized.");
565
561
  return;
566
562
  }
563
+ if (!!user) {
564
+ const latest = this._getLatestPresence(user.id);
565
+ const prevState = pluvWs.state;
566
+ pluvWs.user = user;
567
+ this._platform.setSerializedState(pluvWs, __spreadProps(__spreadValues({}, prevState), {
568
+ presence: latest.presence,
569
+ timers: __spreadProps(__spreadValues({}, prevState.timers), { presence: latest.timer })
570
+ }));
571
+ this._addUserSession(user.id, pluvWs.sessionId);
572
+ }
573
+ this._logDebug(`${import_kleur.default.blue(`Registering connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
574
+ yield this._platform.acceptWebSocket(pluvWs);
567
575
  this._sessions.set(pluvWs.sessionId, pluvWs);
568
576
  yield this._platform.persistence.addUser(this.id, pluvWs.sessionId, user != null ? user : {});
569
577
  if (this._platform._config.registrationMode === "attached") {
@@ -574,17 +582,24 @@ var IORoom = class {
574
582
  pluvWs.addEventListener("message", onMessage);
575
583
  }
576
584
  yield this._emitRegistered(pluvWs);
577
- this._logDebug(`${import_kleur.default.blue(`Registered connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
578
585
  const size = this.getSize();
586
+ this._logDebug(`${import_kleur.default.blue(`Registered connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
579
587
  this._logDebug(`${import_kleur.default.blue(`Room ${this.id} size:`)} ${size}`);
580
588
  });
581
589
  }
590
+ _addUserSession(userId, sessionId) {
591
+ var _a;
592
+ const set = (_a = this._userSessionss.get(userId)) != null ? _a : /* @__PURE__ */ new Set();
593
+ const updated = set.add(sessionId);
594
+ this._userSessionss.set(userId, updated);
595
+ return updated;
596
+ }
582
597
  _broadcast(params) {
583
598
  return __async(this, null, function* () {
584
599
  var _a, _b, _c;
585
600
  const { message, senderId } = params;
586
601
  const sender = senderId ? (_a = this._sessions.get(senderId)) != null ? _a : null : null;
587
- const session = (_b = yield sender == null ? void 0 : sender.getSession()) != null ? _b : null;
602
+ const session = (_b = sender == null ? void 0 : sender.session) != null ? _b : null;
588
603
  const user = (_c = session == null ? void 0 : session.user) != null ? _c : null;
589
604
  if (typeof senderId !== "string") return;
590
605
  this._platform.pubSub.publish(this.id, __spreadValues({
@@ -606,15 +621,18 @@ var IORoom = class {
606
621
  message: { type: "$exit", data: { sessionId } },
607
622
  senderId: sessionId
608
623
  });
624
+ const session = webSocket.session;
625
+ const user = session.user;
626
+ if (!!user) this._removeUserSession(user.id, sessionId);
609
627
  try {
610
- const [session, doc] = yield Promise.all([webSocket.getSession(), this._doc]);
628
+ const doc = yield this._doc;
611
629
  const encodedState = doc.getEncodedState();
612
630
  yield Promise.resolve(
613
631
  this._listeners.onUserDisconnected({
614
632
  context: this._context,
615
633
  encodedState,
616
634
  room: this.id,
617
- user: session.user
635
+ user
618
636
  })
619
637
  );
620
638
  } catch (error) {
@@ -645,10 +663,21 @@ var IORoom = class {
645
663
  }
646
664
  _emitRegistered(pluvWs) {
647
665
  return __async(this, null, function* () {
648
- const session = yield pluvWs.getSession();
666
+ const session = pluvWs.session;
649
667
  const sessionId = session.id;
668
+ const presence = session.presence;
650
669
  const user = session.user;
651
- yield this._sendSelfMessage({ type: "$registered", data: { sessionId } }, { sessionId, user });
670
+ yield this._sendSelfMessage(
671
+ {
672
+ type: "$registered",
673
+ data: {
674
+ presence,
675
+ sessionId,
676
+ timers: { presence: session.timers.presence }
677
+ }
678
+ },
679
+ { sessionId, user }
680
+ );
652
681
  try {
653
682
  const doc = yield this._doc;
654
683
  const encodedState = doc.getEncodedState();
@@ -723,6 +752,27 @@ var IORoom = class {
723
752
  if (typeof this._authorize === "function") return this._authorize(options);
724
753
  return this._authorize;
725
754
  }
755
+ _getLatestPresence(userId) {
756
+ var _a, _b;
757
+ if (!this._authorize) return { timer: null, presence: null };
758
+ const sessionIds = Array.from((_b = (_a = this._userSessionss.get(userId)) == null ? void 0 : _a.values()) != null ? _b : []);
759
+ if (!sessionIds.length) return { timer: null, presence: null };
760
+ return sessionIds.reduce(
761
+ (state, sessionId) => {
762
+ var _a2, _b2;
763
+ const pluvWs = (_a2 = this._sessions.get(sessionId)) != null ? _a2 : null;
764
+ if (!pluvWs) return state;
765
+ const session = pluvWs.session;
766
+ const presence = session.presence;
767
+ const timer = session.timers.presence;
768
+ if (((_b2 = session.user) == null ? void 0 : _b2.id) !== userId) return state;
769
+ if (typeof state.timer !== "number") return { presence, timer };
770
+ if (typeof timer !== "number") return state;
771
+ return timer > state.timer ? { presence, timer } : state;
772
+ },
773
+ { presence: null, timer: null }
774
+ );
775
+ }
726
776
  _getProcedure(message) {
727
777
  var _a;
728
778
  return (_a = this._router._defs.events[message.type]) != null ? _a : null;
@@ -733,19 +783,13 @@ var IORoom = class {
733
783
  return procedure.config.input ? procedure.config.input.parse(message.data) : message.data;
734
784
  }
735
785
  _getSession(webSocket) {
736
- return __async(this, null, function* () {
737
- const pluvWs = this._getAbstractWs(webSocket);
738
- if (!pluvWs) throw new Error("Session could not be found");
739
- return yield pluvWs.getSession();
740
- });
786
+ const pluvWs = this._getAbstractWs(webSocket);
787
+ if (!pluvWs) throw new Error("Session could not be found");
788
+ return pluvWs.session;
741
789
  }
742
790
  _getSessions() {
743
- return __async(this, null, function* () {
744
- const promises = Array.from(this._sessions.values()).map((pluvWs) => __async(this, null, function* () {
745
- return yield pluvWs.getSession();
746
- }));
747
- return yield Promise.all(promises);
748
- });
791
+ const sessions = Array.from(this._sessions.values()).map((pluvWs) => pluvWs.session);
792
+ return sessions;
749
793
  }
750
794
  _initialize() {
751
795
  const promise = (() => __async(this, null, function* () {
@@ -818,17 +862,31 @@ var IORoom = class {
818
862
  if (!(yield this._initialized)) return;
819
863
  const pluvWs = this._getAbstractWs(webSocket);
820
864
  if (!pluvWs) throw new Error("Could not get session");
821
- const [session, sessions] = yield Promise.all([
822
- this._getSession(pluvWs),
823
- this._getSessions()
824
- ]);
865
+ const session = this._getSession(pluvWs);
866
+ const sessions = this._getSessions();
867
+ const setPresence = (params) => {
868
+ this._setPresence.bind(this)(params);
869
+ };
825
870
  const doc = yield this._doc;
826
871
  const eventContext = {
827
872
  context: this._context,
828
873
  doc,
874
+ garbageCollect: () => __async(this, null, function* () {
875
+ yield this.garbageCollect();
876
+ }),
877
+ get presence() {
878
+ return session.presence;
879
+ },
880
+ set presence(presence) {
881
+ var _a2;
882
+ const sessionId2 = (_a2 = this.session) == null ? void 0 : _a2.id;
883
+ if (!sessionId2) return;
884
+ setPresence({ presence, sessionId: sessionId2, timer: this.time });
885
+ },
829
886
  room: this.id,
830
887
  session,
831
- sessions
888
+ sessions,
889
+ time: (/* @__PURE__ */ new Date()).getTime()
832
890
  };
833
891
  if (pluvWs.state.quit) {
834
892
  yield this._closeWebSockets([pluvWs]).catch(() => null);
@@ -874,22 +932,20 @@ var IORoom = class {
874
932
  ]).then((_0) => __async(this, [_0], function* ([broadcast, self, sync]) {
875
933
  const handleBroadcast = () => __async(this, null, function* () {
876
934
  if (!broadcast) return;
877
- yield Promise.all(
878
- Object.entries(broadcast).map((_02) => __async(this, [_02], function* ([type, data]) {
879
- yield this._broadcast({
880
- message: { data, type },
881
- senderId: sessionId
882
- });
883
- }))
884
- );
935
+ const messages = Object.entries(broadcast).map((_02) => __async(this, [_02], function* ([type, data]) {
936
+ yield this._broadcast({
937
+ message: { data, type },
938
+ senderId: sessionId
939
+ });
940
+ }));
941
+ yield Promise.all(messages);
885
942
  });
886
943
  const handleSelf = () => __async(this, null, function* () {
887
944
  if (!self) return;
888
- yield Promise.all(
889
- Object.entries(self).map((_02) => __async(this, [_02], function* ([type, data]) {
890
- yield this._sendSelfMessage({ data, type }, { sessionId, user });
891
- }))
892
- );
945
+ const messages = Object.entries(self).map((_02) => __async(this, [_02], function* ([type, data]) {
946
+ yield this._sendSelfMessage({ data, type }, { sessionId, user });
947
+ }));
948
+ yield Promise.all(messages);
893
949
  });
894
950
  const handleSync = () => __async(this, null, function* () {
895
951
  if (!sync) return;
@@ -899,11 +955,10 @@ var IORoom = class {
899
955
  room: this.id,
900
956
  user
901
957
  }, message));
902
- yield Promise.all(
903
- Object.entries(sync).map((_02) => __async(this, [_02], function* ([type, data]) {
904
- yield this._sendSelfMessage({ data, type }, { sessionId, user });
905
- }))
906
- );
958
+ const messages = Object.entries(sync).map((_02) => __async(this, [_02], function* ([type, data]) {
959
+ yield this._sendSelfMessage({ data, type }, { sessionId, user });
960
+ }));
961
+ yield Promise.all(messages);
907
962
  });
908
963
  yield Promise.all([handleBroadcast(), handleSelf(), handleSync()]);
909
964
  }));
@@ -920,6 +975,36 @@ var IORoom = class {
920
975
  return null;
921
976
  }
922
977
  }
978
+ _setPresence(params) {
979
+ var _a, _b;
980
+ const { presence, sessionId, timer: _timer } = params;
981
+ const timer = _timer != null ? _timer : (/* @__PURE__ */ new Date()).getTime();
982
+ const pluvWs = (_a = this._sessions.get(sessionId)) != null ? _a : null;
983
+ if (!pluvWs) return;
984
+ const wsSession = pluvWs.session;
985
+ const user = wsSession.user;
986
+ const sessionIds = user ? /* @__PURE__ */ new Set([...(_b = this._userSessionss.get(user.id)) != null ? _b : [], sessionId]) : /* @__PURE__ */ new Set([sessionId]);
987
+ sessionIds.forEach((sId) => {
988
+ const pWs = this._sessions.get(sId);
989
+ const session = pWs == null ? void 0 : pWs.session;
990
+ if (!session) return;
991
+ const prevState = session.webSocket.state;
992
+ this._platform.setSerializedState(session.webSocket, __spreadProps(__spreadValues({}, prevState), {
993
+ presence,
994
+ timers: __spreadProps(__spreadValues({}, prevState.timers), {
995
+ presence: timer
996
+ })
997
+ }));
998
+ });
999
+ }
1000
+ _removeUserSession(userId, sessionId) {
1001
+ const set = this._userSessionss.get(userId);
1002
+ if (!set) return null;
1003
+ set.delete(sessionId);
1004
+ if (!!set.size) return set;
1005
+ this._userSessionss.delete(userId);
1006
+ return set;
1007
+ }
923
1008
  _sendMessage(pluvWs, message) {
924
1009
  return __async(this, null, function* () {
925
1010
  if (!(yield this._initialized)) return;
@@ -939,7 +1024,7 @@ var IORoom = class {
939
1024
  }, /* @__PURE__ */ new Map())) != null ? _b : this._sessions;
940
1025
  const promises = Array.from(webSockets.values()).map((pluvWs) => __async(this, null, function* () {
941
1026
  var _a2;
942
- const session = yield pluvWs.getSession();
1027
+ const session = pluvWs.session;
943
1028
  const user = (_a2 = session.user) != null ? _a2 : null;
944
1029
  this._sendMessage(pluvWs, { connectionId, data, room, type, user });
945
1030
  }));
@@ -953,8 +1038,13 @@ var IORoom = class {
953
1038
  if (typeof senderId !== "string") return;
954
1039
  const pluvWs = this._sessions.get(senderId);
955
1040
  if (!pluvWs) return;
956
- if (message.type === "$pong") yield this._emitQuitters();
957
- const session = yield pluvWs.getSession();
1041
+ if (message.type === "$pong") {
1042
+ const elapsedMs = Date.now() - this._lastGarbageCollectMs;
1043
+ if (elapsedMs > GARBAGE_COLLECT_INTERVAL_MS) {
1044
+ yield this.garbageCollect();
1045
+ }
1046
+ }
1047
+ const session = pluvWs.session;
958
1048
  const user = (_b = session.user) != null ? _b : null;
959
1049
  this._sendMessage(pluvWs, __spreadValues({
960
1050
  connectionId: senderId,
@@ -973,9 +1063,19 @@ var IORoom = class {
973
1063
  const context = {
974
1064
  context: this._context,
975
1065
  doc,
1066
+ garbageCollect: () => __async(this, null, function* () {
1067
+ yield this.garbageCollect();
1068
+ }),
1069
+ get presence() {
1070
+ return null;
1071
+ },
1072
+ set presence(presence) {
1073
+ return;
1074
+ },
976
1075
  room: this.id,
977
1076
  session: null,
978
- sessions: yield this._getSessions()
1077
+ sessions: this._getSessions(),
1078
+ time: (/* @__PURE__ */ new Date()).getTime()
979
1079
  };
980
1080
  const resolver = (_a = this._getProcedure(message)) == null ? void 0 : _a.config.sync;
981
1081
  if (!resolver) return;
@@ -1005,7 +1105,7 @@ var IORoom = class {
1005
1105
  };
1006
1106
 
1007
1107
  // src/version.generated.ts
1008
- var version = "0.40.2";
1108
+ var version = "0.41.0";
1009
1109
 
1010
1110
  // src/PluvServer.ts
1011
1111
  var PluvServer = class {
@@ -1021,11 +1121,12 @@ var PluvServer = class {
1021
1121
  if (currentTime - wsSession.timers.ping > PING_TIMEOUT_MS) return false;
1022
1122
  return true;
1023
1123
  }).reduce(
1024
- (acc, { id, presence, user }) => __spreadProps(__spreadValues({}, acc), {
1124
+ (acc, { id, presence, timers, user }) => __spreadProps(__spreadValues({}, acc), {
1025
1125
  [id]: {
1026
1126
  connectionId: id,
1027
1127
  presence,
1028
1128
  room,
1129
+ timers: { presence: timers.presence },
1029
1130
  user
1030
1131
  }
1031
1132
  }),
@@ -1035,13 +1136,15 @@ var PluvServer = class {
1035
1136
  }),
1036
1137
  $initializeSession: this._procedure.broadcast((data, { session }) => {
1037
1138
  const presence = data == null ? void 0 : data.presence;
1139
+ const timers = session.timers;
1038
1140
  if (!session) return {};
1039
1141
  session.presence = presence;
1040
1142
  return {
1041
1143
  $userJoined: {
1042
1144
  connectionId: session.id,
1043
1145
  user: session.user,
1044
- presence
1146
+ presence,
1147
+ timers: { presence: timers.presence }
1045
1148
  }
1046
1149
  };
1047
1150
  }).self((_0, _1) => __async(this, [_0, _1], function* (data, { context, doc, room, session }) {
@@ -1077,12 +1180,24 @@ var PluvServer = class {
1077
1180
  }));
1078
1181
  return { $pong: {} };
1079
1182
  }),
1080
- $updatePresence: this._procedure.broadcast((data, { session }) => {
1183
+ $updatePresence: this._procedure.broadcast((data, context) => {
1081
1184
  const presence = data == null ? void 0 : data.presence;
1185
+ const { session } = context;
1082
1186
  if (!session) return {};
1083
1187
  const updated = Object.assign(/* @__PURE__ */ Object.create(null), session.presence, presence);
1084
- session.webSocket.presence = updated;
1085
- return { $presenceUpdated: { presence: updated } };
1188
+ const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
1189
+ if (bytes > MAX_PRESENCE_SIZE_BYTES) {
1190
+ throw new Error(
1191
+ `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1192
+ );
1193
+ }
1194
+ context.presence = updated;
1195
+ return {
1196
+ $presenceUpdated: {
1197
+ presence: updated,
1198
+ timers: { presence: session.timers.presence }
1199
+ }
1200
+ };
1086
1201
  }),
1087
1202
  $updateStorage: this._procedure.broadcast((data, { context, doc, room }) => {
1088
1203
  var _a;
@@ -1195,8 +1310,16 @@ var PluvServer = class {
1195
1310
  if (!this._authorize) {
1196
1311
  throw new Error("IO does not specify authorize during initialization.");
1197
1312
  }
1198
- if (this._platform._createToken) return yield this._platform._createToken(params);
1199
1313
  const ioAuthorize = this._getIOAuthorize(params);
1314
+ const user = params.user;
1315
+ const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
1316
+ const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
1317
+ if (bytes > MAX_USER_SIZE_BYTES) {
1318
+ throw new Error(
1319
+ `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1320
+ );
1321
+ }
1322
+ if (this._platform._createToken) return yield this._platform._createToken(params);
1200
1323
  const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1201
1324
  if (!secret) throw new Error("`authorize` was specified without a valid secret");
1202
1325
  return yield authorize({ platform: this._platform, secret }).encode(params);