@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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +24 -0
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +209 -85
- package/dist/index.mjs +209 -85
- package/package.json +7 -7
- package/src/AbstractRoom.ts +4 -1
- package/src/MockedRoom.ts +11 -4
- package/src/OtherNotifier.ts +21 -18
- package/src/PluvClient.ts +2 -3
- package/src/PluvRoom.ts +83 -36
- package/src/UsersManager.ts +209 -53
- package/src/constants.ts +1 -0
- package/src/types.ts +1 -1
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
|
|
391
|
-
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 = (
|
|
514
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
808
|
-
|
|
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(
|
|
837
|
-
this._stateNotifier.subjects
|
|
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.
|
|
877
|
+
const result = this._usersManager.addConnection({
|
|
878
|
+
connectionId,
|
|
879
|
+
presence: (presence ?? undefined) as TPresence,
|
|
880
|
+
user,
|
|
881
|
+
});
|
|
851
882
|
|
|
852
|
-
|
|
883
|
+
if (!!presence) this._usersManager.patchPresence(connectionId, presence as TPresence);
|
|
884
|
+
if (result.isMyself) return;
|
|
853
885
|
|
|
854
|
-
|
|
886
|
+
const clientId = result.clientId;
|
|
887
|
+
const other = this._usersManager.getOther(connectionId);
|
|
855
888
|
|
|
856
|
-
this._otherNotifier.subject(
|
|
889
|
+
this._otherNotifier.subject(clientId).next(other);
|
|
857
890
|
});
|
|
858
891
|
|
|
859
892
|
const others = this._usersManager.getOthers();
|
|
860
893
|
|
|
861
|
-
this._stateNotifier.subjects
|
|
894
|
+
this._stateNotifier.subjects.others.next(others);
|
|
862
895
|
}
|
|
863
896
|
|
|
864
897
|
private _handleRegisteredMessage(message: IOEventMessage<TIO>): void {
|
|
865
|
-
const { connectionId
|
|
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
|
-
|
|
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
|
|
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.
|
|
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(
|
|
988
|
-
this._stateNotifier.subjects
|
|
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
|
|
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
|
/**
|
package/src/UsersManager.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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
|
-
|
|
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>):
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
182
|
+
if (!clientId) return null;
|
|
183
|
+
if (myClientId === clientId) return this.updateMyPresence(patch);
|
|
59
184
|
|
|
60
|
-
const other = this._others.get(
|
|
185
|
+
const other = this._others.get(clientId);
|
|
61
186
|
|
|
62
|
-
if (!other) return;
|
|
187
|
+
if (!other) return null;
|
|
63
188
|
|
|
64
|
-
const presence = {
|
|
65
|
-
|
|
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.
|
|
192
|
+
this._others.set(clientId, { ...other, presence: validated });
|
|
70
193
|
|
|
71
|
-
|
|
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
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
this.
|
|
84
|
-
}
|
|
210
|
+
this._myself = { ...userInfo, presence };
|
|
211
|
+
this._myPresence = presence;
|
|
85
212
|
|
|
86
|
-
|
|
87
|
-
this._myself = {
|
|
88
|
-
connectionId,
|
|
89
|
-
presence: this._initialPresence,
|
|
90
|
-
user,
|
|
91
|
-
};
|
|
213
|
+
const clientId = this.getClientId(this._myself);
|
|
92
214
|
|
|
93
|
-
this.
|
|
215
|
+
this._setConnectionId(connectionId, clientId);
|
|
94
216
|
}
|
|
95
217
|
|
|
96
218
|
public setPresence(connectionId: string, presence: TPresence): void {
|
|
97
|
-
|
|
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(
|
|
233
|
+
const other = this._others.get(clientId);
|
|
105
234
|
|
|
106
235
|
if (!other) return;
|
|
107
236
|
|
|
108
|
-
this._others.set(
|
|
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
|
-
|
|
112
|
-
connectionId
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
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
|
|
269
|
+
const set = this._idMap.fromClientId.get(clientId) ?? null;
|
|
119
270
|
|
|
120
|
-
if (
|
|
271
|
+
if (!set) return;
|
|
121
272
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
273
|
+
set.delete(connectionId);
|
|
274
|
+
|
|
275
|
+
if (!!set.size) return;
|
|
276
|
+
|
|
277
|
+
this._idMap.fromClientId.delete(clientId);
|
|
127
278
|
}
|
|
128
279
|
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
+
return updated;
|
|
138
294
|
}
|
|
139
295
|
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
|
16
|
+
user: Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>> | null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export type EventResolver<
|