@automerge/automerge-repo 2.5.2-alpha.0 → 2.5.2-alpha.1

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.
@@ -0,0 +1,245 @@
1
+ import { EventEmitter } from "eventemitter3";
2
+ import { DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_PEER_TTL_MS, PRESENCE_MESSAGE_MARKER, } from "./constants.js";
3
+ import { PeerPresenceInfo } from "./PeerPresenceInfo.js";
4
+ /**
5
+ * Presence encapsulates ephemeral state communication for a specific doc
6
+ * handle. It tracks caller-provided local state and broadcasts that state to
7
+ * all peers. It sends periodic heartbeats when there are no state updates.
8
+ *
9
+ * It also tracks ephemeral state broadcast by peers and emits events when peers
10
+ * send ephemeral state updates (see {@link PresenceEvents}).
11
+ *
12
+ * Presence starts out in an inactive state. Call {@link start} and {@link stop}
13
+ * to activate and deactivate it.
14
+ */
15
+ export class Presence extends EventEmitter {
16
+ #handle;
17
+ deviceId;
18
+ userId;
19
+ #peers;
20
+ #localState;
21
+ #heartbeatMs;
22
+ #handleEphemeralMessage;
23
+ #heartbeatInterval;
24
+ #pruningInterval;
25
+ #hellos = [];
26
+ #running = false;
27
+ /**
28
+ * Create a new Presence to share ephemeral state with peers.
29
+ *
30
+ * @param config see {@link PresenceConfig}
31
+ * @returns
32
+ */
33
+ constructor({ handle, deviceId, userId, }) {
34
+ super();
35
+ this.#handle = handle;
36
+ this.#peers = new PeerPresenceInfo(DEFAULT_PEER_TTL_MS);
37
+ this.#localState = {};
38
+ this.userId = userId;
39
+ this.deviceId = deviceId;
40
+ }
41
+ /**
42
+ * Start listening to ephemeral messages on the handle, broadcast initial
43
+ * state to peers, and start sending heartbeats.
44
+ */
45
+ start({ initialState, heartbeatMs, peerTtlMs }) {
46
+ if (this.#running) {
47
+ return;
48
+ }
49
+ this.#running = true;
50
+ this.#heartbeatMs = heartbeatMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
51
+ this.#peers = new PeerPresenceInfo(peerTtlMs ?? DEFAULT_PEER_TTL_MS);
52
+ this.#localState = initialState;
53
+ // N.B.: We can't use a regular member function here since member functions
54
+ // of two distinct objects are identical, and we need to be able to stop
55
+ // listening to the handle for just this Presence instance in stop()
56
+ this.#handleEphemeralMessage = (e) => {
57
+ const peerId = e.senderId;
58
+ const envelope = e.message;
59
+ if (!(PRESENCE_MESSAGE_MARKER in envelope)) {
60
+ return;
61
+ }
62
+ const message = envelope[PRESENCE_MESSAGE_MARKER];
63
+ const { deviceId, userId } = message;
64
+ if (!this.#peers.has(peerId)) {
65
+ this.announce();
66
+ }
67
+ switch (message.type) {
68
+ case "heartbeat":
69
+ this.#peers.markSeen(peerId);
70
+ this.emit("heartbeat", { type: "heartbeat", peerId });
71
+ break;
72
+ case "goodbye":
73
+ this.#peers.delete(peerId);
74
+ this.emit("goodbye", { type: "goodbye", peerId });
75
+ break;
76
+ case "update":
77
+ this.#peers.update({
78
+ peerId,
79
+ deviceId,
80
+ userId,
81
+ value: { [message.channel]: message.value },
82
+ });
83
+ this.emit("update", {
84
+ type: "update",
85
+ peerId,
86
+ deviceId,
87
+ userId,
88
+ channel: message.channel,
89
+ value: message.value,
90
+ });
91
+ break;
92
+ case "snapshot":
93
+ this.#peers.update({
94
+ peerId,
95
+ deviceId,
96
+ userId,
97
+ value: message.state,
98
+ });
99
+ this.emit("snapshot", {
100
+ type: "snapshot",
101
+ peerId,
102
+ deviceId,
103
+ userId,
104
+ state: message.state,
105
+ });
106
+ break;
107
+ }
108
+ };
109
+ this.#handle.on("ephemeral-message", this.#handleEphemeralMessage);
110
+ this.broadcastLocalState(); // also starts heartbeats
111
+ this.startPruningPeers();
112
+ }
113
+ /**
114
+ * Return a view of current peer states.
115
+ */
116
+ getPeerStates() {
117
+ return this.#peers.states;
118
+ }
119
+ /**
120
+ * Return a view of current local state.
121
+ */
122
+ getLocalState() {
123
+ return this.#localState;
124
+ }
125
+ /**
126
+ * Update state for the specific channel, and broadcast new state to all
127
+ * peers.
128
+ *
129
+ * @param channel
130
+ * @param value
131
+ */
132
+ broadcast(channel, value) {
133
+ this.#localState = Object.assign({}, this.#localState, {
134
+ [channel]: value,
135
+ });
136
+ this.broadcastChannelState(channel);
137
+ }
138
+ /**
139
+ * Whether this Presence is currently active. See
140
+ * {@link start} and {@link stop}.
141
+ */
142
+ get running() {
143
+ return this.#running;
144
+ }
145
+ /**
146
+ * Stop this Presence: broadcast a "goodbye" message (when received, other
147
+ * peers will immediately forget the sender), stop sending heartbeats, and
148
+ * stop listening to ephemeral-messages broadcast from peers.
149
+ *
150
+ * This can be used with browser events like
151
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event | "pagehide"}
152
+ * or
153
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event | "visibilitychange"}
154
+ * to stop sending and receiving updates when not active.
155
+ */
156
+ stop() {
157
+ if (!this.#running) {
158
+ return;
159
+ }
160
+ this.#hellos.forEach(timeoutId => {
161
+ clearTimeout(timeoutId);
162
+ });
163
+ this.#hellos = [];
164
+ this.#handle.off("ephemeral-message", this.#handleEphemeralMessage);
165
+ this.stopHeartbeats();
166
+ this.stopPruningPeers();
167
+ this.send({ type: "goodbye" });
168
+ this.#running = false;
169
+ }
170
+ announce() {
171
+ // Broadcast our current state whenever we see new peers
172
+ // TODO: We currently need to wait for the peer to be ready, but waiting
173
+ // some arbitrary amount of time is brittle
174
+ const helloId = setTimeout(() => {
175
+ this.broadcastLocalState();
176
+ this.#hellos = this.#hellos.filter(id => id !== helloId);
177
+ }, 500);
178
+ this.#hellos.push(helloId);
179
+ }
180
+ broadcastLocalState() {
181
+ this.doBroadcast("snapshot", { state: this.#localState });
182
+ this.resetHeartbeats();
183
+ }
184
+ broadcastChannelState(channel) {
185
+ const value = this.#localState[channel];
186
+ this.doBroadcast("update", { channel, value });
187
+ this.resetHeartbeats();
188
+ }
189
+ resetHeartbeats() {
190
+ // Reset heartbeats every time we broadcast a message to avoid sending
191
+ // unnecessary heartbeats when there is plenty of actual update activity
192
+ // happening.
193
+ this.stopHeartbeats();
194
+ this.startHeartbeats();
195
+ }
196
+ doBroadcast(type, extra) {
197
+ this.send({
198
+ userId: this.userId,
199
+ deviceId: this.deviceId,
200
+ type,
201
+ ...extra,
202
+ });
203
+ }
204
+ send(message) {
205
+ if (!this.#running) {
206
+ return;
207
+ }
208
+ this.#handle.broadcast({
209
+ [PRESENCE_MESSAGE_MARKER]: message,
210
+ });
211
+ }
212
+ startHeartbeats() {
213
+ if (this.#heartbeatInterval !== undefined) {
214
+ return;
215
+ }
216
+ this.#heartbeatInterval = setInterval(() => {
217
+ this.send({ type: "heartbeat" });
218
+ }, this.#heartbeatMs);
219
+ }
220
+ stopHeartbeats() {
221
+ if (this.#heartbeatInterval === undefined) {
222
+ return;
223
+ }
224
+ clearInterval(this.#heartbeatInterval);
225
+ this.#heartbeatInterval = undefined;
226
+ }
227
+ startPruningPeers() {
228
+ if (this.#pruningInterval !== undefined) {
229
+ return;
230
+ }
231
+ // Pruning happens at the heartbeat frequency, not on a peer ttl frequency,
232
+ // to minimize variance between peer expiration, since the heartbeat frequency
233
+ // is expected to be several times higher.
234
+ this.#pruningInterval = setInterval(() => {
235
+ this.#peers.prune();
236
+ }, this.#heartbeatMs);
237
+ }
238
+ stopPruningPeers() {
239
+ if (this.#pruningInterval === undefined) {
240
+ return;
241
+ }
242
+ clearInterval(this.#pruningInterval);
243
+ this.#pruningInterval = undefined;
244
+ }
245
+ }
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
2
+ export declare const DEFAULT_PEER_TTL_MS: number;
3
+ export declare const PRESENCE_MESSAGE_MARKER = "__presence";
4
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/presence/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,6BAA6B,QAAS,CAAA;AACnD,eAAO,MAAM,mBAAmB,QAAoC,CAAA;AACpE,eAAO,MAAM,uBAAuB,eAAe,CAAA"}
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
2
+ export const DEFAULT_PEER_TTL_MS = 3 * DEFAULT_HEARTBEAT_INTERVAL_MS;
3
+ export const PRESENCE_MESSAGE_MARKER = "__presence";
@@ -0,0 +1,75 @@
1
+ import { PeerId } from "../types.js";
2
+ import { PRESENCE_MESSAGE_MARKER } from "./constants.js";
3
+ export type UserId = unknown;
4
+ export type DeviceId = unknown;
5
+ export type PresenceState = Record<string, any>;
6
+ export type PeerStatesValue<State extends PresenceState> = Record<PeerId, PeerState<State>>;
7
+ export type PeerState<State extends PresenceState> = {
8
+ peerId: PeerId;
9
+ lastActiveAt: number;
10
+ lastUpdateAt: number;
11
+ deviceId?: DeviceId;
12
+ userId?: UserId;
13
+ value: State;
14
+ };
15
+ type PresenceMessageBase = {
16
+ deviceId?: DeviceId;
17
+ userId?: UserId;
18
+ };
19
+ type PresenceMessageUpdate = PresenceMessageBase & {
20
+ type: "update";
21
+ channel: string;
22
+ value: any;
23
+ };
24
+ type PresenceMessageSnapshot = PresenceMessageBase & {
25
+ type: "snapshot";
26
+ state: any;
27
+ };
28
+ type PresenceMessageHeartbeat = PresenceMessageBase & {
29
+ type: "heartbeat";
30
+ };
31
+ type PresenceMessageGoodbye = PresenceMessageBase & {
32
+ type: "goodbye";
33
+ };
34
+ export type PresenceMessage = {
35
+ [PRESENCE_MESSAGE_MARKER]: PresenceMessageUpdate | PresenceMessageSnapshot | PresenceMessageHeartbeat | PresenceMessageGoodbye;
36
+ };
37
+ export type PresenceMessageType = PresenceMessage[typeof PRESENCE_MESSAGE_MARKER]["type"];
38
+ type WithPeerId = {
39
+ peerId: PeerId;
40
+ };
41
+ export type PresenceEventUpdate = PresenceMessageUpdate & WithPeerId;
42
+ export type PresenceEventSnapshot = PresenceMessageSnapshot & WithPeerId;
43
+ export type PresenceEventHeartbeat = PresenceMessageHeartbeat & WithPeerId;
44
+ export type PresenceEventGoodbye = PresenceMessageGoodbye & WithPeerId;
45
+ /**
46
+ * Events emitted by Presence when ephemeral messages are received from peers.
47
+ */
48
+ export type PresenceEvents = {
49
+ /**
50
+ * Handle a state update broadcast by a peer.
51
+ */
52
+ update: (msg: PresenceEventUpdate) => void;
53
+ /**
54
+ * Handle a full state snapshot broadcast by a peer.
55
+ */
56
+ snapshot: (msg: PresenceEventSnapshot) => void;
57
+ /**
58
+ * Handle a heartbeat broadcast by a peer.
59
+ */
60
+ heartbeat: (msg: PresenceEventHeartbeat) => void;
61
+ /**
62
+ * Handle a disconnection broadcast by a peer.
63
+ */
64
+ goodbye: (msg: PresenceEventGoodbye) => void;
65
+ };
66
+ export type PresenceConfig<State extends PresenceState> = {
67
+ /** The full initial state to broadcast to peers */
68
+ initialState: State;
69
+ /** How frequently to send heartbeats (default {@link DEFAULT_HEARTBEAT_INTERVAL_MS}) */
70
+ heartbeatMs?: number;
71
+ /** How long to wait until forgetting peers with no activity (default {@link DEFAULT_PEER_TTL_MS}) */
72
+ peerTtlMs?: number;
73
+ };
74
+ export {};
75
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/presence/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAExD,MAAM,MAAM,MAAM,GAAG,OAAO,CAAA;AAC5B,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAA;AAE9B,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAE/C,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,aAAa,IAAI,MAAM,CAC/D,MAAM,EACN,SAAS,CAAC,KAAK,CAAC,CACjB,CAAA;AAED,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,aAAa,IAAI;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,KAAK,CAAA;CACb,CAAA;AAED,KAAK,mBAAmB,GAAG;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,KAAK,qBAAqB,GAAG,mBAAmB,GAAG;IACjD,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,KAAK,uBAAuB,GAAG,mBAAmB,GAAG;IACnD,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,KAAK,wBAAwB,GAAG,mBAAmB,GAAG;IACpD,IAAI,EAAE,WAAW,CAAA;CAClB,CAAA;AAED,KAAK,sBAAsB,GAAG,mBAAmB,GAAG;IAClD,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,uBAAuB,CAAC,EACrB,qBAAqB,GACrB,uBAAuB,GACvB,wBAAwB,GACxB,sBAAsB,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAC7B,eAAe,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,KAAK,UAAU,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpC,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG,UAAU,CAAA;AACpE,MAAM,MAAM,qBAAqB,GAAG,uBAAuB,GAAG,UAAU,CAAA;AACxE,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,GAAG,UAAU,CAAA;AAC1E,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GAAG,UAAU,CAAA;AAEtE;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;OAEG;IACH,MAAM,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAC1C;;OAEG;IACH,QAAQ,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,CAAA;IAC9C;;OAEG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,IAAI,CAAA;IAChD;;OAEG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE,oBAAoB,KAAK,IAAI,CAAA;CAC7C,CAAA;AAED,MAAM,MAAM,cAAc,CAAC,KAAK,SAAS,aAAa,IAAI;IACxD,mDAAmD;IACnD,YAAY,EAAE,KAAK,CAAA;IACnB,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sGAAsG;IACtG,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA"}
@@ -0,0 +1 @@
1
+ import { PRESENCE_MESSAGE_MARKER } from "./constants.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "2.5.2-alpha.0",
3
+ "version": "2.5.2-alpha.1",
4
4
  "description": "A repository object to manage a collection of automerge documents",
5
5
  "repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo",
6
6
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -59,5 +59,5 @@
59
59
  "publishConfig": {
60
60
  "access": "public"
61
61
  },
62
- "gitHead": "ba4493efcd7819fe841d3647f28090837792d964"
62
+ "gitHead": "96ab95c2e84b6bd4d8c3a2852368cb35d7ed1b52"
63
63
  }
@@ -0,0 +1,3 @@
1
+ export function unique<T>(items: T[]) {
2
+ return Array.from(new Set(items))
3
+ }
package/src/index.ts CHANGED
@@ -39,14 +39,16 @@ export {
39
39
  decodeHeads,
40
40
  } from "./AutomergeUrl.js"
41
41
  export { Repo } from "./Repo.js"
42
- export {
43
- Presence,
44
- PeerPresenceView,
42
+ export { Presence } from "./presence/Presence.js"
43
+ export { PeerStateView } from "./presence/PeerStateView.js"
44
+ export type {
45
45
  PeerState,
46
+ PresenceState,
46
47
  PresenceConfig,
47
48
  UserId,
48
49
  DeviceId,
49
- } from "./Presence.js"
50
+ } from "./presence/types.js"
51
+
50
52
  export { NetworkAdapter } from "./network/NetworkAdapter.js"
51
53
  export type { NetworkAdapterInterface } from "./network/NetworkAdapterInterface.js"
52
54
  export { isRepoMessage } from "./network/messages.js"
@@ -0,0 +1,108 @@
1
+ import { PeerId } from "../types.js"
2
+ import { PeerStateView } from "./PeerStateView.js"
3
+ import { DeviceId, PresenceState, UserId } from "./types.js"
4
+
5
+ export class PeerPresenceInfo<State extends PresenceState> {
6
+ #peerStates = new PeerStateView<State>({})
7
+
8
+ /**
9
+ * Build a new peer presence state.
10
+ *
11
+ * @param ttl in milliseconds - peers with no activity within this timeframe
12
+ * are forgotten when {@link prune} is called.
13
+ */
14
+ constructor(readonly ttl: number) {}
15
+
16
+ has(peerId: PeerId) {
17
+ return peerId in this.#peerStates.value
18
+ }
19
+
20
+ /**
21
+ * Record that we've seen the given peer recently.
22
+ *
23
+ * @param peerId
24
+ */
25
+ markSeen(peerId: PeerId) {
26
+ this.#peerStates = new PeerStateView<State>({
27
+ ...this.#peerStates.value,
28
+ [peerId]: {
29
+ ...this.#peerStates.value[peerId],
30
+ lastSeen: Date.now(),
31
+ },
32
+ })
33
+ }
34
+
35
+ /**
36
+ * Record a state update for the given peer. Note that existing state is not
37
+ * overwritten.
38
+ *
39
+ * @param peerId
40
+ * @param value
41
+ */
42
+ update({
43
+ peerId,
44
+ deviceId,
45
+ userId,
46
+ value,
47
+ }: {
48
+ peerId: PeerId
49
+ deviceId?: DeviceId
50
+ userId?: UserId
51
+ value: Partial<State>
52
+ }) {
53
+ const peerState = this.#peerStates.value[peerId]
54
+ const existingState = peerState?.value ?? ({} as State)
55
+ const now = Date.now()
56
+ this.#peerStates = new PeerStateView<State>({
57
+ ...this.#peerStates.value,
58
+ [peerId]: {
59
+ peerId,
60
+ deviceId,
61
+ userId,
62
+ lastActiveAt: now,
63
+ lastUpdateAt: now,
64
+ value: {
65
+ ...existingState,
66
+ ...value,
67
+ },
68
+ },
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Forget the given peer.
74
+ *
75
+ * @param peerId
76
+ */
77
+ delete(peerId: PeerId) {
78
+ this.#peerStates = new PeerStateView<State>(
79
+ Object.fromEntries(
80
+ Object.entries(this.#peerStates.value).filter(([existingId]) => {
81
+ return existingId != peerId
82
+ })
83
+ )
84
+ )
85
+ }
86
+
87
+ /**
88
+ * Prune all peers that have not been seen since the configured ttl has
89
+ * elapsed.
90
+ */
91
+ prune() {
92
+ const threshold = Date.now() - this.ttl
93
+ this.#peerStates = new PeerStateView<State>(
94
+ Object.fromEntries(
95
+ Object.entries(this.#peerStates).filter(([, state]) => {
96
+ return state.lastActiveAt >= threshold
97
+ })
98
+ )
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Get a snapshot of the current peer states
104
+ */
105
+ get states() {
106
+ return this.#peerStates
107
+ }
108
+ }
@@ -0,0 +1,154 @@
1
+ import { unique } from "../helpers/array.js"
2
+ import { PeerId } from "../types.js"
3
+ import { DeviceId, PeerStatesValue, PresenceState, UserId } from "./types.js"
4
+
5
+ export class PeerStateView<State extends PresenceState> {
6
+ readonly value
7
+
8
+ constructor(value: PeerStatesValue<State>) {
9
+ this.value = value
10
+ }
11
+
12
+ /**
13
+ * Get all users.
14
+ *
15
+ * @returns Array of user presence {@link State}s
16
+ */
17
+ get users() {
18
+ const userIds = unique(
19
+ Object.values(this.value).map(peerState => peerState.userId)
20
+ )
21
+ return userIds.map(u => this.getUserState(u))
22
+ }
23
+
24
+ /**
25
+ * Get all devices.
26
+ *
27
+ * @returns Array of device presence {@link State}s
28
+ */
29
+ get devices() {
30
+ const deviceIds = unique(
31
+ Object.values(this.value).map(peerState => peerState.deviceId)
32
+ )
33
+ return deviceIds.map(d => this.getDeviceState(d))
34
+ }
35
+
36
+ /**
37
+ * Get all peers.
38
+ *
39
+ * @returns Array of peer presence {@link State}s
40
+ */
41
+ get peers() {
42
+ return Object.values(this.value)
43
+ }
44
+
45
+ /**
46
+ * Get all peer ids for this user.
47
+ *
48
+ * @param userId
49
+ * @returns Array of peer ids for this user
50
+ */
51
+ getUserPeers(userId: UserId) {
52
+ return Object.values(this.value)
53
+ .filter(peerState => peerState.userId === userId)
54
+ .map(peerState => peerState.peerId)
55
+ }
56
+
57
+ /**
58
+ * Get all peers for this device.
59
+ *
60
+ * @param deviceId
61
+ * @returns Array of peer ids for this device
62
+ */
63
+ getDevicePeers(deviceId: DeviceId) {
64
+ return Object.values(this.value)
65
+ .filter(peerState => peerState.deviceId === deviceId)
66
+ .map(peerState => peerState.peerId)
67
+ }
68
+
69
+ /**
70
+ * Return the most-recently-seen peer from this group.
71
+ *
72
+ * @param peers
73
+ * @returns id of most recently seen peer
74
+ */
75
+ getLastSeenPeer(peers: PeerId[]) {
76
+ let freshestLastSeenAt: number
77
+ return peers.reduce((freshest: PeerId | undefined, curr) => {
78
+ const lastSeenAt = this.value[curr]?.lastActiveAt
79
+ if (!lastSeenAt) {
80
+ return freshest
81
+ }
82
+
83
+ if (!freshest || lastSeenAt > freshestLastSeenAt) {
84
+ freshestLastSeenAt = lastSeenAt
85
+ return curr
86
+ }
87
+
88
+ return freshest
89
+ }, undefined)
90
+ }
91
+
92
+ /**
93
+ * Return the peer from this group that sent a state update most recently
94
+ *
95
+ * @param peers
96
+ * @returns id of most recently seen peer
97
+ */
98
+ getLastActivePeer(peers: PeerId[]) {
99
+ let freshestLastActiveAt: number
100
+ return peers.reduce((freshest: PeerId | undefined, curr) => {
101
+ const lastActiveAt = this.value[curr]?.lastActiveAt
102
+ if (!lastActiveAt) {
103
+ return freshest
104
+ }
105
+
106
+ if (!freshest || lastActiveAt > freshestLastActiveAt) {
107
+ freshestLastActiveAt = lastActiveAt
108
+ return curr
109
+ }
110
+
111
+ return freshest
112
+ }, undefined)
113
+ }
114
+
115
+ /**
116
+ * Get current ephemeral state value for this user's most-recently-active
117
+ * peer.
118
+ *
119
+ * @param userId
120
+ * @returns user's {@link State}
121
+ */
122
+ getUserState(userId: UserId) {
123
+ const peers = this.getUserPeers(userId)
124
+ if (!peers) {
125
+ return undefined
126
+ }
127
+ const peer = this.getLastActivePeer(peers)
128
+ if (!peer) {
129
+ return undefined
130
+ }
131
+
132
+ return this.value[peer]
133
+ }
134
+
135
+ /**
136
+ * Get current ephemeral state value for this device's most-recently-active
137
+ * peer.
138
+ *
139
+ * @param deviceId
140
+ * @returns device's {@link State}
141
+ */
142
+ getDeviceState(deviceId: DeviceId) {
143
+ const peers = this.getDevicePeers(deviceId)
144
+ if (!peers) {
145
+ return undefined
146
+ }
147
+ const peer = this.getLastActivePeer(peers)
148
+ if (!peer) {
149
+ return undefined
150
+ }
151
+
152
+ return this.value[peer]
153
+ }
154
+ }