@automerge/automerge-repo 2.5.0 → 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.
@@ -0,0 +1,244 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import assert from "assert"
3
+ import twoPeers from "./helpers/twoPeers.js"
4
+ import connectRepos from "./helpers/connectRepos.js"
5
+ import awaitState from "./helpers/awaitState.js"
6
+ import withTimeout from "./helpers/withTimeout.js"
7
+ import pause from "./helpers/pause.js"
8
+ import { Repo } from "../src/Repo.js"
9
+ import { PeerId } from "../src/types.js"
10
+
11
+ describe("the sharePolicy APIs", () => {
12
+ describe("the legacy API", () => {
13
+ it("should announce documents to peers for whom the sharePolicy returns true", async () => {
14
+ const { alice, bob } = await twoPeers({
15
+ alice: { sharePolicy: async () => true },
16
+ bob: { sharePolicy: async () => true },
17
+ })
18
+ const handle = alice.create({ foo: "bar" })
19
+
20
+ // Wait for the announcement to be synced
21
+ await pause(100)
22
+
23
+ // Disconnect and stop alice
24
+ await alice.shutdown()
25
+
26
+ // Bob should have the handle already because it was announced to him
27
+ const bobHandle = await bob.find(handle.url)
28
+ })
29
+
30
+ it("should not annouce documents to peers for whom the sharePolicy returns false", async () => {
31
+ const { alice, bob } = await twoPeers({
32
+ alice: { sharePolicy: async () => false },
33
+ bob: { sharePolicy: async () => true },
34
+ })
35
+ const handle = alice.create({ foo: "bar" })
36
+
37
+ // Disconnect and stop alice
38
+ await alice.shutdown()
39
+
40
+ // Bob should have the handle already because it was announced to him
41
+ const bobHandle = await withTimeout(bob.find(handle.url), 100)
42
+ assert.equal(bobHandle, null)
43
+ })
44
+
45
+ it("should respond to direct requests for document where the sharePolicy returns false", async () => {
46
+ const { alice, bob } = await twoPeers({
47
+ alice: { sharePolicy: async () => false },
48
+ bob: { sharePolicy: async () => true },
49
+ })
50
+
51
+ const aliceHandle = alice.create({ foo: "bar" })
52
+ const bobHandle = await bob.find(aliceHandle.url)
53
+ })
54
+ })
55
+
56
+ it("should respond to direct requests for document where the announce policy returns false but the access policy returns true", async () => {
57
+ const { alice, bob } = await twoPeers({
58
+ alice: {
59
+ shareConfig: {
60
+ announce: async () => false,
61
+ access: async () => true,
62
+ },
63
+ },
64
+ bob: { sharePolicy: async () => true },
65
+ })
66
+
67
+ const aliceHandle = alice.create({ foo: "bar" })
68
+ const bobHandle = await bob.find(aliceHandle.url)
69
+ })
70
+
71
+ it("should not respond to direct requests for a document where the access policy returns false and the announce policy return trrrue", async () => {
72
+ const { alice, bob } = await twoPeers({
73
+ alice: {
74
+ shareConfig: {
75
+ announce: async () => true,
76
+ access: async () => false,
77
+ },
78
+ },
79
+ bob: { sharePolicy: async () => true },
80
+ })
81
+
82
+ const aliceHandle = alice.create({ foo: "bar" })
83
+ withTimeout(
84
+ awaitState(bob.findWithProgress(aliceHandle.url), "unavailable"),
85
+ 500
86
+ )
87
+ })
88
+
89
+ it("should not respond to direct requests for a document where the access policy and the announce policy return false", async () => {
90
+ const { alice, bob } = await twoPeers({
91
+ alice: {
92
+ shareConfig: {
93
+ announce: async () => false,
94
+ access: async () => false,
95
+ },
96
+ },
97
+ bob: { sharePolicy: async () => false },
98
+ })
99
+
100
+ const aliceHandle = alice.create({ foo: "bar" })
101
+ withTimeout(
102
+ awaitState(bob.findWithProgress(aliceHandle.url), "unavailable"),
103
+ 500
104
+ )
105
+ })
106
+
107
+ describe("Repo.sharePolicyChanged", () => {
108
+ it("should respond to requests for a dochandle which was denied by the sharepolicy but then allowed", async () => {
109
+ const alicePolicy = { shouldShare: false }
110
+ const { alice, bob } = await twoPeers({
111
+ alice: {
112
+ shareConfig: {
113
+ announce: async () => false,
114
+ access: async () => alicePolicy.shouldShare,
115
+ },
116
+ },
117
+ bob: { sharePolicy: async () => true },
118
+ })
119
+
120
+ const aliceHandle = alice.create({ foo: "bar" })
121
+ await withTimeout(
122
+ awaitState(bob.findWithProgress(aliceHandle.url), "unavailable"),
123
+ 500
124
+ )
125
+
126
+ // Change policy to allow sharing
127
+ alicePolicy.shouldShare = true
128
+ alice.shareConfigChanged()
129
+
130
+ // Give time for Alices syncDebounceRate to elapse to start syncing with Bob
131
+ await pause(150)
132
+
133
+ const bobHandle = await bob.find(aliceHandle.url)
134
+ expect(bobHandle.doc()).toEqual({ foo: "bar" })
135
+ })
136
+
137
+ it("should stop sending changes to a peer who had access but was then removed", async () => {
138
+ const alicePolicy = {
139
+ shouldShareWithBob: true,
140
+ }
141
+ const alice = new Repo({
142
+ peerId: "alice" as PeerId,
143
+ shareConfig: {
144
+ announce: async () => false,
145
+ access: async peerId => {
146
+ if (peerId === "bob") {
147
+ return alicePolicy.shouldShareWithBob
148
+ }
149
+ return true
150
+ },
151
+ },
152
+ })
153
+ const bob = new Repo({
154
+ peerId: "bob" as PeerId,
155
+ shareConfig: {
156
+ announce: async () => true,
157
+ access: async () => true,
158
+ },
159
+ })
160
+ const charlie = new Repo({
161
+ peerId: "charlie" as PeerId,
162
+ shareConfig: {
163
+ announce: async () => true,
164
+ access: async () => true,
165
+ },
166
+ })
167
+
168
+ await connectRepos(alice, charlie)
169
+ await connectRepos(alice, bob)
170
+
171
+ // create a handle on alice, request it on bob and charlie
172
+ const aliceHandle = alice.create({ foo: "bar" })
173
+ const bobHandle = await bob.find<{ foo: string }>(aliceHandle.url)
174
+ const charlieHandle = await charlie.find<{ foo: string }>(aliceHandle.url)
175
+
176
+ // Now remove bobs access
177
+ alicePolicy.shouldShareWithBob = false
178
+ alice.shareConfigChanged()
179
+
180
+ // Now make a change on charlie
181
+ charlieHandle.change(d => (d.foo = "baz"))
182
+
183
+ // Wait for sync to propagate
184
+ await pause(300)
185
+
186
+ assert.deepStrictEqual(bobHandle.doc(), { foo: "bar" })
187
+ })
188
+
189
+ it("should not announce changes to a peer who reconnects", async () => {
190
+ // This test is exercising an issue where a peer who reconnects receives
191
+ // notifications about changes to a document they requested in a
192
+ // previous connection but have not requested since reconnection. This
193
+ // occurs because in order to calculate whether a peer has access to a
194
+ // document the Repo keeps track of whether the given peer has ever
195
+ // requested that document. If this state is not cleared on reconnection
196
+ // then repo will continue to announce changes to the peer in question.
197
+ const { alice, bob } = await twoPeers({
198
+ alice: {
199
+ shareConfig: {
200
+ announce: async () => false,
201
+ access: async () => true,
202
+ },
203
+ },
204
+ bob: { sharePolicy: async () => true },
205
+ })
206
+
207
+ const aliceHandle = alice.create({ foo: "bar" })
208
+ const bobHandle = await bob.find(aliceHandle.url)
209
+ assert(bobHandle != null)
210
+
211
+ // Disconnect everyone
212
+ bob.networkSubsystem.adapters[0].emit("peer-disconnected", {
213
+ peerId: alice.peerId,
214
+ })
215
+ alice.networkSubsystem.adapters[0].emit("peer-disconnected", {
216
+ peerId: bob.peerId,
217
+ })
218
+ bob.networkSubsystem.disconnect()
219
+ alice.networkSubsystem.disconnect()
220
+
221
+ await pause(150)
222
+
223
+ // Create a new repo with the same peer ID and reconnect
224
+ const bob2 = new Repo({ peerId: "bob" as PeerId })
225
+ await connectRepos(alice, bob2)
226
+
227
+ // Now create a third repo and connect it to alice
228
+ const charlie = new Repo({ peerId: "charlie" as PeerId })
229
+ await connectRepos(alice, charlie)
230
+
231
+ // Make a change on charlie, this will send a message to alice
232
+ // who will forward to anyone who she thinks has previously
233
+ // requested the document
234
+ const charlieHandle = await charlie.find<{ foo: string }>(aliceHandle.url)
235
+ charlieHandle.change(d => (d.foo = "baz"))
236
+
237
+ await pause(300)
238
+
239
+ // Bob should not have the handle, i.e. Alice should not have forwarded
240
+ // the messages from charlie
241
+ assert.equal(Object.entries(bob2.handles).length, 0)
242
+ })
243
+ })
244
+ })
@@ -0,0 +1,24 @@
1
+ import { FindProgress } from "../../src/FindProgress.js"
2
+ import { FindProgressWithMethods } from "../../src/Repo.js"
3
+
4
+ export default async function awaitState(
5
+ progress: FindProgress<unknown> | FindProgressWithMethods<unknown>,
6
+ state: string
7
+ ): Promise<void> {
8
+ if (progress.state == state) {
9
+ return
10
+ }
11
+ if (!("subscribe" in progress)) {
12
+ throw new Error(
13
+ `expected progress in state ${state} but was in final state ${progress.state}`
14
+ )
15
+ }
16
+ await new Promise(resolve => {
17
+ const unsubscribe = progress.subscribe(progress => {
18
+ if (progress.state === state) {
19
+ unsubscribe()
20
+ resolve(null)
21
+ }
22
+ })
23
+ })
24
+ }
@@ -0,0 +1,18 @@
1
+ import { DummyNetworkAdapter } from "../../src/helpers/DummyNetworkAdapter.js"
2
+ import { Repo } from "../../src/Repo.js"
3
+ import pause from "./pause.js"
4
+
5
+ export default async function connectRepos(left: Repo, right: Repo) {
6
+ const [leftToRight, rightToLeft] = DummyNetworkAdapter.createConnectedPair({
7
+ latency: 0,
8
+ })
9
+ left.networkSubsystem.addNetworkAdapter(leftToRight)
10
+ right.networkSubsystem.addNetworkAdapter(rightToLeft)
11
+ leftToRight.peerCandidate(right.peerId)
12
+ rightToLeft.peerCandidate(left.peerId)
13
+ await Promise.all([
14
+ left.networkSubsystem.whenReady(),
15
+ right.networkSubsystem.whenReady(),
16
+ ])
17
+ await pause(10)
18
+ }
@@ -0,0 +1,3 @@
1
+ export default async function pause(millis: number) {
2
+ return new Promise(resolve => setTimeout(resolve, millis))
3
+ }
@@ -0,0 +1,30 @@
1
+ import { DummyNetworkAdapter } from "../../src/helpers/DummyNetworkAdapter.js"
2
+ import { Repo, ShareConfig, SharePolicy } from "../../src/Repo.js"
3
+ import { PeerId } from "../../src/types.js"
4
+ import connectRepos from "./connectRepos.js"
5
+
6
+ // The parts of `RepoConfig` which are either the old sharePolicy API or the new shareConfig API
7
+ export type EitherConfig = {
8
+ sharePolicy?: SharePolicy
9
+ shareConfig?: ShareConfig
10
+ }
11
+
12
+ /// Create two connected peers with the given share configurations
13
+ export default async function twoPeers({
14
+ alice: aliceConfig,
15
+ bob: bobConfig,
16
+ }: {
17
+ alice: EitherConfig
18
+ bob: EitherConfig
19
+ }): Promise<{ alice: Repo; bob: Repo }> {
20
+ const alice = new Repo({
21
+ peerId: "alice" as PeerId,
22
+ ...aliceConfig,
23
+ })
24
+ const bob = new Repo({
25
+ peerId: "bob" as PeerId,
26
+ ...bobConfig,
27
+ })
28
+ await connectRepos(alice, bob)
29
+ return { alice, bob }
30
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export default async function withTimeout<T>(
2
+ promise: Promise<T>,
3
+ timeout: number
4
+ ): Promise<T | undefined> {
5
+ const timeoutPromise = new Promise<T | undefined>(resolve => {
6
+ setTimeout(() => resolve(undefined), timeout)
7
+ })
8
+ return Promise.race([promise, timeoutPromise])
9
+ }