@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/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
+ })
@@ -0,0 +1,5 @@
1
+ export async function wait(ms: number) {
2
+ return new Promise(resolve => {
3
+ setTimeout(resolve, ms)
4
+ })
5
+ }
@@ -0,0 +1,14 @@
1
+ export async function waitFor(callback: () => void) {
2
+ let sleepMs = 10
3
+ while (true) {
4
+ try {
5
+ callback()
6
+ break
7
+ } catch (e) {
8
+ sleepMs *= 2
9
+ await new Promise(resolve => {
10
+ setTimeout(resolve, sleepMs)
11
+ })
12
+ }
13
+ }
14
+ }