@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.
- package/dist/Presence.d.ts +245 -0
- package/dist/Presence.d.ts.map +1 -0
- package/dist/Presence.js +526 -0
- package/dist/Repo.d.ts +1 -0
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/synchronizer/CollectionSynchronizer.d.ts +8 -0
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +48 -1
- package/dist/synchronizer/DocSynchronizer.js +1 -1
- package/package.json +2 -2
- package/src/Presence.ts +722 -0
- package/src/Repo.ts +4 -0
- package/src/index.ts +8 -0
- package/src/synchronizer/CollectionSynchronizer.ts +61 -1
- package/src/synchronizer/DocSynchronizer.ts +1 -1
- package/test/Presence.test.ts +264 -0
- package/test/Repo.test.ts +47 -168
- package/test/SharePolicy.test.ts +244 -0
- package/test/helpers/awaitState.ts +24 -0
- package/test/helpers/connectRepos.ts +18 -0
- package/test/helpers/pause.ts +3 -0
- package/test/helpers/twoPeers.ts +30 -0
- package/test/helpers/wait.ts +5 -0
- package/test/helpers/waitFor.ts +14 -0
- package/test/helpers/withTimeout.ts +9 -0
|
@@ -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,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,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
|
+
}
|