@automerge/automerge-repo 2.5.1 → 2.5.2-alpha.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/dist/Presence.d.ts +245 -0
- package/dist/Presence.d.ts.map +1 -0
- package/dist/Presence.js +526 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +2 -2
- package/src/Presence.ts +722 -0
- package/src/index.ts +8 -0
- package/test/Presence.test.ts +264 -0
- package/test/helpers/wait.ts +5 -0
- package/test/helpers/waitFor.ts +14 -0
package/src/index.ts
CHANGED
|
@@ -39,6 +39,14 @@ export {
|
|
|
39
39
|
decodeHeads,
|
|
40
40
|
} from "./AutomergeUrl.js"
|
|
41
41
|
export { Repo } from "./Repo.js"
|
|
42
|
+
export {
|
|
43
|
+
Presence,
|
|
44
|
+
PeerPresenceView,
|
|
45
|
+
PeerState,
|
|
46
|
+
PresenceConfig,
|
|
47
|
+
UserId,
|
|
48
|
+
DeviceId,
|
|
49
|
+
} from "./Presence.js"
|
|
42
50
|
export { NetworkAdapter } from "./network/NetworkAdapter.js"
|
|
43
51
|
export type { NetworkAdapterInterface } from "./network/NetworkAdapterInterface.js"
|
|
44
52
|
export { isRepoMessage } from "./network/messages.js"
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { Presence, PresenceEventHeartbeat } from "../src/Presence.js"
|
|
4
|
+
import { Repo } from "../src/Repo.js"
|
|
5
|
+
import { PeerId } from "../src/types.js"
|
|
6
|
+
import { DummyNetworkAdapter } from "../src/helpers/DummyNetworkAdapter.js"
|
|
7
|
+
import { waitFor } from "./helpers/waitFor.js"
|
|
8
|
+
import { wait } from "./helpers/wait.js"
|
|
9
|
+
|
|
10
|
+
type PresenceState = { position: number }
|
|
11
|
+
|
|
12
|
+
describe("Presence", () => {
|
|
13
|
+
async function setup(opts?: { skipAnnounce?: boolean }) {
|
|
14
|
+
const alice = new Repo({ peerId: "alice" as PeerId })
|
|
15
|
+
const bob = new Repo({ peerId: "bob" as PeerId })
|
|
16
|
+
const [aliceToBob, bobToAlice] = DummyNetworkAdapter.createConnectedPair()
|
|
17
|
+
alice.networkSubsystem.addNetworkAdapter(aliceToBob)
|
|
18
|
+
bob.networkSubsystem.addNetworkAdapter(bobToAlice)
|
|
19
|
+
if (!opts?.skipAnnounce) {
|
|
20
|
+
aliceToBob.peerCandidate("bob" as PeerId)
|
|
21
|
+
bobToAlice.peerCandidate("alice" as PeerId)
|
|
22
|
+
}
|
|
23
|
+
await Promise.all([
|
|
24
|
+
alice.networkSubsystem.whenReady(),
|
|
25
|
+
bob.networkSubsystem.whenReady(),
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
const aliceHandle = alice.create({
|
|
29
|
+
test: "doc",
|
|
30
|
+
})
|
|
31
|
+
const alicePresence = new Presence<PresenceState>({
|
|
32
|
+
handle: aliceHandle,
|
|
33
|
+
userId: "alice",
|
|
34
|
+
deviceId: "phone",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const bobHandle = await bob.find(aliceHandle.url)
|
|
38
|
+
const bobPresence = new Presence<PresenceState>({
|
|
39
|
+
handle: bobHandle,
|
|
40
|
+
userId: "bob",
|
|
41
|
+
deviceId: "phone",
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
alice: {
|
|
46
|
+
repo: alice,
|
|
47
|
+
handle: aliceHandle,
|
|
48
|
+
presence: alicePresence,
|
|
49
|
+
network: aliceToBob,
|
|
50
|
+
},
|
|
51
|
+
bob: {
|
|
52
|
+
repo: bob,
|
|
53
|
+
handle: bobHandle,
|
|
54
|
+
presence: bobPresence,
|
|
55
|
+
network: bobToAlice,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("start", () => {
|
|
61
|
+
it("activates presence and shares initial state", async () => {
|
|
62
|
+
const { alice, bob } = await setup()
|
|
63
|
+
|
|
64
|
+
alice.presence.start({
|
|
65
|
+
initialState: {
|
|
66
|
+
position: 123,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
expect(alice.presence.running).toBe(true)
|
|
70
|
+
|
|
71
|
+
bob.presence.start({
|
|
72
|
+
initialState: {
|
|
73
|
+
position: 456,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
expect(bob.presence.running).toBe(true)
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
const bobPeerStates = bob.presence.getPeerStates()
|
|
80
|
+
const bobPeers = bobPeerStates.getPeers()
|
|
81
|
+
|
|
82
|
+
expect(bobPeers.length).toBe(1)
|
|
83
|
+
expect(bobPeers[0]).toBe(alice.repo.peerId)
|
|
84
|
+
expect(bobPeerStates.getPeerState(bobPeers[0], "position")).toBe(123)
|
|
85
|
+
|
|
86
|
+
const alicePeerStates = alice.presence.getPeerStates()
|
|
87
|
+
const alicePeers = alicePeerStates.getPeers()
|
|
88
|
+
|
|
89
|
+
expect(alicePeers.length).toBe(1)
|
|
90
|
+
expect(alicePeers[0]).toBe(bob.repo.peerId)
|
|
91
|
+
expect(alicePeerStates.getPeerState(alicePeers[0], "position")).toBe(
|
|
92
|
+
456
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("does nothing if invoked on an already-running Presence", async () => {
|
|
98
|
+
const { alice } = await setup()
|
|
99
|
+
|
|
100
|
+
alice.presence.start({
|
|
101
|
+
initialState: {
|
|
102
|
+
position: 123,
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
expect(alice.presence.running).toBe(true)
|
|
106
|
+
|
|
107
|
+
alice.presence.start({
|
|
108
|
+
initialState: {
|
|
109
|
+
position: 789,
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
expect(alice.presence.running).toBe(true)
|
|
113
|
+
expect(alice.presence.getLocalState().position).toBe(123)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe("stop", () => {
|
|
118
|
+
it("stops running presence and ignores further broadcasts", async () => {
|
|
119
|
+
const { alice, bob } = await setup()
|
|
120
|
+
|
|
121
|
+
alice.presence.start({
|
|
122
|
+
initialState: {
|
|
123
|
+
position: 123,
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
expect(alice.presence.running).toBe(true)
|
|
127
|
+
|
|
128
|
+
bob.presence.start({
|
|
129
|
+
initialState: {
|
|
130
|
+
position: 456,
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
const bobPeerStates = bob.presence.getPeerStates()
|
|
136
|
+
const bobPeers = bobPeerStates.getPeers()
|
|
137
|
+
|
|
138
|
+
expect(bobPeers.length).toBe(1)
|
|
139
|
+
expect(bobPeers[0]).toBe(alice.repo.peerId)
|
|
140
|
+
expect(bobPeerStates.getPeerState(bobPeers[0], "position")).toBe(123)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
alice.presence.stop()
|
|
144
|
+
expect(alice.presence.running).toBe(false)
|
|
145
|
+
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
const bobPeerStates = bob.presence.getPeerStates()
|
|
148
|
+
const bobPeers = bobPeerStates.getPeers()
|
|
149
|
+
|
|
150
|
+
expect(bobPeers.length).toBe(0)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("does nothing if invoked on a non-running Presence", async () => {
|
|
155
|
+
const { alice } = await setup()
|
|
156
|
+
|
|
157
|
+
expect(alice.presence.running).toBe(false)
|
|
158
|
+
|
|
159
|
+
alice.presence.stop()
|
|
160
|
+
|
|
161
|
+
expect(alice.presence.running).toBe(false)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe("heartbeats", () => {
|
|
166
|
+
it("sends heartbeats on the configured interval", async () => {
|
|
167
|
+
const { alice, bob } = await setup()
|
|
168
|
+
alice.presence.start({
|
|
169
|
+
initialState: {
|
|
170
|
+
position: 123,
|
|
171
|
+
},
|
|
172
|
+
heartbeatMs: 10,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
bob.presence.start({
|
|
176
|
+
initialState: {
|
|
177
|
+
position: 456,
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
let hbPeerMsg: PresenceEventHeartbeat
|
|
182
|
+
bob.presence.on("heartbeat", msg => {
|
|
183
|
+
hbPeerMsg = msg
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
expect(hbPeerMsg.peerId).toEqual(alice.repo.peerId)
|
|
188
|
+
expect(hbPeerMsg.type).toEqual("heartbeat")
|
|
189
|
+
expect(hbPeerMsg.userId).toEqual("alice")
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it("delays heartbeats when there is a state update", async () => {
|
|
194
|
+
const { alice, bob } = await setup()
|
|
195
|
+
alice.presence.start({
|
|
196
|
+
initialState: {
|
|
197
|
+
position: 123,
|
|
198
|
+
},
|
|
199
|
+
heartbeatMs: 10,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
bob.presence.start({
|
|
203
|
+
initialState: {
|
|
204
|
+
position: 456,
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
let hbPeerMsg: PresenceEventHeartbeat
|
|
209
|
+
bob.presence.on("heartbeat", msg => {
|
|
210
|
+
hbPeerMsg = msg
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await wait(7)
|
|
214
|
+
alice.presence.broadcast("position", 789)
|
|
215
|
+
await wait(7)
|
|
216
|
+
|
|
217
|
+
expect(hbPeerMsg).toBeUndefined()
|
|
218
|
+
|
|
219
|
+
await wait(20)
|
|
220
|
+
expect(hbPeerMsg.peerId).toEqual(alice.repo.peerId)
|
|
221
|
+
expect(hbPeerMsg.type).toEqual("heartbeat")
|
|
222
|
+
expect(hbPeerMsg.userId).toEqual("alice")
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe("broadcast", () => {
|
|
227
|
+
it("sends updates to peers", async () => {
|
|
228
|
+
const { alice, bob } = await setup()
|
|
229
|
+
alice.presence.start({
|
|
230
|
+
initialState: {
|
|
231
|
+
position: 123,
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
bob.presence.start({
|
|
236
|
+
initialState: {
|
|
237
|
+
position: 456,
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
const bobPeerStates = bob.presence.getPeerStates()
|
|
243
|
+
const bobPeers = bobPeerStates.getPeers()
|
|
244
|
+
|
|
245
|
+
expect(bobPeers.length).toBe(1)
|
|
246
|
+
expect(bobPeerStates.getPeerState(alice.repo.peerId, "position")).toBe(
|
|
247
|
+
123
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
alice.presence.broadcast("position", 213)
|
|
252
|
+
|
|
253
|
+
await waitFor(() => {
|
|
254
|
+
const bobPeerStates = bob.presence.getPeerStates()
|
|
255
|
+
const bobPeers = bobPeerStates.getPeers()
|
|
256
|
+
|
|
257
|
+
expect(bobPeers.length).toBe(1)
|
|
258
|
+
expect(bobPeerStates.getPeerState(alice.repo.peerId, "position")).toBe(
|
|
259
|
+
213
|
|
260
|
+
)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
})
|