@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
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"
|
|
@@ -22,6 +22,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
22
22
|
#docSetUp: Record<DocumentId, boolean> = {}
|
|
23
23
|
|
|
24
24
|
#denylist: DocumentId[]
|
|
25
|
+
#hasRequested: Map<DocumentId, Set<PeerId>> = new Map()
|
|
25
26
|
|
|
26
27
|
constructor(private repo: Repo, denylist: AutomergeUrl[] = []) {
|
|
27
28
|
super()
|
|
@@ -108,6 +109,16 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
108
109
|
return
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// Record the request so that even if access is denied now, we know that the
|
|
113
|
+
// peer requested the document so that if the share policy changes we know
|
|
114
|
+
// to begin syncing with this peer
|
|
115
|
+
if (message.type === "request") {
|
|
116
|
+
if (!this.#hasRequested.has(documentId)) {
|
|
117
|
+
this.#hasRequested.set(documentId, new Set())
|
|
118
|
+
}
|
|
119
|
+
this.#hasRequested.get(documentId)?.add(message.senderId)
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
const hasAccess = await this.repo.shareConfig.access(
|
|
112
123
|
message.senderId,
|
|
113
124
|
documentId
|
|
@@ -146,6 +157,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
146
157
|
if (this.#docSetUp[handle.documentId]) {
|
|
147
158
|
return
|
|
148
159
|
}
|
|
160
|
+
this.#docSetUp[handle.documentId] = true
|
|
149
161
|
const docSynchronizer = this.#fetchDocSynchronizer(handle)
|
|
150
162
|
void this.#documentGenerousPeers(handle.documentId).then(peers => {
|
|
151
163
|
void docSynchronizer.beginSync(peers)
|
|
@@ -184,6 +196,9 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
184
196
|
removePeer(peerId: PeerId) {
|
|
185
197
|
log(`removing peer ${peerId}`)
|
|
186
198
|
this.#peers.delete(peerId)
|
|
199
|
+
for (const requested of this.#hasRequested.values()) {
|
|
200
|
+
requested.delete(peerId)
|
|
201
|
+
}
|
|
187
202
|
|
|
188
203
|
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
189
204
|
docSynchronizer.endSync(peerId)
|
|
@@ -195,6 +210,49 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
195
210
|
return Array.from(this.#peers)
|
|
196
211
|
}
|
|
197
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Re-evaluates share policy for a document and updates sync accordingly
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* This is called when the share policy for a document has changed. It re-evaluates
|
|
218
|
+
* which peers should have access and starts/stops synchronization as needed.
|
|
219
|
+
*/
|
|
220
|
+
async reevaluateDocumentShare() {
|
|
221
|
+
const peers = Array.from(this.#peers)
|
|
222
|
+
const docPromises = []
|
|
223
|
+
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
224
|
+
const documentId = docSynchronizer.documentId
|
|
225
|
+
docPromises.push(
|
|
226
|
+
(async () => {
|
|
227
|
+
for (const peerId of peers) {
|
|
228
|
+
const shouldShare = await this.#shouldShare(peerId, documentId)
|
|
229
|
+
const isAlreadySyncing = docSynchronizer.hasPeer(peerId)
|
|
230
|
+
|
|
231
|
+
log(
|
|
232
|
+
`reevaluateDocumentShare: ${peerId} for ${documentId}, shouldShare: ${shouldShare}, isAlreadySyncing: ${isAlreadySyncing}`
|
|
233
|
+
)
|
|
234
|
+
if (shouldShare && !isAlreadySyncing) {
|
|
235
|
+
log(
|
|
236
|
+
`reevaluateDocumentShare: starting sync with ${peerId} for ${documentId}`
|
|
237
|
+
)
|
|
238
|
+
void docSynchronizer.beginSync([peerId])
|
|
239
|
+
} else if (!shouldShare && isAlreadySyncing) {
|
|
240
|
+
log(
|
|
241
|
+
`reevaluateDocumentShare: stopping sync with ${peerId} for ${documentId}`
|
|
242
|
+
)
|
|
243
|
+
docSynchronizer.endSync(peerId)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})().catch(e => {
|
|
247
|
+
console.log(
|
|
248
|
+
`error reevaluating document share for ${documentId}: ${e}`
|
|
249
|
+
)
|
|
250
|
+
})
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
await Promise.allSettled(docPromises)
|
|
254
|
+
}
|
|
255
|
+
|
|
198
256
|
metrics(): {
|
|
199
257
|
[key: string]: {
|
|
200
258
|
peers: PeerId[]
|
|
@@ -215,6 +273,8 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
215
273
|
this.repo.shareConfig.announce(peerId, documentId),
|
|
216
274
|
this.repo.shareConfig.access(peerId, documentId),
|
|
217
275
|
])
|
|
218
|
-
|
|
276
|
+
const hasRequested =
|
|
277
|
+
this.#hasRequested.get(documentId)?.has(peerId) ?? false
|
|
278
|
+
return announce || (access && hasRequested)
|
|
219
279
|
}
|
|
220
280
|
}
|
|
@@ -201,8 +201,8 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
201
201
|
forPeer: peerId,
|
|
202
202
|
})
|
|
203
203
|
|
|
204
|
+
this.#setSyncState(peerId, newSyncState)
|
|
204
205
|
if (message) {
|
|
205
|
-
this.#setSyncState(peerId, newSyncState)
|
|
206
206
|
const isNew = A.getHeads(doc).length === 0
|
|
207
207
|
|
|
208
208
|
if (
|
|
@@ -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
|
+
})
|
package/test/Repo.test.ts
CHANGED
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
LargeObject,
|
|
36
36
|
generateLargeObject,
|
|
37
37
|
} from "./helpers/generate-large-object.js"
|
|
38
|
+
import twoPeers from "./helpers/twoPeers.js"
|
|
39
|
+
import connectRepos from "./helpers/connectRepos.js"
|
|
40
|
+
import awaitState from "./helpers/awaitState.js"
|
|
41
|
+
import withTimeout from "./helpers/withTimeout.js"
|
|
38
42
|
import { getRandomItem } from "./helpers/getRandomItem.js"
|
|
39
43
|
import { TestDoc } from "./types.js"
|
|
40
44
|
import { StorageId, StorageKey } from "../src/storage/types.js"
|
|
@@ -1182,6 +1186,49 @@ describe("Repo", () => {
|
|
|
1182
1186
|
teardown()
|
|
1183
1187
|
})
|
|
1184
1188
|
|
|
1189
|
+
it("a previously unavailable document syncs if a connected peer obtains it (but doesn't announce it)", async () => {
|
|
1190
|
+
const alice = new Repo({
|
|
1191
|
+
peerId: "alice" as PeerId,
|
|
1192
|
+
shareConfig: {
|
|
1193
|
+
announce: async peerId => peerId === "charlie",
|
|
1194
|
+
access: async () => true,
|
|
1195
|
+
},
|
|
1196
|
+
})
|
|
1197
|
+
const bob = new Repo({
|
|
1198
|
+
peerId: "bob" as PeerId,
|
|
1199
|
+
shareConfig: {
|
|
1200
|
+
announce: async () => true,
|
|
1201
|
+
access: async () => true,
|
|
1202
|
+
},
|
|
1203
|
+
})
|
|
1204
|
+
const charlie = new Repo({
|
|
1205
|
+
peerId: "charlie" as PeerId,
|
|
1206
|
+
shareConfig: {
|
|
1207
|
+
announce: async () => false,
|
|
1208
|
+
access: async () => true,
|
|
1209
|
+
},
|
|
1210
|
+
})
|
|
1211
|
+
await connectRepos(alice, bob)
|
|
1212
|
+
|
|
1213
|
+
const charlieHandle = charlie.create({ foo: "bar" })
|
|
1214
|
+
|
|
1215
|
+
// Charlie isn't connected to any peer, so we don't have the document
|
|
1216
|
+
await assert.rejects(bob.find(charlieHandle.url))
|
|
1217
|
+
|
|
1218
|
+
// Now, connect charlie to alice
|
|
1219
|
+
await connectRepos(alice, charlie)
|
|
1220
|
+
|
|
1221
|
+
// Alice should now find the document
|
|
1222
|
+
const aliceHandle = await withTimeout(alice.find(charlieHandle.url), 500)
|
|
1223
|
+
assert.deepStrictEqual(aliceHandle.doc(), { foo: "bar" })
|
|
1224
|
+
|
|
1225
|
+
await pause(150) // wait for the sync debounce rate to elapse
|
|
1226
|
+
|
|
1227
|
+
// Bob should now find the document via alice
|
|
1228
|
+
const bobHandle = await withTimeout(bob.find(charlieHandle.url), 500)
|
|
1229
|
+
assert.deepStrictEqual(bobHandle.doc(), { foo: "bar" })
|
|
1230
|
+
})
|
|
1231
|
+
|
|
1185
1232
|
it("a previously unavailable document becomes available if the network adapter initially has no peers", async () => {
|
|
1186
1233
|
// It is possible for a network adapter to be ready without any peer
|
|
1187
1234
|
// being announced (e.g. the BroadcastChannelNetworkAdapter). In this
|
|
@@ -1716,174 +1763,6 @@ describe("Repo", () => {
|
|
|
1716
1763
|
assert.deepEqual(openDocs, 0)
|
|
1717
1764
|
})
|
|
1718
1765
|
})
|
|
1719
|
-
|
|
1720
|
-
describe("the sharePolicy", () => {
|
|
1721
|
-
async function connect(left: Repo, right: Repo) {
|
|
1722
|
-
const [leftToRight, rightToLeft] =
|
|
1723
|
-
DummyNetworkAdapter.createConnectedPair({ latency: 0 })
|
|
1724
|
-
left.networkSubsystem.addNetworkAdapter(leftToRight)
|
|
1725
|
-
right.networkSubsystem.addNetworkAdapter(rightToLeft)
|
|
1726
|
-
leftToRight.peerCandidate(right.peerId)
|
|
1727
|
-
rightToLeft.peerCandidate(left.peerId)
|
|
1728
|
-
await Promise.all([
|
|
1729
|
-
left.networkSubsystem.whenReady(),
|
|
1730
|
-
right.networkSubsystem.whenReady(),
|
|
1731
|
-
])
|
|
1732
|
-
await pause(10)
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
async function withTimeout<T>(
|
|
1736
|
-
promise: Promise<T>,
|
|
1737
|
-
timeout: number
|
|
1738
|
-
): Promise<T | undefined> {
|
|
1739
|
-
const timeoutPromise = new Promise<T | undefined>(resolve => {
|
|
1740
|
-
setTimeout(() => resolve(undefined), timeout)
|
|
1741
|
-
})
|
|
1742
|
-
return Promise.race([promise, timeoutPromise])
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
async function awaitState(
|
|
1746
|
-
progress: FindProgress<unknown> | FindProgressWithMethods<unknown>,
|
|
1747
|
-
state: string
|
|
1748
|
-
): Promise<void> {
|
|
1749
|
-
if (progress.state == state) {
|
|
1750
|
-
return
|
|
1751
|
-
}
|
|
1752
|
-
if (!("subscribe" in progress)) {
|
|
1753
|
-
throw new Error(
|
|
1754
|
-
`expected progress in state ${state} but was in final state ${progress.state}`
|
|
1755
|
-
)
|
|
1756
|
-
}
|
|
1757
|
-
await new Promise(resolve => {
|
|
1758
|
-
const unsubscribe = progress.subscribe(progress => {
|
|
1759
|
-
if (progress.state === state) {
|
|
1760
|
-
unsubscribe()
|
|
1761
|
-
resolve(null)
|
|
1762
|
-
}
|
|
1763
|
-
})
|
|
1764
|
-
})
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// The parts of `RepoConfig` which are either the old sharePolicy API or the new shareConfig API
|
|
1768
|
-
type EitherConfig = { sharePolicy?: SharePolicy; shareConfig?: ShareConfig }
|
|
1769
|
-
|
|
1770
|
-
/// Create two connected peers with the given share configurations
|
|
1771
|
-
async function twoPeers({
|
|
1772
|
-
alice: aliceConfig,
|
|
1773
|
-
bob: bobConfig,
|
|
1774
|
-
}: {
|
|
1775
|
-
alice: EitherConfig
|
|
1776
|
-
bob: EitherConfig
|
|
1777
|
-
}): Promise<{ alice: Repo; bob: Repo }> {
|
|
1778
|
-
const alice = new Repo({
|
|
1779
|
-
peerId: "alice" as PeerId,
|
|
1780
|
-
...aliceConfig,
|
|
1781
|
-
})
|
|
1782
|
-
const bob = new Repo({
|
|
1783
|
-
peerId: "bob" as PeerId,
|
|
1784
|
-
...bobConfig,
|
|
1785
|
-
})
|
|
1786
|
-
await connect(alice, bob)
|
|
1787
|
-
return { alice, bob }
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
describe("the legacy API", () => {
|
|
1791
|
-
it("should announce documents to peers for whom the sharePolicy returns true", async () => {
|
|
1792
|
-
const { alice, bob } = await twoPeers({
|
|
1793
|
-
alice: { sharePolicy: async () => true },
|
|
1794
|
-
bob: { sharePolicy: async () => true },
|
|
1795
|
-
})
|
|
1796
|
-
const handle = alice.create({ foo: "bar" })
|
|
1797
|
-
|
|
1798
|
-
// Wait for the announcement to be synced
|
|
1799
|
-
await pause(100)
|
|
1800
|
-
|
|
1801
|
-
// Disconnect and stop alice
|
|
1802
|
-
await alice.shutdown()
|
|
1803
|
-
|
|
1804
|
-
// Bob should have the handle already because it was announced to him
|
|
1805
|
-
const bobHandle = await bob.find(handle.url)
|
|
1806
|
-
})
|
|
1807
|
-
|
|
1808
|
-
it("should not annouce documents to peers for whom the sharePolicy returns false", async () => {
|
|
1809
|
-
const { alice, bob } = await twoPeers({
|
|
1810
|
-
alice: { sharePolicy: async () => false },
|
|
1811
|
-
bob: { sharePolicy: async () => true },
|
|
1812
|
-
})
|
|
1813
|
-
const handle = alice.create({ foo: "bar" })
|
|
1814
|
-
|
|
1815
|
-
// Disconnect and stop alice
|
|
1816
|
-
await alice.shutdown()
|
|
1817
|
-
|
|
1818
|
-
// Bob should have the handle already because it was announced to him
|
|
1819
|
-
const bobHandle = await withTimeout(bob.find(handle.url), 100)
|
|
1820
|
-
assert.equal(bobHandle, null)
|
|
1821
|
-
})
|
|
1822
|
-
|
|
1823
|
-
it("should respond to direct requests for document where the sharePolicy returns false", async () => {
|
|
1824
|
-
const { alice, bob } = await twoPeers({
|
|
1825
|
-
alice: { sharePolicy: async () => false },
|
|
1826
|
-
bob: { sharePolicy: async () => true },
|
|
1827
|
-
})
|
|
1828
|
-
await connect(alice, bob)
|
|
1829
|
-
|
|
1830
|
-
const aliceHandle = alice.create({ foo: "bar" })
|
|
1831
|
-
const bobHandle = await bob.find(aliceHandle.url)
|
|
1832
|
-
})
|
|
1833
|
-
})
|
|
1834
|
-
|
|
1835
|
-
it("should respond to direct requests for document where the announce policy returns false but the access policy returns true", async () => {
|
|
1836
|
-
const { alice, bob } = await twoPeers({
|
|
1837
|
-
alice: {
|
|
1838
|
-
shareConfig: {
|
|
1839
|
-
announce: async () => false,
|
|
1840
|
-
access: async () => true,
|
|
1841
|
-
},
|
|
1842
|
-
},
|
|
1843
|
-
bob: { sharePolicy: async () => true },
|
|
1844
|
-
})
|
|
1845
|
-
|
|
1846
|
-
const aliceHandle = alice.create({ foo: "bar" })
|
|
1847
|
-
const bobHandle = await bob.find(aliceHandle.url)
|
|
1848
|
-
})
|
|
1849
|
-
|
|
1850
|
-
it("should not respond to direct requests for a document where the access policy returns false and the announce policy return trrrue", async () => {
|
|
1851
|
-
const { alice, bob } = await twoPeers({
|
|
1852
|
-
alice: {
|
|
1853
|
-
shareConfig: {
|
|
1854
|
-
announce: async () => true,
|
|
1855
|
-
access: async () => false,
|
|
1856
|
-
},
|
|
1857
|
-
},
|
|
1858
|
-
bob: { sharePolicy: async () => true },
|
|
1859
|
-
})
|
|
1860
|
-
await connect(alice, bob)
|
|
1861
|
-
|
|
1862
|
-
const aliceHandle = alice.create({ foo: "bar" })
|
|
1863
|
-
withTimeout(
|
|
1864
|
-
awaitState(bob.findWithProgress(aliceHandle.url), "unavailable"),
|
|
1865
|
-
500
|
|
1866
|
-
)
|
|
1867
|
-
})
|
|
1868
|
-
|
|
1869
|
-
it("should not respond to direct requests for a document where the access policy and the announce policy return false", async () => {
|
|
1870
|
-
const { alice, bob } = await twoPeers({
|
|
1871
|
-
alice: {
|
|
1872
|
-
shareConfig: {
|
|
1873
|
-
announce: async () => false,
|
|
1874
|
-
access: async () => false,
|
|
1875
|
-
},
|
|
1876
|
-
},
|
|
1877
|
-
bob: { sharePolicy: async () => false },
|
|
1878
|
-
})
|
|
1879
|
-
|
|
1880
|
-
const aliceHandle = alice.create({ foo: "bar" })
|
|
1881
|
-
withTimeout(
|
|
1882
|
-
awaitState(bob.findWithProgress(aliceHandle.url), "unavailable"),
|
|
1883
|
-
500
|
|
1884
|
-
)
|
|
1885
|
-
})
|
|
1886
|
-
})
|
|
1887
1766
|
})
|
|
1888
1767
|
|
|
1889
1768
|
describe("Repo heads-in-URLs functionality", () => {
|