@pluv/client 0.40.1 → 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/src/PluvRoom.ts CHANGED
@@ -12,6 +12,7 @@ import type {
12
12
  InferIOOutput,
13
13
  InputZodLike,
14
14
  JsonObject,
15
+ OptionalProps,
15
16
  } from "@pluv/types";
16
17
  import { AbstractRoom } from "./AbstractRoom";
17
18
  import type { AbstractStorageStore } from "./AbstractStorageStore";
@@ -57,6 +58,7 @@ const ORIGIN_INITIALIZED = "$initialized";
57
58
  const ORIGIN_STORAGE_UPDATED = "$storageUpdated";
58
59
 
59
60
  declare global {
61
+ // eslint-disable-next-line no-var
60
62
  var process: {
61
63
  env: {
62
64
  [key: string]: string | undefined;
@@ -199,7 +201,7 @@ export class PluvRoom<
199
201
  heartbeat: null,
200
202
  };
201
203
  private readonly _listeners: InternalListeners;
202
- private readonly _otherNotifier = new OtherNotifier<TIO>();
204
+ private readonly _otherNotifier = new OtherNotifier<TIO, TPresence>();
203
205
  private readonly _publicKey: PublicKey<TMetadata> | null = null;
204
206
  private readonly _reconnectTimeoutMs: ReconnectTimeoutMs;
205
207
  private readonly _router: PluvRouter<TIO, TPresence, TStorage, TEvents>;
@@ -387,8 +389,8 @@ export class PluvRoom<
387
389
 
388
390
  authToken = await this._getAuthorization(this.id, params);
389
391
 
390
- authToken && url.searchParams.set("token", encodeURIComponent(authToken));
391
- publicKey && url.searchParams.set("public_key", encodeURIComponent(publicKey));
392
+ if (authToken) url.searchParams.set("token", encodeURIComponent(authToken));
393
+ if (publicKey) url.searchParams.set("public_key", encodeURIComponent(publicKey));
392
394
 
393
395
  webSocket = new WebSocket(url.toString());
394
396
  } catch (err) {
@@ -510,8 +512,15 @@ export class PluvRoom<
510
512
  return this._crdtManager.doc.toJson(type);
511
513
  }
512
514
 
513
- public other = (connectionId: string, callback: OtherNotifierSubscriptionCallback<TIO>): (() => void) => {
514
- return this._otherNotifier.subscribe(connectionId, callback);
515
+ public other = (
516
+ connectionId: string,
517
+ callback: OtherNotifierSubscriptionCallback<TIO, TPresence>,
518
+ ): (() => void) => {
519
+ const clientId = this._usersManager.getClientId(connectionId);
520
+
521
+ if (!clientId) return () => undefined;
522
+
523
+ return this._otherNotifier.subscribe(clientId, callback);
515
524
  };
516
525
 
517
526
  public redo = (): void => {
@@ -572,7 +581,7 @@ export class PluvRoom<
572
581
  const myself = this._usersManager.myself ?? null;
573
582
 
574
583
  this._stateNotifier.subjects["my-presence"].next(myPresence);
575
- !!myself && this._stateNotifier.subjects["myself"].next(myself);
584
+ if (!!myself) this._stateNotifier.subjects.myself.next(myself);
576
585
 
577
586
  this.broadcast(
578
587
  "$updatePresence" as keyof InferIOInput<MergeEvents<TEvents, TIO>>,
@@ -652,7 +661,7 @@ export class PluvRoom<
652
661
  this._clearTimeout(this._timeouts.pong);
653
662
  this._detachWindowListeners();
654
663
  this._usersManager.removeMyself();
655
- this._usersManager.removeUsers();
664
+ this._usersManager.clearConnections();
656
665
  this._otherNotifier.clear();
657
666
  this._stateNotifier.subjects.myself.next(null);
658
667
  this._stateNotifier.subjects.others.next([]);
@@ -799,13 +808,19 @@ export class PluvRoom<
799
808
  // Should not reach here
800
809
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
801
810
 
802
- this._usersManager.removeUser(connectionId);
803
-
811
+ const clientId = this._usersManager.getClientId(connectionId);
812
+ const deleted = this._usersManager.deleteConnection(connectionId);
804
813
  const others = this._usersManager.getOthers();
805
814
 
806
815
  this._stateNotifier.subjects.others.next(others);
807
- this._otherNotifier.subject(connectionId).next(null);
808
- this._otherNotifier.subjects.delete(connectionId);
816
+
817
+ /**
818
+ * @description A single user can have multiple connections. So we're checking that there
819
+ * isn't a remaining connection before choosing to delete that user for other connected
820
+ * participants.
821
+ * @date April 16, 2025
822
+ */
823
+ if (!deleted?.remaining && !!clientId) this._otherNotifier.delete(clientId);
809
824
  }
810
825
 
811
826
  private _handlePresenceUpdatedMessage(message: IOEventMessage<TIO>): void {
@@ -816,7 +831,6 @@ export class PluvRoom<
816
831
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
817
832
 
818
833
  const data = message.data as BaseIOEventRecord<InferIOAuthorize<TIO>>["$presenceUpdated"];
819
-
820
834
  const myself = this._usersManager.myself ?? null;
821
835
 
822
836
  /**
@@ -824,17 +838,30 @@ export class PluvRoom<
824
838
  * @description We're going to have the user's own presence be patched only via local calls
825
839
  * to this.updateMyPresence. So we'll not update the user's presence in this handler to
826
840
  * avoid weird update delays to the user's own presence.
841
+ *
842
+ * However, we will patch the user's own presence if the user has updated their own
843
+ * presence via another connection that is not this one (e.g. if the user has opened
844
+ * another connection in another browser tab/window somewhere).
827
845
  * @date April 2, 2025
828
846
  */
829
847
  if (myself?.connectionId === connectionId) return;
830
848
 
831
- this._usersManager.patchPresence(connectionId, data.presence as TPresence);
849
+ const updated = this._usersManager.patchPresence(connectionId, data.presence as TPresence);
850
+ const myClientId = !!myself ? this._usersManager.getClientId(myself) : null;
851
+ const clientId = this._usersManager.getClientId(connectionId);
852
+
853
+ if (!!clientId && myClientId === clientId) {
854
+ this._stateNotifier.subjects["my-presence"].next(updated);
855
+ this._stateNotifier.subjects.myself.next(this._usersManager.myself);
856
+
857
+ return;
858
+ }
832
859
 
833
860
  const other = this._usersManager.getOther(connectionId);
834
861
  const others = this._usersManager.getOthers();
835
862
 
836
- this._otherNotifier.subject(connectionId).next(other);
837
- this._stateNotifier.subjects["others"].next(others);
863
+ if (!!clientId) this._otherNotifier.subject(clientId).next(other);
864
+ this._stateNotifier.subjects.others.next(others);
838
865
  }
839
866
 
840
867
  private _handleReceiveOthers(message: IOEventMessage<TIO>): void {
@@ -847,27 +874,36 @@ export class PluvRoom<
847
874
  Object.keys(data.others).forEach((connectionId) => {
848
875
  const { presence, user } = data.others[connectionId];
849
876
 
850
- this._usersManager.setUser(connectionId, user);
877
+ const result = this._usersManager.addConnection({
878
+ connectionId,
879
+ presence: (presence ?? undefined) as TPresence,
880
+ user,
881
+ });
851
882
 
852
- const other = this._usersManager.getOther(connectionId);
883
+ if (!!presence) this._usersManager.patchPresence(connectionId, presence as TPresence);
884
+ if (result.isMyself) return;
853
885
 
854
- !!presence && this._usersManager.patchPresence(connectionId, presence as TPresence);
886
+ const clientId = result.clientId;
887
+ const other = this._usersManager.getOther(connectionId);
855
888
 
856
- this._otherNotifier.subject(connectionId).next(other);
889
+ this._otherNotifier.subject(clientId).next(other);
857
890
  });
858
891
 
859
892
  const others = this._usersManager.getOthers();
860
893
 
861
- this._stateNotifier.subjects["others"].next(others);
894
+ this._stateNotifier.subjects.others.next(others);
862
895
  }
863
896
 
864
897
  private _handleRegisteredMessage(message: IOEventMessage<TIO>): void {
865
- const { connectionId, user } = message;
898
+ const { connectionId } = message;
899
+ const user = message.user as Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>>;
866
900
 
867
901
  if (!connectionId) return;
868
902
  // Should not reach here
869
903
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
870
904
 
905
+ const data = message.data as BaseIOEventRecord<InferIOAuthorize<TIO>>["$registered"];
906
+
871
907
  this._updateState((oldState) => {
872
908
  oldState.connection.count += 1;
873
909
  oldState.connection.id = connectionId;
@@ -876,13 +912,18 @@ export class PluvRoom<
876
912
  return oldState;
877
913
  });
878
914
 
879
- this._usersManager.setMyself(connectionId, user as Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>>);
915
+ const userInfo: OptionalProps<UserInfo<TIO, TPresence>, "presence"> = {
916
+ connectionId,
917
+ presence: (data.presence as TPresence | null) ?? undefined,
918
+ user,
919
+ };
920
+ this._usersManager.setMyself(userInfo);
880
921
 
881
922
  const presence = this._usersManager.myPresence;
882
923
  const myself = this._usersManager.myself ?? null;
883
924
 
884
925
  this._stateNotifier.subjects["my-presence"].next(presence);
885
- this._stateNotifier.subjects["myself"].next(myself);
926
+ this._stateNotifier.subjects.myself.next(myself);
886
927
 
887
928
  const update = this._state.connection.count > 1 ? (this._crdtManager.doc.getEncodedState() ?? null) : null;
888
929
 
@@ -972,20 +1013,25 @@ export class PluvRoom<
972
1013
  if (!this._usersManager.myself) return;
973
1014
 
974
1015
  const data = message.data as BaseIOEventRecord<InferIOAuthorize<TIO>>["$userJoined"];
975
-
976
1016
  const myself = this._usersManager.myself;
977
1017
 
978
1018
  if (myself.connectionId === connectionId) {
979
1019
  this._sendMessage({ type: "$getOthers", data: {} });
1020
+ return;
980
1021
  }
981
1022
 
982
- this._usersManager.setUser(connectionId, data.user, data.presence as TPresence);
1023
+ const result = this._usersManager.addConnection({
1024
+ connectionId,
1025
+ presence: data.presence as TPresence,
1026
+ user: data.user,
1027
+ });
983
1028
 
1029
+ const clientId = result?.clientId ?? null;
984
1030
  const other = this._usersManager.getOther(connectionId);
985
1031
  const others = this._usersManager.getOthers();
986
1032
 
987
- this._otherNotifier.subject(connectionId).next(other);
988
- this._stateNotifier.subjects["others"].next(others);
1033
+ if (!!clientId) this._otherNotifier.subject(clientId).next(other);
1034
+ this._stateNotifier.subjects.others.next(others);
989
1035
  }
990
1036
 
991
1037
  private _heartbeat(): void {
@@ -1002,10 +1048,8 @@ export class PluvRoom<
1002
1048
  }
1003
1049
 
1004
1050
  private _logDebug(...data: any[]): void {
1005
- // eslint-disable-next-line turbo/no-undeclared-env-vars
1006
1051
  if ((process as unknown as any)?.env?.NODE_ENV === "production") return;
1007
-
1008
- this._debug && console.log(...data);
1052
+ if (this._debug) console.log(...data);
1009
1053
  }
1010
1054
 
1011
1055
  private _observeCrdt(): void {
@@ -1050,7 +1094,7 @@ export class PluvRoom<
1050
1094
 
1051
1095
  private async _onClose(event: CloseEvent): Promise<void> {
1052
1096
  this._logDebug("WebSocket closed");
1053
- !!event.reason && this._logDebug(event.reason);
1097
+ if (!!event.reason) this._logDebug(event.reason);
1054
1098
 
1055
1099
  const shouldRetry = [
1056
1100
  // Going away: Client/server is shutting down or navigating
@@ -1067,11 +1111,6 @@ export class PluvRoom<
1067
1111
  1014,
1068
1112
  ].some((okToRetryCode) => event.code === okToRetryCode);
1069
1113
 
1070
- this._closeWs();
1071
-
1072
- if (!shouldRetry) this._clearTimeout(this._timeouts.reconnect);
1073
- else this._pollReconnect();
1074
-
1075
1114
  this._updateState((oldState) => {
1076
1115
  oldState.authorization.token = null;
1077
1116
  oldState.connection.state = shouldRetry ? ConnectionState.Unavailable : ConnectionState.Closed;
@@ -1079,6 +1118,14 @@ export class PluvRoom<
1079
1118
 
1080
1119
  return oldState;
1081
1120
  });
1121
+
1122
+ if (shouldRetry) {
1123
+ await this._reconnect();
1124
+ return;
1125
+ }
1126
+
1127
+ this._closeWs();
1128
+ this._clearTimeout(this._timeouts.reconnect);
1082
1129
  }
1083
1130
 
1084
1131
  /**
@@ -1,4 +1,5 @@
1
- import type { Id, InferIOAuthorize, InferIOAuthorizeUser, InputZodLike, IOLike, JsonObject } from "@pluv/types";
1
+ import type { InputZodLike, IOLike, JsonObject, OptionalProps } from "@pluv/types";
2
+ import { MAX_PRESENCE_SIZE_BYTES } from "./constants";
2
3
  import type { UserInfo } from "./types";
3
4
 
4
5
  export type Presence = Record<string, unknown>;
@@ -8,7 +9,25 @@ export type UsersManagerConfig<TPresence extends JsonObject = {}> = {
8
9
  presence?: InputZodLike<TPresence>;
9
10
  };
10
11
 
12
+ export type AddConnectionResult<TIO extends IOLike, TPresence extends JsonObject = {}> = {
13
+ clientId: string;
14
+ data: UserInfo<TIO, TPresence>;
15
+ isMyself: boolean;
16
+ remaining: number;
17
+ };
18
+
19
+ export type DeleteConnectionResult<TIO extends IOLike, TPresence extends JsonObject = {}> = {
20
+ clientId: string;
21
+ data: UserInfo<TIO, TPresence>;
22
+ remaining: number;
23
+ };
24
+
11
25
  export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}> {
26
+ private _idMap = {
27
+ fromConnectionId: new Map<[connectionId: string][0], [clientId: string][0]>(),
28
+ fromClientId: new Map<[clientId: string][0], Set<[connectionId: string][0]>>(),
29
+ };
30
+
12
31
  private _initialPresence: TPresence;
13
32
  /**
14
33
  * @description This presence can be updated while the user is not
@@ -19,7 +38,7 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
19
38
  * @description This is only set when the user is connected.
20
39
  */
21
40
  private _myself: UserInfo<TIO, TPresence> | null = null;
22
- private _others = new Map<string, UserInfo<TIO, TPresence>>();
41
+ private _others = new Map<[clientId: string][0], UserInfo<TIO, TPresence>>();
23
42
  private _presence: InputZodLike<TPresence> | null = null;
24
43
 
25
44
  constructor(config: UsersManagerConfig<TPresence>) {
@@ -38,102 +57,239 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
38
57
  return this._myself;
39
58
  }
40
59
 
41
- public clear(): void {
60
+ public addConnection(
61
+ userInfo: OptionalProps<UserInfo<TIO, TPresence>, "presence">,
62
+ ): AddConnectionResult<TIO, TPresence> {
63
+ const data: UserInfo<TIO, TPresence> = {
64
+ ...userInfo,
65
+ presence: userInfo.presence ?? this._initialPresence,
66
+ };
67
+
68
+ const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
69
+ const clientId = this.getClientId(data);
70
+ const remaining = this._setConnectionId(userInfo.connectionId, clientId).size;
71
+
72
+ if (myClientId === clientId) {
73
+ const result: AddConnectionResult<TIO, TPresence> = {
74
+ clientId,
75
+ data,
76
+ isMyself: true,
77
+ remaining,
78
+ };
79
+
80
+ return result;
81
+ }
82
+
83
+ const other = this._others.get(clientId);
84
+
85
+ /**
86
+ * !HACK
87
+ * @description Don't override the other user, so that we ensure the user's presence isn't
88
+ * overwritten by whatever incoming connection.
89
+ * @date April 16, 2025
90
+ */
91
+ if (!other) this._others.set(clientId, data);
92
+
93
+ const result: AddConnectionResult<TIO, TPresence> = {
94
+ clientId,
95
+ data,
96
+ isMyself: false,
97
+ remaining,
98
+ };
99
+
100
+ return result;
101
+ }
102
+
103
+ public clearConnections(): void {
104
+ this.removeMyself();
42
105
  this._others.clear();
106
+ this._idMap.fromClientId.clear();
107
+ this._idMap.fromConnectionId.clear();
108
+ }
109
+
110
+ public deleteConnection(connectionId: string): DeleteConnectionResult<TIO, TPresence> | null {
111
+ const clientId = this.getClientId(connectionId);
112
+
113
+ this._deleteConnectionId(connectionId);
114
+
115
+ // We expect that a clientId should always exist here, but we'll just check this anyways
116
+ if (!clientId) return null;
117
+
118
+ const userInfo = this._others.get(clientId) ?? null;
119
+
120
+ // We also expect this should always exist here, but we'll just check this anyways
121
+ if (!userInfo) return null;
122
+
123
+ const remaining = this._getConnectionIds(clientId)?.size ?? 0;
124
+ const result: DeleteConnectionResult<TIO, TPresence> = { clientId, remaining, data: userInfo };
125
+
126
+ /**
127
+ * @description A user may have multiple websocket connections open. If one connection is
128
+ * dropped, the user may still be connected via another websocket (e.g. on anther browser
129
+ * tab). In this case, we just want to remove the connection mapping for the connection
130
+ * that is dropped, but keep the remaining connections and the user.
131
+ * @date April 16, 2025
132
+ */
133
+ if (!remaining) this._others.delete(clientId);
134
+
135
+ return result;
136
+ }
137
+
138
+ /**
139
+ * @description The client id is either the user's id (if authorized) or the connection id
140
+ * otherwise. This is so that, despite having multiple connections, the user will only have one
141
+ * presence to all other users.
142
+ * @date April 16, 2025
143
+ */
144
+ public getClientId(connectionId: string): string | null;
145
+ public getClientId(userInfo: UserInfo<TIO, TPresence>): string;
146
+ public getClientId(input: string | UserInfo<TIO, TPresence>): string | null {
147
+ if (typeof input !== "string") {
148
+ const userInfo = input;
149
+ const connectionId = userInfo.connectionId;
150
+ const userId = userInfo.user?.id ?? null;
151
+
152
+ return userId ?? connectionId;
153
+ }
154
+
155
+ const connectionId = input;
156
+
157
+ if (this._myself?.connectionId === connectionId) {
158
+ return this.getClientId(this._myself);
159
+ }
160
+
161
+ const clientId = this._idMap.fromConnectionId.get(connectionId) ?? null;
162
+
163
+ return clientId;
43
164
  }
44
165
 
45
166
  public getOther(connectionId: string): UserInfo<TIO, TPresence> | null {
46
- return this._others.get(connectionId) ?? null;
167
+ const clientId = this.getClientId(connectionId);
168
+
169
+ if (!clientId) return null;
170
+
171
+ return this._others.get(clientId) ?? null;
47
172
  }
48
173
 
49
174
  public getOthers(): readonly UserInfo<TIO, TPresence>[] {
50
175
  return Array.from(this._others.values());
51
176
  }
52
177
 
53
- public patchPresence(connectionId: string, patch: Partial<TPresence>): void {
54
- if (this._myself?.connectionId === connectionId) {
55
- this.updateMyPresence(patch);
178
+ public patchPresence(connectionId: string, patch: Partial<TPresence>): TPresence | null {
179
+ const clientId = this.getClientId(connectionId);
180
+ const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
56
181
 
57
- return;
58
- }
182
+ if (!clientId) return null;
183
+ if (myClientId === clientId) return this.updateMyPresence(patch);
59
184
 
60
- const other = this._others.get(connectionId);
185
+ const other = this._others.get(clientId);
61
186
 
62
- if (!other) return;
187
+ if (!other) return null;
63
188
 
64
- const presence = {
65
- ...other.presence,
66
- ...patch,
67
- } as TPresence;
189
+ const presence = { ...other.presence, ...patch } as TPresence;
190
+ const validated = this._presence ? this._presence.parse(presence) : presence;
68
191
 
69
- this._presence?.parse(presence);
192
+ this._others.set(clientId, { ...other, presence: validated });
70
193
 
71
- this._others.set(connectionId, { ...other, presence });
194
+ return validated;
72
195
  }
73
196
 
74
197
  public removeMyself(): void {
198
+ if (!this._myself) return;
199
+
200
+ const connectionId = this._myself.connectionId;
201
+
202
+ this._deleteConnectionId(connectionId);
75
203
  this._myself = null;
76
204
  }
77
205
 
78
- public removeUser(connectionId: string): void {
79
- this._others.delete(connectionId);
80
- }
206
+ public setMyself(userInfo: OptionalProps<UserInfo<TIO, TPresence>, "presence">): void {
207
+ const connectionId = userInfo.connectionId;
208
+ const presence = userInfo.presence ?? this._initialPresence;
81
209
 
82
- public removeUsers(): void {
83
- this._others.clear();
84
- }
210
+ this._myself = { ...userInfo, presence };
211
+ this._myPresence = presence;
85
212
 
86
- public setMyself(connectionId: string, user: Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>>): void {
87
- this._myself = {
88
- connectionId,
89
- presence: this._initialPresence,
90
- user,
91
- };
213
+ const clientId = this.getClientId(this._myself);
92
214
 
93
- this._myPresence = this._initialPresence;
215
+ this._setConnectionId(connectionId, clientId);
94
216
  }
95
217
 
96
218
  public setPresence(connectionId: string, presence: TPresence): void {
97
- if (this._myself?.connectionId === connectionId) {
219
+ const clientId = this.getClientId(connectionId);
220
+ const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
221
+
222
+ if (!clientId) return;
223
+
224
+ if (myClientId === clientId) {
225
+ if (!this._myself) return;
226
+
98
227
  this._myself.presence = presence;
99
228
  this._myPresence = presence;
100
229
 
101
230
  return;
102
231
  }
103
232
 
104
- const other = this._others.get(connectionId);
233
+ const other = this._others.get(clientId);
105
234
 
106
235
  if (!other) return;
107
236
 
108
- this._others.set(connectionId, { ...other, presence });
237
+ this._others.set(clientId, { ...other, presence });
238
+ }
239
+
240
+ /**
241
+ * @description This method need not care about being connected.
242
+ */
243
+ public updateMyPresence(patch: Partial<TPresence>): TPresence {
244
+ const updated = { ...this._myPresence, ...patch };
245
+ const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
246
+
247
+ if (bytes > MAX_PRESENCE_SIZE_BYTES) {
248
+ throw new Error(
249
+ `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`,
250
+ );
251
+ }
252
+
253
+ this._myPresence = updated;
254
+
255
+ if (!this._myself) return updated;
256
+
257
+ this._myself.presence = updated;
258
+
259
+ return updated;
109
260
  }
110
261
 
111
- public setUser(
112
- connectionId: string,
113
- user: Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>>,
114
- presence?: TPresence,
115
- ): void {
116
- if (this._myself?.connectionId === connectionId) return;
262
+ private _deleteConnectionId(connectionId: string): void {
263
+ const clientId = this.getClientId(connectionId);
264
+
265
+ this._idMap.fromConnectionId.delete(connectionId);
266
+
267
+ if (!clientId) return;
117
268
 
118
- const other = this._others.get(connectionId);
269
+ const set = this._idMap.fromClientId.get(clientId) ?? null;
119
270
 
120
- if (other) return;
271
+ if (!set) return;
121
272
 
122
- this._others.set(connectionId, {
123
- connectionId,
124
- presence: presence ?? this._initialPresence,
125
- user,
126
- });
273
+ set.delete(connectionId);
274
+
275
+ if (!!set.size) return;
276
+
277
+ this._idMap.fromClientId.delete(clientId);
127
278
  }
128
279
 
129
- /**
130
- * @description This method need not care about being connected.
131
- */
132
- public updateMyPresence(patch: Partial<TPresence>): void {
133
- this._myPresence = { ...this._myPresence, ...patch };
280
+ private _getConnectionIds(clientId: string): Set<[connectionId: string][0]> | null {
281
+ const set = this._idMap.fromClientId.get(clientId);
134
282
 
135
- if (!this._myself) return;
283
+ return set ?? null;
284
+ }
285
+
286
+ private _setConnectionId(connectionId: string, clientId: string): Set<[connectionId: string][0]> {
287
+ const set = this._idMap.fromClientId.get(clientId) ?? new Set<string>();
288
+ const updated = set.add(connectionId);
289
+
290
+ this._idMap.fromConnectionId.set(connectionId, clientId);
291
+ this._idMap.fromClientId.set(clientId, updated);
136
292
 
137
- this._myself.presence = this._myPresence;
293
+ return updated;
138
294
  }
139
295
  }
@@ -0,0 +1 @@
1
+ export const MAX_PRESENCE_SIZE_BYTES = 512;
package/src/types.ts CHANGED
@@ -13,7 +13,7 @@ import type { PluvClient } from "./PluvClient";
13
13
 
14
14
  export interface AuthorizationState<TIO extends IOLike> {
15
15
  token: string | null;
16
- user: InferIOAuthorizeUser<InferIOAuthorize<TIO>> | null;
16
+ user: Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>> | null;
17
17
  }
18
18
 
19
19
  export type EventResolver<