@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.
- package/dist/helpers/array.d.ts +2 -0
- package/dist/helpers/array.d.ts.map +1 -0
- package/dist/helpers/array.js +3 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/presence/PeerPresenceInfo.d.ts +50 -0
- package/dist/presence/PeerPresenceInfo.d.ts.map +1 -0
- package/dist/presence/PeerPresenceInfo.js +83 -0
- package/dist/presence/PeerStateView.d.ts +69 -0
- package/dist/presence/PeerStateView.d.ts.map +1 -0
- package/dist/presence/PeerStateView.js +131 -0
- package/dist/presence/Presence.d.ts +81 -0
- package/dist/presence/Presence.d.ts.map +1 -0
- package/dist/presence/Presence.js +245 -0
- package/dist/presence/constants.d.ts +4 -0
- package/dist/presence/constants.d.ts.map +1 -0
- package/dist/presence/constants.js +3 -0
- package/dist/presence/types.d.ts +75 -0
- package/dist/presence/types.d.ts.map +1 -0
- package/dist/presence/types.js +1 -0
- package/package.json +2 -2
- package/src/helpers/array.ts +3 -0
- package/src/index.ts +6 -4
- package/src/presence/PeerPresenceInfo.ts +108 -0
- package/src/presence/PeerStateView.ts +154 -0
- package/src/presence/Presence.ts +311 -0
- package/src/presence/constants.ts +3 -0
- package/src/presence/types.ts +94 -0
- package/test/Presence.test.ts +20 -26
- package/dist/Presence.d.ts +0 -245
- package/dist/Presence.d.ts.map +0 -1
- package/dist/Presence.js +0 -526
- package/src/Presence.ts +0 -722
package/src/Presence.ts
DELETED
|
@@ -1,722 +0,0 @@
|
|
|
1
|
-
import { DocHandle, DocHandleEphemeralMessagePayload } from "./DocHandle.js"
|
|
2
|
-
import { PeerId } from "./types.js"
|
|
3
|
-
import { EventEmitter } from "eventemitter3"
|
|
4
|
-
|
|
5
|
-
export type UserId = unknown
|
|
6
|
-
export type DeviceId = unknown
|
|
7
|
-
|
|
8
|
-
export const PRESENCE_MESSAGE_MARKER = "__presence"
|
|
9
|
-
|
|
10
|
-
export type PeerState<State> = {
|
|
11
|
-
peerId: PeerId
|
|
12
|
-
deviceId?: DeviceId
|
|
13
|
-
userId?: UserId
|
|
14
|
-
value: State
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type PresenceMessageBase = {
|
|
18
|
-
deviceId?: DeviceId
|
|
19
|
-
userId?: UserId
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type PresenceMessageUpdate = PresenceMessageBase & {
|
|
23
|
-
type: "update"
|
|
24
|
-
channel: string
|
|
25
|
-
value: any
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type PresenceMessageSnapshot = PresenceMessageBase & {
|
|
29
|
-
type: "snapshot"
|
|
30
|
-
state: any
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type PresenceMessageHeartbeat = PresenceMessageBase & {
|
|
34
|
-
type: "heartbeat"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type PresenceMessageGoodbye = PresenceMessageBase & {
|
|
38
|
-
type: "goodbye"
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type PresenceMessage = {
|
|
42
|
-
[PRESENCE_MESSAGE_MARKER]:
|
|
43
|
-
| PresenceMessageUpdate
|
|
44
|
-
| PresenceMessageSnapshot
|
|
45
|
-
| PresenceMessageHeartbeat
|
|
46
|
-
| PresenceMessageGoodbye
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type PresenceMessageType =
|
|
50
|
-
PresenceMessage[typeof PRESENCE_MESSAGE_MARKER]["type"]
|
|
51
|
-
|
|
52
|
-
type WithPeerId = { peerId: PeerId }
|
|
53
|
-
|
|
54
|
-
export type PresenceEventUpdate = PresenceMessageUpdate & WithPeerId
|
|
55
|
-
export type PresenceEventSnapshot = PresenceMessageSnapshot & WithPeerId
|
|
56
|
-
export type PresenceEventHeartbeat = PresenceMessageHeartbeat & WithPeerId
|
|
57
|
-
export type PresenceEventGoodbye = PresenceMessageGoodbye & WithPeerId
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Events emitted by Presence when ephemeral messages are received from peers.
|
|
61
|
-
*/
|
|
62
|
-
export type PresenceEvents = {
|
|
63
|
-
/**
|
|
64
|
-
* Handle a state update broadcast by a peer.
|
|
65
|
-
*/
|
|
66
|
-
update: (msg: PresenceEventUpdate) => void
|
|
67
|
-
/**
|
|
68
|
-
* Handle a full state snapshot broadcast by a peer.
|
|
69
|
-
*/
|
|
70
|
-
snapshot: (msg: PresenceEventSnapshot) => void
|
|
71
|
-
/**
|
|
72
|
-
* Handle a heartbeat broadcast by a peer.
|
|
73
|
-
*/
|
|
74
|
-
heartbeat: (msg: PresenceEventHeartbeat) => void
|
|
75
|
-
/**
|
|
76
|
-
* Handle a disconnection broadcast by a peer.
|
|
77
|
-
*/
|
|
78
|
-
goodbye: (msg: PresenceEventGoodbye) => void
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000
|
|
82
|
-
export const DEFAULT_PEER_TTL_MS = 3 * DEFAULT_HEARTBEAT_INTERVAL_MS
|
|
83
|
-
|
|
84
|
-
export type PresenceConfig<State> = {
|
|
85
|
-
/** The full initial state to broadcast to peers */
|
|
86
|
-
initialState: State
|
|
87
|
-
/** How frequently to send heartbeats (default {@link DEFAULT_HEARTBEAT_INTERVAL_MS}) */
|
|
88
|
-
heartbeatMs?: number
|
|
89
|
-
/** How long to wait until forgetting peers with no activity (default {@link DEFAULT_PEER_TTL_MS}) */
|
|
90
|
-
peerTtlMs?: number
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Presence encapsulates ephemeral state communication for a specific doc
|
|
95
|
-
* handle. It tracks caller-provided local state and broadcasts that state to
|
|
96
|
-
* all peers. It sends periodic heartbeats when there are no state updates.
|
|
97
|
-
*
|
|
98
|
-
* It also tracks ephemeral state broadcast by peers and emits events when peers
|
|
99
|
-
* send ephemeral state updates (see {@link PresenceEvents}).
|
|
100
|
-
*
|
|
101
|
-
* Presence starts out in an inactive state. Call {@link start} and {@link stop}
|
|
102
|
-
* to activate and deactivate it.
|
|
103
|
-
*/
|
|
104
|
-
export class Presence<
|
|
105
|
-
State extends Record<string, any>,
|
|
106
|
-
DocType = any
|
|
107
|
-
> extends EventEmitter<PresenceEvents> {
|
|
108
|
-
#handle: DocHandle<DocType>
|
|
109
|
-
readonly deviceId?: DeviceId
|
|
110
|
-
readonly userId?: UserId
|
|
111
|
-
#peers: PeerPresenceInfo<State>
|
|
112
|
-
#localState?: State
|
|
113
|
-
#heartbeatMs?: number
|
|
114
|
-
|
|
115
|
-
#handleEphemeralMessage:
|
|
116
|
-
| ((e: DocHandleEphemeralMessagePayload<DocType>) => void)
|
|
117
|
-
| undefined
|
|
118
|
-
|
|
119
|
-
#heartbeatInterval: ReturnType<typeof setInterval> | undefined
|
|
120
|
-
#pruningInterval: ReturnType<typeof setInterval> | undefined
|
|
121
|
-
#hellos: ReturnType<typeof setTimeout>[] = []
|
|
122
|
-
|
|
123
|
-
#running = false
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Create a new Presence to share ephemeral state with peers.
|
|
127
|
-
*
|
|
128
|
-
* @param config see {@link PresenceConfig}
|
|
129
|
-
* @returns
|
|
130
|
-
*/
|
|
131
|
-
constructor({
|
|
132
|
-
handle,
|
|
133
|
-
deviceId,
|
|
134
|
-
userId,
|
|
135
|
-
}: {
|
|
136
|
-
handle: DocHandle<DocType>
|
|
137
|
-
/** Our device id (like userId, this is unverified; peers can send anything) */
|
|
138
|
-
deviceId?: DeviceId
|
|
139
|
-
/** Our user id (this is unverified; peers can send anything) */
|
|
140
|
-
userId?: UserId
|
|
141
|
-
}) {
|
|
142
|
-
super()
|
|
143
|
-
this.#handle = handle
|
|
144
|
-
this.#peers = new PeerPresenceInfo(DEFAULT_PEER_TTL_MS)
|
|
145
|
-
this.userId = userId
|
|
146
|
-
this.deviceId = deviceId
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Start listening to ephemeral messages on the handle, broadcast initial
|
|
151
|
-
* state to peers, and start sending heartbeats.
|
|
152
|
-
*/
|
|
153
|
-
start({ initialState, heartbeatMs, peerTtlMs }: PresenceConfig<State>) {
|
|
154
|
-
if (this.#running) {
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
this.#running = true
|
|
158
|
-
|
|
159
|
-
this.#heartbeatMs = heartbeatMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS
|
|
160
|
-
this.#peers = new PeerPresenceInfo(peerTtlMs ?? DEFAULT_PEER_TTL_MS)
|
|
161
|
-
this.#localState = initialState
|
|
162
|
-
|
|
163
|
-
// N.B.: We can't use a regular member function here since member functions
|
|
164
|
-
// of two distinct objects are identical, and we need to be able to stop
|
|
165
|
-
// listening to the handle for just this Presence instance in stop()
|
|
166
|
-
this.#handleEphemeralMessage = (
|
|
167
|
-
e: DocHandleEphemeralMessagePayload<DocType>
|
|
168
|
-
) => {
|
|
169
|
-
const peerId = e.senderId
|
|
170
|
-
const envelope = e.message as PresenceMessage
|
|
171
|
-
|
|
172
|
-
if (!(PRESENCE_MESSAGE_MARKER in envelope)) {
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const message = envelope[PRESENCE_MESSAGE_MARKER]
|
|
177
|
-
const { deviceId, userId } = message
|
|
178
|
-
|
|
179
|
-
if (!this.#peers.view.has(peerId)) {
|
|
180
|
-
this.announce()
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
switch (message.type) {
|
|
184
|
-
case "heartbeat":
|
|
185
|
-
this.#peers.markSeen(peerId, deviceId, userId)
|
|
186
|
-
this.emit("heartbeat", {
|
|
187
|
-
type: "heartbeat",
|
|
188
|
-
peerId,
|
|
189
|
-
deviceId,
|
|
190
|
-
userId,
|
|
191
|
-
})
|
|
192
|
-
break
|
|
193
|
-
case "goodbye":
|
|
194
|
-
this.#peers.delete(peerId)
|
|
195
|
-
this.emit("goodbye", {
|
|
196
|
-
type: "goodbye",
|
|
197
|
-
peerId,
|
|
198
|
-
deviceId,
|
|
199
|
-
userId,
|
|
200
|
-
})
|
|
201
|
-
break
|
|
202
|
-
case "update":
|
|
203
|
-
this.#peers.update({
|
|
204
|
-
peerId,
|
|
205
|
-
deviceId,
|
|
206
|
-
userId,
|
|
207
|
-
channel: message.channel as keyof State,
|
|
208
|
-
value: message.value,
|
|
209
|
-
})
|
|
210
|
-
this.emit("update", {
|
|
211
|
-
type: "update",
|
|
212
|
-
peerId,
|
|
213
|
-
deviceId,
|
|
214
|
-
userId,
|
|
215
|
-
channel: message.channel,
|
|
216
|
-
value: message.value,
|
|
217
|
-
})
|
|
218
|
-
break
|
|
219
|
-
case "snapshot":
|
|
220
|
-
Object.entries(message.state as State).forEach(([channel, value]) => {
|
|
221
|
-
this.#peers.update({
|
|
222
|
-
peerId,
|
|
223
|
-
deviceId,
|
|
224
|
-
userId,
|
|
225
|
-
channel: channel as keyof State,
|
|
226
|
-
value,
|
|
227
|
-
})
|
|
228
|
-
})
|
|
229
|
-
this.emit("snapshot", {
|
|
230
|
-
type: "snapshot",
|
|
231
|
-
peerId,
|
|
232
|
-
deviceId,
|
|
233
|
-
userId,
|
|
234
|
-
state: message.state,
|
|
235
|
-
})
|
|
236
|
-
break
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
this.#handle.on("ephemeral-message", this.#handleEphemeralMessage)
|
|
240
|
-
|
|
241
|
-
this.broadcastLocalState() // also starts heartbeats
|
|
242
|
-
this.startPruningPeers()
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Return a view of current peer states.
|
|
247
|
-
*/
|
|
248
|
-
getPeerStates() {
|
|
249
|
-
return this.#peers.view
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Return a view of current local state.
|
|
254
|
-
*/
|
|
255
|
-
getLocalState() {
|
|
256
|
-
return this.#localState
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Update state for the specific channel, and broadcast new state to all
|
|
261
|
-
* peers.
|
|
262
|
-
*
|
|
263
|
-
* @param channel
|
|
264
|
-
* @param value
|
|
265
|
-
*/
|
|
266
|
-
broadcast<Channel extends keyof State>(
|
|
267
|
-
channel: Channel,
|
|
268
|
-
value: State[Channel]
|
|
269
|
-
) {
|
|
270
|
-
this.#localState = Object.assign({}, this.#localState, {
|
|
271
|
-
[channel]: value,
|
|
272
|
-
})
|
|
273
|
-
this.broadcastChannelState(channel, value)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Whether this Presence is currently active. See
|
|
278
|
-
* {@link start} and {@link stop}.
|
|
279
|
-
*/
|
|
280
|
-
get running() {
|
|
281
|
-
return this.#running
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Stop this Presence: broadcast a "goodbye" message (when received, other
|
|
286
|
-
* peers will immediately forget the sender), stop sending heartbeats, and
|
|
287
|
-
* stop listening to ephemeral-messages broadcast from peers.
|
|
288
|
-
*
|
|
289
|
-
* This can be used with browser events like
|
|
290
|
-
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event | "pagehide"}
|
|
291
|
-
* or
|
|
292
|
-
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event | "visibilitychange"}
|
|
293
|
-
* to stop sending and receiving updates when not active.
|
|
294
|
-
*/
|
|
295
|
-
stop() {
|
|
296
|
-
if (!this.#running) {
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
this.#hellos.forEach(timeoutId => {
|
|
300
|
-
clearTimeout(timeoutId)
|
|
301
|
-
})
|
|
302
|
-
this.#hellos = []
|
|
303
|
-
this.#handle.off("ephemeral-message", this.#handleEphemeralMessage)
|
|
304
|
-
this.stopHeartbeats()
|
|
305
|
-
this.stopPruningPeers()
|
|
306
|
-
this.doBroadcast("goodbye")
|
|
307
|
-
this.#running = false
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
private announce() {
|
|
311
|
-
// Broadcast our current state whenever we see new peers
|
|
312
|
-
// TODO: We currently need to wait for the peer to be ready, but waiting
|
|
313
|
-
// some arbitrary amount of time is brittle
|
|
314
|
-
const helloId = setTimeout(() => {
|
|
315
|
-
this.broadcastLocalState()
|
|
316
|
-
this.#hellos = this.#hellos.filter(id => id !== helloId)
|
|
317
|
-
}, 500)
|
|
318
|
-
this.#hellos.push(helloId)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private broadcastLocalState() {
|
|
322
|
-
this.doBroadcast("snapshot", { state: this.#localState })
|
|
323
|
-
this.resetHeartbeats()
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private broadcastChannelState<Channel extends keyof State>(
|
|
327
|
-
channel: Channel,
|
|
328
|
-
value: State[Channel]
|
|
329
|
-
) {
|
|
330
|
-
this.doBroadcast("update", { channel, value })
|
|
331
|
-
this.resetHeartbeats()
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private resetHeartbeats() {
|
|
335
|
-
// Reset heartbeats every time we broadcast a message to avoid sending
|
|
336
|
-
// unnecessary heartbeats when there is plenty of actual update activity
|
|
337
|
-
// happening.
|
|
338
|
-
this.stopHeartbeats()
|
|
339
|
-
this.startHeartbeats()
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
private sendHeartbeat() {
|
|
343
|
-
this.doBroadcast("heartbeat")
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private doBroadcast(
|
|
347
|
-
type: PresenceMessageType,
|
|
348
|
-
extra?: Record<string, unknown>
|
|
349
|
-
) {
|
|
350
|
-
if (!this.#running) {
|
|
351
|
-
return
|
|
352
|
-
}
|
|
353
|
-
this.#handle.broadcast({
|
|
354
|
-
[PRESENCE_MESSAGE_MARKER]: {
|
|
355
|
-
userId: this.userId,
|
|
356
|
-
deviceId: this.deviceId,
|
|
357
|
-
type,
|
|
358
|
-
...extra,
|
|
359
|
-
},
|
|
360
|
-
})
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private startHeartbeats() {
|
|
364
|
-
if (this.#heartbeatInterval !== undefined) {
|
|
365
|
-
return
|
|
366
|
-
}
|
|
367
|
-
this.#heartbeatInterval = setInterval(() => {
|
|
368
|
-
this.sendHeartbeat()
|
|
369
|
-
}, this.#heartbeatMs)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
private stopHeartbeats() {
|
|
373
|
-
if (this.#heartbeatInterval === undefined) {
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
clearInterval(this.#heartbeatInterval)
|
|
377
|
-
this.#heartbeatInterval = undefined
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
private startPruningPeers() {
|
|
381
|
-
if (this.#pruningInterval !== undefined) {
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
// Pruning happens at the heartbeat frequency, not on a peer ttl frequency,
|
|
385
|
-
// to minimize variance between peer expiration, since the heartbeat frequency
|
|
386
|
-
// is expected to be several times higher.
|
|
387
|
-
this.#pruningInterval = setInterval(() => {
|
|
388
|
-
this.#peers.prune()
|
|
389
|
-
}, this.#heartbeatMs)
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private stopPruningPeers() {
|
|
393
|
-
if (this.#pruningInterval === undefined) {
|
|
394
|
-
return
|
|
395
|
-
}
|
|
396
|
-
clearInterval(this.#pruningInterval)
|
|
397
|
-
this.#pruningInterval = undefined
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* A summary of the latest Presence information for the set of peers who have
|
|
403
|
-
* reported a Presence status to us.
|
|
404
|
-
*/
|
|
405
|
-
export class PeerPresenceView<State> {
|
|
406
|
-
#peersLastSeen = new Map<PeerId, number>()
|
|
407
|
-
#peerStates = new Map<PeerId, PeerState<State>>()
|
|
408
|
-
#userPeers = new Map<UserId, Set<PeerId>>()
|
|
409
|
-
#devicePeers = new Map<DeviceId, Set<PeerId>>()
|
|
410
|
-
|
|
411
|
-
/** @hidden */
|
|
412
|
-
constructor(
|
|
413
|
-
peersLastSeen: Map<PeerId, number>,
|
|
414
|
-
peerStates: Map<PeerId, PeerState<State>>,
|
|
415
|
-
userPeers: Map<UserId, Set<PeerId>>,
|
|
416
|
-
devicePeers: Map<DeviceId, Set<PeerId>>
|
|
417
|
-
) {
|
|
418
|
-
this.#peersLastSeen = peersLastSeen
|
|
419
|
-
this.#peerStates = peerStates
|
|
420
|
-
this.#userPeers = userPeers
|
|
421
|
-
this.#devicePeers = devicePeers
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Check if peer is currently present.
|
|
426
|
-
*
|
|
427
|
-
* @param peerId
|
|
428
|
-
* @returns true if the peer has been seen recently
|
|
429
|
-
*/
|
|
430
|
-
has(peerId: PeerId) {
|
|
431
|
-
return this.#peerStates.has(peerId)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Check when the peer was last seen.
|
|
436
|
-
*
|
|
437
|
-
* @param peerId
|
|
438
|
-
* @returns last seen UNIX timestamp, or undefined for unknown peers
|
|
439
|
-
*/
|
|
440
|
-
getLastSeen(peerId: PeerId) {
|
|
441
|
-
return this.#peersLastSeen.get(peerId)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Get all recently-seen peers.
|
|
446
|
-
*
|
|
447
|
-
* @returns Array of peer ids
|
|
448
|
-
*/
|
|
449
|
-
getPeers() {
|
|
450
|
-
return Array.from(this.#peerStates.keys())
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Get all recently-seen users.
|
|
455
|
-
*
|
|
456
|
-
* @returns Array of user ids
|
|
457
|
-
*/
|
|
458
|
-
getUsers() {
|
|
459
|
-
return Array.from(this.#userPeers.keys())
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Get all recently-seen devices.
|
|
464
|
-
*
|
|
465
|
-
* @returns Array of device ids
|
|
466
|
-
*/
|
|
467
|
-
getDevices() {
|
|
468
|
-
return Array.from(this.#devicePeers.keys())
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Get all recently-seen peers for this user.
|
|
473
|
-
*
|
|
474
|
-
* @param userId
|
|
475
|
-
* @returns Array of peer ids for this user
|
|
476
|
-
*/
|
|
477
|
-
getUserPeers(userId: UserId) {
|
|
478
|
-
const peers = this.#userPeers.get(userId)
|
|
479
|
-
if (!peers) {
|
|
480
|
-
return
|
|
481
|
-
}
|
|
482
|
-
return Array.from(peers)
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Get all recently-seen peers for this device.
|
|
487
|
-
*
|
|
488
|
-
* @param deviceId
|
|
489
|
-
* @returns Array of peer ids for this device
|
|
490
|
-
*/
|
|
491
|
-
getDevicePeers(deviceId: DeviceId) {
|
|
492
|
-
const peers = this.#devicePeers.get(deviceId)
|
|
493
|
-
if (!peers) {
|
|
494
|
-
return
|
|
495
|
-
}
|
|
496
|
-
return Array.from(peers)
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Get most-recently-seen peer from this group.
|
|
501
|
-
*
|
|
502
|
-
* @param peers
|
|
503
|
-
* @returns id of most recently seen peer
|
|
504
|
-
*/
|
|
505
|
-
getFreshestPeer(peers: Set<PeerId>) {
|
|
506
|
-
let freshestLastSeen: number
|
|
507
|
-
return Array.from(peers).reduce((freshest: PeerId | undefined, curr) => {
|
|
508
|
-
const lastSeen = this.#peersLastSeen.get(curr)
|
|
509
|
-
if (!lastSeen) {
|
|
510
|
-
return freshest
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (!freshest || lastSeen > freshestLastSeen) {
|
|
514
|
-
freshestLastSeen = lastSeen
|
|
515
|
-
return curr
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return freshest
|
|
519
|
-
}, undefined)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Get current @type PeerState for given peer.
|
|
524
|
-
*
|
|
525
|
-
* @param peerId
|
|
526
|
-
* @returns details for the peer
|
|
527
|
-
*/
|
|
528
|
-
getPeerInfo(peerId: PeerId) {
|
|
529
|
-
return this.#peerStates.get(peerId)
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Get current ephemeral state value for this peer. If a channel is specified,
|
|
534
|
-
* only returns the ephemeral state for that specific channel. Otherwise,
|
|
535
|
-
* returns the full ephemeral state.
|
|
536
|
-
*
|
|
537
|
-
* @param peerId
|
|
538
|
-
* @param channel
|
|
539
|
-
* @returns latest ephemeral state received
|
|
540
|
-
*/
|
|
541
|
-
getPeerState<Channel extends keyof State>(peerId: PeerId, channel?: Channel) {
|
|
542
|
-
const fullState = this.#peerStates.get(peerId)?.value
|
|
543
|
-
if (!channel) {
|
|
544
|
-
return fullState
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return fullState?.[channel]
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Get current ephemeral state value for this user's most-recently-active
|
|
552
|
-
* peer. See {@link getPeerState}.
|
|
553
|
-
*
|
|
554
|
-
* @param userId
|
|
555
|
-
* @param channel
|
|
556
|
-
* @returns
|
|
557
|
-
*/
|
|
558
|
-
getUserState<Channel extends keyof State>(userId: UserId, channel?: Channel) {
|
|
559
|
-
const peers = this.#userPeers.get(userId)
|
|
560
|
-
if (!peers) {
|
|
561
|
-
return undefined
|
|
562
|
-
}
|
|
563
|
-
const peer = this.getFreshestPeer(peers)
|
|
564
|
-
if (!peer) {
|
|
565
|
-
return undefined
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return this.getPeerState(peer, channel)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Get current ephemeral state value for this device's most-recently-active
|
|
573
|
-
* peer. See {@link getPeerState}.
|
|
574
|
-
*
|
|
575
|
-
* @param userId
|
|
576
|
-
* @param channel
|
|
577
|
-
* @returns
|
|
578
|
-
*/
|
|
579
|
-
getDeviceState<Channel extends keyof State>(
|
|
580
|
-
deviceId: UserId,
|
|
581
|
-
channel?: Channel
|
|
582
|
-
) {
|
|
583
|
-
const peers = this.#devicePeers.get(deviceId)
|
|
584
|
-
if (!peers) {
|
|
585
|
-
return undefined
|
|
586
|
-
}
|
|
587
|
-
const peer = this.getFreshestPeer(peers)
|
|
588
|
-
if (!peer) {
|
|
589
|
-
return undefined
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
return this.getPeerState(peer, channel)
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
class PeerPresenceInfo<State> extends EventEmitter<PresenceEvents> {
|
|
597
|
-
#peersLastSeen = new Map<PeerId, number>()
|
|
598
|
-
#peerStates = new Map<PeerId, PeerState<State>>()
|
|
599
|
-
#userPeers = new Map<UserId, Set<PeerId>>()
|
|
600
|
-
#devicePeers = new Map<DeviceId, Set<PeerId>>()
|
|
601
|
-
|
|
602
|
-
readonly view: PeerPresenceView<State>
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Build a new peer presence state.
|
|
606
|
-
*
|
|
607
|
-
* @param ttl in milliseconds - peers with no activity within this timeframe
|
|
608
|
-
* are forgotten when {@link prune} is called.
|
|
609
|
-
*/
|
|
610
|
-
constructor(readonly ttl: number) {
|
|
611
|
-
super()
|
|
612
|
-
this.view = new PeerPresenceView(
|
|
613
|
-
this.#peersLastSeen,
|
|
614
|
-
this.#peerStates,
|
|
615
|
-
this.#userPeers,
|
|
616
|
-
this.#devicePeers
|
|
617
|
-
)
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Record that we've seen the given peer recently.
|
|
622
|
-
*
|
|
623
|
-
* @param peerId
|
|
624
|
-
* @param deviceId
|
|
625
|
-
* @param userId
|
|
626
|
-
*/
|
|
627
|
-
markSeen(peerId: PeerId, deviceId?: DeviceId, userId?: UserId) {
|
|
628
|
-
const devicePeers = this.#devicePeers.get(deviceId) ?? new Set<PeerId>()
|
|
629
|
-
devicePeers.add(peerId)
|
|
630
|
-
this.#devicePeers.set(deviceId, devicePeers)
|
|
631
|
-
|
|
632
|
-
const userPeers = this.#userPeers.get(userId) ?? new Set<PeerId>()
|
|
633
|
-
userPeers.add(peerId)
|
|
634
|
-
this.#userPeers.set(userId, userPeers)
|
|
635
|
-
|
|
636
|
-
this.#peersLastSeen.set(peerId, Date.now())
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Record a state update for the given peer. It is also automatically updated with {@link markSeen}.
|
|
641
|
-
*
|
|
642
|
-
* @param peerId
|
|
643
|
-
* @param deviceId
|
|
644
|
-
* @param userId
|
|
645
|
-
* @param value
|
|
646
|
-
*/
|
|
647
|
-
update<Channel extends keyof State>({
|
|
648
|
-
peerId,
|
|
649
|
-
deviceId,
|
|
650
|
-
userId,
|
|
651
|
-
channel,
|
|
652
|
-
value,
|
|
653
|
-
}: {
|
|
654
|
-
peerId: PeerId
|
|
655
|
-
deviceId?: DeviceId
|
|
656
|
-
userId?: UserId
|
|
657
|
-
channel: Channel
|
|
658
|
-
value: State[Channel]
|
|
659
|
-
}) {
|
|
660
|
-
this.markSeen(peerId, deviceId, userId)
|
|
661
|
-
|
|
662
|
-
const peerState = this.#peerStates.get(peerId)
|
|
663
|
-
const existingState = peerState?.value ?? ({} as State)
|
|
664
|
-
this.#peerStates.set(peerId, {
|
|
665
|
-
peerId,
|
|
666
|
-
deviceId,
|
|
667
|
-
userId,
|
|
668
|
-
value: {
|
|
669
|
-
...existingState,
|
|
670
|
-
[channel]: value,
|
|
671
|
-
},
|
|
672
|
-
})
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Forget the given peer.
|
|
677
|
-
*
|
|
678
|
-
* @param peerId
|
|
679
|
-
*/
|
|
680
|
-
delete(peerId: PeerId) {
|
|
681
|
-
this.#peersLastSeen.delete(peerId)
|
|
682
|
-
this.#peerStates.delete(peerId)
|
|
683
|
-
|
|
684
|
-
Array.from(this.#devicePeers.entries()).forEach(([deviceId, peerIds]) => {
|
|
685
|
-
if (peerIds.has(peerId)) {
|
|
686
|
-
peerIds.delete(peerId)
|
|
687
|
-
}
|
|
688
|
-
if (peerIds.size === 0) {
|
|
689
|
-
this.#devicePeers.delete(deviceId)
|
|
690
|
-
}
|
|
691
|
-
})
|
|
692
|
-
Array.from(this.#userPeers.entries()).forEach(([userId, peerIds]) => {
|
|
693
|
-
if (peerIds.has(peerId)) {
|
|
694
|
-
peerIds.delete(peerId)
|
|
695
|
-
}
|
|
696
|
-
if (peerIds.size === 0) {
|
|
697
|
-
this.#userPeers.delete(userId)
|
|
698
|
-
}
|
|
699
|
-
})
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Prune all peers that have not been seen since the configured ttl has
|
|
704
|
-
* elapsed.
|
|
705
|
-
*/
|
|
706
|
-
prune() {
|
|
707
|
-
const threshold = Date.now() - this.ttl
|
|
708
|
-
const stalePeers = new Set(
|
|
709
|
-
Array.from(this.#peersLastSeen.entries())
|
|
710
|
-
.filter(([, lastSeen]) => {
|
|
711
|
-
return lastSeen < threshold
|
|
712
|
-
})
|
|
713
|
-
.map(([peerId]) => peerId)
|
|
714
|
-
)
|
|
715
|
-
if (stalePeers.size === 0) {
|
|
716
|
-
return
|
|
717
|
-
}
|
|
718
|
-
stalePeers.forEach(stalePeer => {
|
|
719
|
-
this.delete(stalePeer)
|
|
720
|
-
})
|
|
721
|
-
}
|
|
722
|
-
}
|