@automerge/automerge-repo-react-hooks 2.5.2-alpha.0 → 2.5.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +269 -323
- package/dist/index.js.map +1 -1
- package/dist/usePresence.d.ts +29 -5
- package/dist/usePresence.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/usePresence.ts +60 -23
- package/dist/helpers/useInvalidate.d.ts +0 -2
- package/dist/helpers/useInvalidate.d.ts.map +0 -1
- package/src/helpers/useInvalidate.ts +0 -7
package/dist/index.js
CHANGED
|
@@ -6681,9 +6681,228 @@ requireSha256();
|
|
|
6681
6681
|
|
|
6682
6682
|
debug("automerge-repo:collectionsync");
|
|
6683
6683
|
|
|
6684
|
-
const PRESENCE_MESSAGE_MARKER = "__presence";
|
|
6685
6684
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
6686
6685
|
const DEFAULT_PEER_TTL_MS = 3 * DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
6686
|
+
const PRESENCE_MESSAGE_MARKER = "__presence";
|
|
6687
|
+
|
|
6688
|
+
function unique(items) {
|
|
6689
|
+
return Array.from(new Set(items));
|
|
6690
|
+
}
|
|
6691
|
+
|
|
6692
|
+
class PeerStateView {
|
|
6693
|
+
value;
|
|
6694
|
+
constructor(value) {
|
|
6695
|
+
this.value = value;
|
|
6696
|
+
}
|
|
6697
|
+
/**
|
|
6698
|
+
* Get all users.
|
|
6699
|
+
*
|
|
6700
|
+
* @returns Array of user presence {@link State}s
|
|
6701
|
+
*/
|
|
6702
|
+
get users() {
|
|
6703
|
+
const userIds = unique(Object.values(this.value).map(peerState => peerState.userId));
|
|
6704
|
+
return userIds.map(u => this.getUserState(u));
|
|
6705
|
+
}
|
|
6706
|
+
/**
|
|
6707
|
+
* Get all devices.
|
|
6708
|
+
*
|
|
6709
|
+
* @returns Array of device presence {@link State}s
|
|
6710
|
+
*/
|
|
6711
|
+
get devices() {
|
|
6712
|
+
const deviceIds = unique(Object.values(this.value).map(peerState => peerState.deviceId));
|
|
6713
|
+
return deviceIds.map(d => this.getDeviceState(d));
|
|
6714
|
+
}
|
|
6715
|
+
/**
|
|
6716
|
+
* Get all peers.
|
|
6717
|
+
*
|
|
6718
|
+
* @returns Array of peer presence {@link State}s
|
|
6719
|
+
*/
|
|
6720
|
+
get peers() {
|
|
6721
|
+
return Object.values(this.value);
|
|
6722
|
+
}
|
|
6723
|
+
/**
|
|
6724
|
+
* Get all peer ids for this user.
|
|
6725
|
+
*
|
|
6726
|
+
* @param userId
|
|
6727
|
+
* @returns Array of peer ids for this user
|
|
6728
|
+
*/
|
|
6729
|
+
getUserPeers(userId) {
|
|
6730
|
+
return Object.values(this.value)
|
|
6731
|
+
.filter(peerState => peerState.userId === userId)
|
|
6732
|
+
.map(peerState => peerState.peerId);
|
|
6733
|
+
}
|
|
6734
|
+
/**
|
|
6735
|
+
* Get all peers for this device.
|
|
6736
|
+
*
|
|
6737
|
+
* @param deviceId
|
|
6738
|
+
* @returns Array of peer ids for this device
|
|
6739
|
+
*/
|
|
6740
|
+
getDevicePeers(deviceId) {
|
|
6741
|
+
return Object.values(this.value)
|
|
6742
|
+
.filter(peerState => peerState.deviceId === deviceId)
|
|
6743
|
+
.map(peerState => peerState.peerId);
|
|
6744
|
+
}
|
|
6745
|
+
/**
|
|
6746
|
+
* Return the most-recently-seen peer from this group.
|
|
6747
|
+
*
|
|
6748
|
+
* @param peers
|
|
6749
|
+
* @returns id of most recently seen peer
|
|
6750
|
+
*/
|
|
6751
|
+
getLastSeenPeer(peers) {
|
|
6752
|
+
let freshestLastSeenAt;
|
|
6753
|
+
return peers.reduce((freshest, curr) => {
|
|
6754
|
+
const lastSeenAt = this.value[curr]?.lastActiveAt;
|
|
6755
|
+
if (!lastSeenAt) {
|
|
6756
|
+
return freshest;
|
|
6757
|
+
}
|
|
6758
|
+
if (!freshest || lastSeenAt > freshestLastSeenAt) {
|
|
6759
|
+
freshestLastSeenAt = lastSeenAt;
|
|
6760
|
+
return curr;
|
|
6761
|
+
}
|
|
6762
|
+
return freshest;
|
|
6763
|
+
}, undefined);
|
|
6764
|
+
}
|
|
6765
|
+
/**
|
|
6766
|
+
* Return the peer from this group that sent a state update most recently
|
|
6767
|
+
*
|
|
6768
|
+
* @param peers
|
|
6769
|
+
* @returns id of most recently seen peer
|
|
6770
|
+
*/
|
|
6771
|
+
getLastActivePeer(peers) {
|
|
6772
|
+
let freshestLastActiveAt;
|
|
6773
|
+
return peers.reduce((freshest, curr) => {
|
|
6774
|
+
const lastActiveAt = this.value[curr]?.lastActiveAt;
|
|
6775
|
+
if (!lastActiveAt) {
|
|
6776
|
+
return freshest;
|
|
6777
|
+
}
|
|
6778
|
+
if (!freshest || lastActiveAt > freshestLastActiveAt) {
|
|
6779
|
+
freshestLastActiveAt = lastActiveAt;
|
|
6780
|
+
return curr;
|
|
6781
|
+
}
|
|
6782
|
+
return freshest;
|
|
6783
|
+
}, undefined);
|
|
6784
|
+
}
|
|
6785
|
+
/**
|
|
6786
|
+
* Get current ephemeral state value for this user's most-recently-active
|
|
6787
|
+
* peer.
|
|
6788
|
+
*
|
|
6789
|
+
* @param userId
|
|
6790
|
+
* @returns user's {@link State}
|
|
6791
|
+
*/
|
|
6792
|
+
getUserState(userId) {
|
|
6793
|
+
const peers = this.getUserPeers(userId);
|
|
6794
|
+
if (!peers) {
|
|
6795
|
+
return undefined;
|
|
6796
|
+
}
|
|
6797
|
+
const peer = this.getLastActivePeer(peers);
|
|
6798
|
+
if (!peer) {
|
|
6799
|
+
return undefined;
|
|
6800
|
+
}
|
|
6801
|
+
return this.value[peer];
|
|
6802
|
+
}
|
|
6803
|
+
/**
|
|
6804
|
+
* Get current ephemeral state value for this device's most-recently-active
|
|
6805
|
+
* peer.
|
|
6806
|
+
*
|
|
6807
|
+
* @param deviceId
|
|
6808
|
+
* @returns device's {@link State}
|
|
6809
|
+
*/
|
|
6810
|
+
getDeviceState(deviceId) {
|
|
6811
|
+
const peers = this.getDevicePeers(deviceId);
|
|
6812
|
+
if (!peers) {
|
|
6813
|
+
return undefined;
|
|
6814
|
+
}
|
|
6815
|
+
const peer = this.getLastActivePeer(peers);
|
|
6816
|
+
if (!peer) {
|
|
6817
|
+
return undefined;
|
|
6818
|
+
}
|
|
6819
|
+
return this.value[peer];
|
|
6820
|
+
}
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6823
|
+
class PeerPresenceInfo {
|
|
6824
|
+
ttl;
|
|
6825
|
+
#peerStates = new PeerStateView({});
|
|
6826
|
+
/**
|
|
6827
|
+
* Build a new peer presence state.
|
|
6828
|
+
*
|
|
6829
|
+
* @param ttl in milliseconds - peers with no activity within this timeframe
|
|
6830
|
+
* are forgotten when {@link prune} is called.
|
|
6831
|
+
*/
|
|
6832
|
+
constructor(ttl) {
|
|
6833
|
+
this.ttl = ttl;
|
|
6834
|
+
}
|
|
6835
|
+
has(peerId) {
|
|
6836
|
+
return peerId in this.#peerStates.value;
|
|
6837
|
+
}
|
|
6838
|
+
/**
|
|
6839
|
+
* Record that we've seen the given peer recently.
|
|
6840
|
+
*
|
|
6841
|
+
* @param peerId
|
|
6842
|
+
*/
|
|
6843
|
+
markSeen(peerId) {
|
|
6844
|
+
this.#peerStates = new PeerStateView({
|
|
6845
|
+
...this.#peerStates.value,
|
|
6846
|
+
[peerId]: {
|
|
6847
|
+
...this.#peerStates.value[peerId],
|
|
6848
|
+
lastSeen: Date.now(),
|
|
6849
|
+
},
|
|
6850
|
+
});
|
|
6851
|
+
}
|
|
6852
|
+
/**
|
|
6853
|
+
* Record a state update for the given peer. Note that existing state is not
|
|
6854
|
+
* overwritten.
|
|
6855
|
+
*
|
|
6856
|
+
* @param peerId
|
|
6857
|
+
* @param value
|
|
6858
|
+
*/
|
|
6859
|
+
update({ peerId, deviceId, userId, value, }) {
|
|
6860
|
+
const peerState = this.#peerStates.value[peerId];
|
|
6861
|
+
const existingState = peerState?.value ?? {};
|
|
6862
|
+
const now = Date.now();
|
|
6863
|
+
this.#peerStates = new PeerStateView({
|
|
6864
|
+
...this.#peerStates.value,
|
|
6865
|
+
[peerId]: {
|
|
6866
|
+
peerId,
|
|
6867
|
+
deviceId,
|
|
6868
|
+
userId,
|
|
6869
|
+
lastActiveAt: now,
|
|
6870
|
+
lastUpdateAt: now,
|
|
6871
|
+
value: {
|
|
6872
|
+
...existingState,
|
|
6873
|
+
...value,
|
|
6874
|
+
},
|
|
6875
|
+
},
|
|
6876
|
+
});
|
|
6877
|
+
}
|
|
6878
|
+
/**
|
|
6879
|
+
* Forget the given peer.
|
|
6880
|
+
*
|
|
6881
|
+
* @param peerId
|
|
6882
|
+
*/
|
|
6883
|
+
delete(peerId) {
|
|
6884
|
+
this.#peerStates = new PeerStateView(Object.fromEntries(Object.entries(this.#peerStates.value).filter(([existingId]) => {
|
|
6885
|
+
return existingId != peerId;
|
|
6886
|
+
})));
|
|
6887
|
+
}
|
|
6888
|
+
/**
|
|
6889
|
+
* Prune all peers that have not been seen since the configured ttl has
|
|
6890
|
+
* elapsed.
|
|
6891
|
+
*/
|
|
6892
|
+
prune() {
|
|
6893
|
+
const threshold = Date.now() - this.ttl;
|
|
6894
|
+
this.#peerStates = new PeerStateView(Object.fromEntries(Object.entries(this.#peerStates).filter(([, state]) => {
|
|
6895
|
+
return state.lastActiveAt >= threshold;
|
|
6896
|
+
})));
|
|
6897
|
+
}
|
|
6898
|
+
/**
|
|
6899
|
+
* Get a snapshot of the current peer states
|
|
6900
|
+
*/
|
|
6901
|
+
get states() {
|
|
6902
|
+
return this.#peerStates;
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
|
|
6687
6906
|
/**
|
|
6688
6907
|
* Presence encapsulates ephemeral state communication for a specific doc
|
|
6689
6908
|
* handle. It tracks caller-provided local state and broadcasts that state to
|
|
@@ -6717,6 +6936,7 @@ class Presence extends EventEmitter {
|
|
|
6717
6936
|
super();
|
|
6718
6937
|
this.#handle = handle;
|
|
6719
6938
|
this.#peers = new PeerPresenceInfo(DEFAULT_PEER_TTL_MS);
|
|
6939
|
+
this.#localState = {};
|
|
6720
6940
|
this.userId = userId;
|
|
6721
6941
|
this.deviceId = deviceId;
|
|
6722
6942
|
}
|
|
@@ -6743,35 +6963,24 @@ class Presence extends EventEmitter {
|
|
|
6743
6963
|
}
|
|
6744
6964
|
const message = envelope[PRESENCE_MESSAGE_MARKER];
|
|
6745
6965
|
const { deviceId, userId } = message;
|
|
6746
|
-
if (!this.#peers.
|
|
6966
|
+
if (!this.#peers.has(peerId)) {
|
|
6747
6967
|
this.announce();
|
|
6748
6968
|
}
|
|
6749
6969
|
switch (message.type) {
|
|
6750
6970
|
case "heartbeat":
|
|
6751
|
-
this.#peers.markSeen(peerId
|
|
6752
|
-
this.emit("heartbeat", {
|
|
6753
|
-
type: "heartbeat",
|
|
6754
|
-
peerId,
|
|
6755
|
-
deviceId,
|
|
6756
|
-
userId,
|
|
6757
|
-
});
|
|
6971
|
+
this.#peers.markSeen(peerId);
|
|
6972
|
+
this.emit("heartbeat", { type: "heartbeat", peerId });
|
|
6758
6973
|
break;
|
|
6759
6974
|
case "goodbye":
|
|
6760
6975
|
this.#peers.delete(peerId);
|
|
6761
|
-
this.emit("goodbye", {
|
|
6762
|
-
type: "goodbye",
|
|
6763
|
-
peerId,
|
|
6764
|
-
deviceId,
|
|
6765
|
-
userId,
|
|
6766
|
-
});
|
|
6976
|
+
this.emit("goodbye", { type: "goodbye", peerId });
|
|
6767
6977
|
break;
|
|
6768
6978
|
case "update":
|
|
6769
6979
|
this.#peers.update({
|
|
6770
6980
|
peerId,
|
|
6771
6981
|
deviceId,
|
|
6772
6982
|
userId,
|
|
6773
|
-
channel: message.
|
|
6774
|
-
value: message.value,
|
|
6983
|
+
value: { [message.channel]: message.value },
|
|
6775
6984
|
});
|
|
6776
6985
|
this.emit("update", {
|
|
6777
6986
|
type: "update",
|
|
@@ -6783,14 +6992,11 @@ class Presence extends EventEmitter {
|
|
|
6783
6992
|
});
|
|
6784
6993
|
break;
|
|
6785
6994
|
case "snapshot":
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
channel: channel,
|
|
6792
|
-
value,
|
|
6793
|
-
});
|
|
6995
|
+
this.#peers.update({
|
|
6996
|
+
peerId,
|
|
6997
|
+
deviceId,
|
|
6998
|
+
userId,
|
|
6999
|
+
value: message.state,
|
|
6794
7000
|
});
|
|
6795
7001
|
this.emit("snapshot", {
|
|
6796
7002
|
type: "snapshot",
|
|
@@ -6810,7 +7016,7 @@ class Presence extends EventEmitter {
|
|
|
6810
7016
|
* Return a view of current peer states.
|
|
6811
7017
|
*/
|
|
6812
7018
|
getPeerStates() {
|
|
6813
|
-
return this.#peers.
|
|
7019
|
+
return this.#peers.states;
|
|
6814
7020
|
}
|
|
6815
7021
|
/**
|
|
6816
7022
|
* Return a view of current local state.
|
|
@@ -6829,7 +7035,7 @@ class Presence extends EventEmitter {
|
|
|
6829
7035
|
this.#localState = Object.assign({}, this.#localState, {
|
|
6830
7036
|
[channel]: value,
|
|
6831
7037
|
});
|
|
6832
|
-
this.broadcastChannelState(channel
|
|
7038
|
+
this.broadcastChannelState(channel);
|
|
6833
7039
|
}
|
|
6834
7040
|
/**
|
|
6835
7041
|
* Whether this Presence is currently active. See
|
|
@@ -6860,7 +7066,7 @@ class Presence extends EventEmitter {
|
|
|
6860
7066
|
this.#handle.off("ephemeral-message", this.#handleEphemeralMessage);
|
|
6861
7067
|
this.stopHeartbeats();
|
|
6862
7068
|
this.stopPruningPeers();
|
|
6863
|
-
this.
|
|
7069
|
+
this.send({ type: "goodbye" });
|
|
6864
7070
|
this.#running = false;
|
|
6865
7071
|
}
|
|
6866
7072
|
announce() {
|
|
@@ -6877,7 +7083,8 @@ class Presence extends EventEmitter {
|
|
|
6877
7083
|
this.doBroadcast("snapshot", { state: this.#localState });
|
|
6878
7084
|
this.resetHeartbeats();
|
|
6879
7085
|
}
|
|
6880
|
-
broadcastChannelState(channel
|
|
7086
|
+
broadcastChannelState(channel) {
|
|
7087
|
+
const value = this.#localState[channel];
|
|
6881
7088
|
this.doBroadcast("update", { channel, value });
|
|
6882
7089
|
this.resetHeartbeats();
|
|
6883
7090
|
}
|
|
@@ -6888,20 +7095,20 @@ class Presence extends EventEmitter {
|
|
|
6888
7095
|
this.stopHeartbeats();
|
|
6889
7096
|
this.startHeartbeats();
|
|
6890
7097
|
}
|
|
6891
|
-
sendHeartbeat() {
|
|
6892
|
-
this.doBroadcast("heartbeat");
|
|
6893
|
-
}
|
|
6894
7098
|
doBroadcast(type, extra) {
|
|
7099
|
+
this.send({
|
|
7100
|
+
userId: this.userId,
|
|
7101
|
+
deviceId: this.deviceId,
|
|
7102
|
+
type,
|
|
7103
|
+
...extra,
|
|
7104
|
+
});
|
|
7105
|
+
}
|
|
7106
|
+
send(message) {
|
|
6895
7107
|
if (!this.#running) {
|
|
6896
7108
|
return;
|
|
6897
7109
|
}
|
|
6898
7110
|
this.#handle.broadcast({
|
|
6899
|
-
[PRESENCE_MESSAGE_MARKER]:
|
|
6900
|
-
userId: this.userId,
|
|
6901
|
-
deviceId: this.deviceId,
|
|
6902
|
-
type,
|
|
6903
|
-
...extra,
|
|
6904
|
-
},
|
|
7111
|
+
[PRESENCE_MESSAGE_MARKER]: message,
|
|
6905
7112
|
});
|
|
6906
7113
|
}
|
|
6907
7114
|
startHeartbeats() {
|
|
@@ -6909,7 +7116,7 @@ class Presence extends EventEmitter {
|
|
|
6909
7116
|
return;
|
|
6910
7117
|
}
|
|
6911
7118
|
this.#heartbeatInterval = setInterval(() => {
|
|
6912
|
-
this.
|
|
7119
|
+
this.send({ type: "heartbeat" });
|
|
6913
7120
|
}, this.#heartbeatMs);
|
|
6914
7121
|
}
|
|
6915
7122
|
stopHeartbeats() {
|
|
@@ -6938,280 +7145,6 @@ class Presence extends EventEmitter {
|
|
|
6938
7145
|
this.#pruningInterval = undefined;
|
|
6939
7146
|
}
|
|
6940
7147
|
}
|
|
6941
|
-
/**
|
|
6942
|
-
* A summary of the latest Presence information for the set of peers who have
|
|
6943
|
-
* reported a Presence status to us.
|
|
6944
|
-
*/
|
|
6945
|
-
class PeerPresenceView {
|
|
6946
|
-
#peersLastSeen = new Map();
|
|
6947
|
-
#peerStates = new Map();
|
|
6948
|
-
#userPeers = new Map();
|
|
6949
|
-
#devicePeers = new Map();
|
|
6950
|
-
/** @hidden */
|
|
6951
|
-
constructor(peersLastSeen, peerStates, userPeers, devicePeers) {
|
|
6952
|
-
this.#peersLastSeen = peersLastSeen;
|
|
6953
|
-
this.#peerStates = peerStates;
|
|
6954
|
-
this.#userPeers = userPeers;
|
|
6955
|
-
this.#devicePeers = devicePeers;
|
|
6956
|
-
}
|
|
6957
|
-
/**
|
|
6958
|
-
* Check if peer is currently present.
|
|
6959
|
-
*
|
|
6960
|
-
* @param peerId
|
|
6961
|
-
* @returns true if the peer has been seen recently
|
|
6962
|
-
*/
|
|
6963
|
-
has(peerId) {
|
|
6964
|
-
return this.#peerStates.has(peerId);
|
|
6965
|
-
}
|
|
6966
|
-
/**
|
|
6967
|
-
* Check when the peer was last seen.
|
|
6968
|
-
*
|
|
6969
|
-
* @param peerId
|
|
6970
|
-
* @returns last seen UNIX timestamp, or undefined for unknown peers
|
|
6971
|
-
*/
|
|
6972
|
-
getLastSeen(peerId) {
|
|
6973
|
-
return this.#peersLastSeen.get(peerId);
|
|
6974
|
-
}
|
|
6975
|
-
/**
|
|
6976
|
-
* Get all recently-seen peers.
|
|
6977
|
-
*
|
|
6978
|
-
* @returns Array of peer ids
|
|
6979
|
-
*/
|
|
6980
|
-
getPeers() {
|
|
6981
|
-
return Array.from(this.#peerStates.keys());
|
|
6982
|
-
}
|
|
6983
|
-
/**
|
|
6984
|
-
* Get all recently-seen users.
|
|
6985
|
-
*
|
|
6986
|
-
* @returns Array of user ids
|
|
6987
|
-
*/
|
|
6988
|
-
getUsers() {
|
|
6989
|
-
return Array.from(this.#userPeers.keys());
|
|
6990
|
-
}
|
|
6991
|
-
/**
|
|
6992
|
-
* Get all recently-seen devices.
|
|
6993
|
-
*
|
|
6994
|
-
* @returns Array of device ids
|
|
6995
|
-
*/
|
|
6996
|
-
getDevices() {
|
|
6997
|
-
return Array.from(this.#devicePeers.keys());
|
|
6998
|
-
}
|
|
6999
|
-
/**
|
|
7000
|
-
* Get all recently-seen peers for this user.
|
|
7001
|
-
*
|
|
7002
|
-
* @param userId
|
|
7003
|
-
* @returns Array of peer ids for this user
|
|
7004
|
-
*/
|
|
7005
|
-
getUserPeers(userId) {
|
|
7006
|
-
const peers = this.#userPeers.get(userId);
|
|
7007
|
-
if (!peers) {
|
|
7008
|
-
return;
|
|
7009
|
-
}
|
|
7010
|
-
return Array.from(peers);
|
|
7011
|
-
}
|
|
7012
|
-
/**
|
|
7013
|
-
* Get all recently-seen peers for this device.
|
|
7014
|
-
*
|
|
7015
|
-
* @param deviceId
|
|
7016
|
-
* @returns Array of peer ids for this device
|
|
7017
|
-
*/
|
|
7018
|
-
getDevicePeers(deviceId) {
|
|
7019
|
-
const peers = this.#devicePeers.get(deviceId);
|
|
7020
|
-
if (!peers) {
|
|
7021
|
-
return;
|
|
7022
|
-
}
|
|
7023
|
-
return Array.from(peers);
|
|
7024
|
-
}
|
|
7025
|
-
/**
|
|
7026
|
-
* Get most-recently-seen peer from this group.
|
|
7027
|
-
*
|
|
7028
|
-
* @param peers
|
|
7029
|
-
* @returns id of most recently seen peer
|
|
7030
|
-
*/
|
|
7031
|
-
getFreshestPeer(peers) {
|
|
7032
|
-
let freshestLastSeen;
|
|
7033
|
-
return Array.from(peers).reduce((freshest, curr) => {
|
|
7034
|
-
const lastSeen = this.#peersLastSeen.get(curr);
|
|
7035
|
-
if (!lastSeen) {
|
|
7036
|
-
return freshest;
|
|
7037
|
-
}
|
|
7038
|
-
if (!freshest || lastSeen > freshestLastSeen) {
|
|
7039
|
-
freshestLastSeen = lastSeen;
|
|
7040
|
-
return curr;
|
|
7041
|
-
}
|
|
7042
|
-
return freshest;
|
|
7043
|
-
}, undefined);
|
|
7044
|
-
}
|
|
7045
|
-
/**
|
|
7046
|
-
* Get current @type PeerState for given peer.
|
|
7047
|
-
*
|
|
7048
|
-
* @param peerId
|
|
7049
|
-
* @returns details for the peer
|
|
7050
|
-
*/
|
|
7051
|
-
getPeerInfo(peerId) {
|
|
7052
|
-
return this.#peerStates.get(peerId);
|
|
7053
|
-
}
|
|
7054
|
-
/**
|
|
7055
|
-
* Get current ephemeral state value for this peer. If a channel is specified,
|
|
7056
|
-
* only returns the ephemeral state for that specific channel. Otherwise,
|
|
7057
|
-
* returns the full ephemeral state.
|
|
7058
|
-
*
|
|
7059
|
-
* @param peerId
|
|
7060
|
-
* @param channel
|
|
7061
|
-
* @returns latest ephemeral state received
|
|
7062
|
-
*/
|
|
7063
|
-
getPeerState(peerId, channel) {
|
|
7064
|
-
const fullState = this.#peerStates.get(peerId)?.value;
|
|
7065
|
-
if (!channel) {
|
|
7066
|
-
return fullState;
|
|
7067
|
-
}
|
|
7068
|
-
return fullState?.[channel];
|
|
7069
|
-
}
|
|
7070
|
-
/**
|
|
7071
|
-
* Get current ephemeral state value for this user's most-recently-active
|
|
7072
|
-
* peer. See {@link getPeerState}.
|
|
7073
|
-
*
|
|
7074
|
-
* @param userId
|
|
7075
|
-
* @param channel
|
|
7076
|
-
* @returns
|
|
7077
|
-
*/
|
|
7078
|
-
getUserState(userId, channel) {
|
|
7079
|
-
const peers = this.#userPeers.get(userId);
|
|
7080
|
-
if (!peers) {
|
|
7081
|
-
return undefined;
|
|
7082
|
-
}
|
|
7083
|
-
const peer = this.getFreshestPeer(peers);
|
|
7084
|
-
if (!peer) {
|
|
7085
|
-
return undefined;
|
|
7086
|
-
}
|
|
7087
|
-
return this.getPeerState(peer, channel);
|
|
7088
|
-
}
|
|
7089
|
-
/**
|
|
7090
|
-
* Get current ephemeral state value for this device's most-recently-active
|
|
7091
|
-
* peer. See {@link getPeerState}.
|
|
7092
|
-
*
|
|
7093
|
-
* @param userId
|
|
7094
|
-
* @param channel
|
|
7095
|
-
* @returns
|
|
7096
|
-
*/
|
|
7097
|
-
getDeviceState(deviceId, channel) {
|
|
7098
|
-
const peers = this.#devicePeers.get(deviceId);
|
|
7099
|
-
if (!peers) {
|
|
7100
|
-
return undefined;
|
|
7101
|
-
}
|
|
7102
|
-
const peer = this.getFreshestPeer(peers);
|
|
7103
|
-
if (!peer) {
|
|
7104
|
-
return undefined;
|
|
7105
|
-
}
|
|
7106
|
-
return this.getPeerState(peer, channel);
|
|
7107
|
-
}
|
|
7108
|
-
}
|
|
7109
|
-
class PeerPresenceInfo extends EventEmitter {
|
|
7110
|
-
ttl;
|
|
7111
|
-
#peersLastSeen = new Map();
|
|
7112
|
-
#peerStates = new Map();
|
|
7113
|
-
#userPeers = new Map();
|
|
7114
|
-
#devicePeers = new Map();
|
|
7115
|
-
view;
|
|
7116
|
-
/**
|
|
7117
|
-
* Build a new peer presence state.
|
|
7118
|
-
*
|
|
7119
|
-
* @param ttl in milliseconds - peers with no activity within this timeframe
|
|
7120
|
-
* are forgotten when {@link prune} is called.
|
|
7121
|
-
*/
|
|
7122
|
-
constructor(ttl) {
|
|
7123
|
-
super();
|
|
7124
|
-
this.ttl = ttl;
|
|
7125
|
-
this.view = new PeerPresenceView(this.#peersLastSeen, this.#peerStates, this.#userPeers, this.#devicePeers);
|
|
7126
|
-
}
|
|
7127
|
-
/**
|
|
7128
|
-
* Record that we've seen the given peer recently.
|
|
7129
|
-
*
|
|
7130
|
-
* @param peerId
|
|
7131
|
-
* @param deviceId
|
|
7132
|
-
* @param userId
|
|
7133
|
-
*/
|
|
7134
|
-
markSeen(peerId, deviceId, userId) {
|
|
7135
|
-
const devicePeers = this.#devicePeers.get(deviceId) ?? new Set();
|
|
7136
|
-
devicePeers.add(peerId);
|
|
7137
|
-
this.#devicePeers.set(deviceId, devicePeers);
|
|
7138
|
-
const userPeers = this.#userPeers.get(userId) ?? new Set();
|
|
7139
|
-
userPeers.add(peerId);
|
|
7140
|
-
this.#userPeers.set(userId, userPeers);
|
|
7141
|
-
this.#peersLastSeen.set(peerId, Date.now());
|
|
7142
|
-
}
|
|
7143
|
-
/**
|
|
7144
|
-
* Record a state update for the given peer. It is also automatically updated with {@link markSeen}.
|
|
7145
|
-
*
|
|
7146
|
-
* @param peerId
|
|
7147
|
-
* @param deviceId
|
|
7148
|
-
* @param userId
|
|
7149
|
-
* @param value
|
|
7150
|
-
*/
|
|
7151
|
-
update({ peerId, deviceId, userId, channel, value, }) {
|
|
7152
|
-
this.markSeen(peerId, deviceId, userId);
|
|
7153
|
-
const peerState = this.#peerStates.get(peerId);
|
|
7154
|
-
const existingState = peerState?.value ?? {};
|
|
7155
|
-
this.#peerStates.set(peerId, {
|
|
7156
|
-
peerId,
|
|
7157
|
-
deviceId,
|
|
7158
|
-
userId,
|
|
7159
|
-
value: {
|
|
7160
|
-
...existingState,
|
|
7161
|
-
[channel]: value,
|
|
7162
|
-
},
|
|
7163
|
-
});
|
|
7164
|
-
}
|
|
7165
|
-
/**
|
|
7166
|
-
* Forget the given peer.
|
|
7167
|
-
*
|
|
7168
|
-
* @param peerId
|
|
7169
|
-
*/
|
|
7170
|
-
delete(peerId) {
|
|
7171
|
-
this.#peersLastSeen.delete(peerId);
|
|
7172
|
-
this.#peerStates.delete(peerId);
|
|
7173
|
-
Array.from(this.#devicePeers.entries()).forEach(([deviceId, peerIds]) => {
|
|
7174
|
-
if (peerIds.has(peerId)) {
|
|
7175
|
-
peerIds.delete(peerId);
|
|
7176
|
-
}
|
|
7177
|
-
if (peerIds.size === 0) {
|
|
7178
|
-
this.#devicePeers.delete(deviceId);
|
|
7179
|
-
}
|
|
7180
|
-
});
|
|
7181
|
-
Array.from(this.#userPeers.entries()).forEach(([userId, peerIds]) => {
|
|
7182
|
-
if (peerIds.has(peerId)) {
|
|
7183
|
-
peerIds.delete(peerId);
|
|
7184
|
-
}
|
|
7185
|
-
if (peerIds.size === 0) {
|
|
7186
|
-
this.#userPeers.delete(userId);
|
|
7187
|
-
}
|
|
7188
|
-
});
|
|
7189
|
-
}
|
|
7190
|
-
/**
|
|
7191
|
-
* Prune all peers that have not been seen since the configured ttl has
|
|
7192
|
-
* elapsed.
|
|
7193
|
-
*/
|
|
7194
|
-
prune() {
|
|
7195
|
-
const threshold = Date.now() - this.ttl;
|
|
7196
|
-
const stalePeers = new Set(Array.from(this.#peersLastSeen.entries())
|
|
7197
|
-
.filter(([, lastSeen]) => {
|
|
7198
|
-
return lastSeen < threshold;
|
|
7199
|
-
})
|
|
7200
|
-
.map(([peerId]) => peerId));
|
|
7201
|
-
if (stalePeers.size === 0) {
|
|
7202
|
-
return;
|
|
7203
|
-
}
|
|
7204
|
-
stalePeers.forEach(stalePeer => {
|
|
7205
|
-
this.delete(stalePeer);
|
|
7206
|
-
});
|
|
7207
|
-
}
|
|
7208
|
-
}
|
|
7209
|
-
|
|
7210
|
-
function useInvalidate() {
|
|
7211
|
-
const [, setState] = useState(0);
|
|
7212
|
-
const increment = useCallback(() => setState((value) => value + 1), [setState]);
|
|
7213
|
-
return increment;
|
|
7214
|
-
}
|
|
7215
7148
|
|
|
7216
7149
|
function usePresence({
|
|
7217
7150
|
handle,
|
|
@@ -7221,7 +7154,8 @@ function usePresence({
|
|
|
7221
7154
|
heartbeatMs,
|
|
7222
7155
|
peerTtlMs
|
|
7223
7156
|
}) {
|
|
7224
|
-
const
|
|
7157
|
+
const [localState, setLocalState] = useState(initialState);
|
|
7158
|
+
const [peerStates, setPeerStates] = useState(new PeerStateView({}));
|
|
7225
7159
|
const firstOpts = useRef({
|
|
7226
7160
|
heartbeatMs,
|
|
7227
7161
|
peerTtlMs
|
|
@@ -7235,25 +7169,37 @@ function usePresence({
|
|
|
7235
7169
|
initialState: firstInitialState.current,
|
|
7236
7170
|
...firstOpts.current
|
|
7237
7171
|
});
|
|
7238
|
-
presence.on("heartbeat",
|
|
7239
|
-
presence.on("snapshot",
|
|
7240
|
-
presence.on("update",
|
|
7241
|
-
presence.on("goodbye",
|
|
7172
|
+
presence.on("heartbeat", () => setPeerStates(presence.getPeerStates()));
|
|
7173
|
+
presence.on("snapshot", () => setPeerStates(presence.getPeerStates()));
|
|
7174
|
+
presence.on("update", () => setPeerStates(presence.getPeerStates()));
|
|
7175
|
+
presence.on("goodbye", () => setPeerStates(presence.getPeerStates()));
|
|
7242
7176
|
return () => {
|
|
7243
7177
|
presence.stop();
|
|
7244
7178
|
};
|
|
7245
7179
|
}, [presence, userId, deviceId, firstInitialState, firstOpts]);
|
|
7246
|
-
const
|
|
7180
|
+
const start = useCallback(() => {
|
|
7181
|
+
presence.start({
|
|
7182
|
+
initialState: presence.getLocalState(),
|
|
7183
|
+
...firstOpts.current
|
|
7184
|
+
});
|
|
7185
|
+
}, [presence, firstOpts]);
|
|
7186
|
+
const stop = useCallback(() => {
|
|
7187
|
+
presence.stop();
|
|
7188
|
+
}, [presence]);
|
|
7189
|
+
const update = useCallback(
|
|
7247
7190
|
(channel, msg) => {
|
|
7248
|
-
invalidate();
|
|
7249
7191
|
presence.broadcast(channel, msg);
|
|
7192
|
+
const updated = presence.getLocalState();
|
|
7193
|
+
setLocalState(updated);
|
|
7250
7194
|
},
|
|
7251
7195
|
[presence]
|
|
7252
7196
|
);
|
|
7253
7197
|
return {
|
|
7254
|
-
peerStates
|
|
7255
|
-
localState
|
|
7256
|
-
update
|
|
7198
|
+
peerStates,
|
|
7199
|
+
localState,
|
|
7200
|
+
update,
|
|
7201
|
+
start,
|
|
7202
|
+
stop
|
|
7257
7203
|
};
|
|
7258
7204
|
}
|
|
7259
7205
|
|