@automerge/automerge-repo 1.1.0-alpha.7 → 1.1.1
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/README.md +5 -3
- package/dist/AutomergeUrl.js +1 -1
- package/dist/DocHandle.d.ts +10 -4
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +21 -13
- package/dist/Repo.d.ts +22 -10
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +90 -76
- package/dist/helpers/pause.d.ts +0 -1
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/pause.js +2 -8
- package/dist/helpers/tests/network-adapter-tests.d.ts +2 -2
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +16 -1
- package/dist/helpers/withTimeout.d.ts.map +1 -1
- package/dist/helpers/withTimeout.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/network/NetworkAdapter.d.ts +4 -34
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.js +2 -0
- package/dist/network/NetworkAdapterInterface.d.ts +61 -0
- package/dist/network/NetworkAdapterInterface.d.ts.map +1 -0
- package/dist/network/NetworkAdapterInterface.js +2 -0
- package/dist/network/NetworkSubsystem.d.ts +3 -3
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +7 -5
- package/dist/network/messages.d.ts +43 -38
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +7 -9
- package/dist/storage/StorageAdapter.d.ts +3 -1
- package/dist/storage/StorageAdapter.d.ts.map +1 -1
- package/dist/storage/StorageAdapter.js +1 -0
- package/dist/storage/StorageAdapterInterface.d.ts +30 -0
- package/dist/storage/StorageAdapterInterface.d.ts.map +1 -0
- package/dist/storage/StorageAdapterInterface.js +1 -0
- package/dist/storage/StorageSubsystem.d.ts +2 -2
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +1 -0
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +13 -9
- package/dist/synchronizer/Synchronizer.d.ts +11 -3
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/AutomergeUrl.ts +1 -1
- package/src/DocHandle.ts +40 -19
- package/src/Repo.ts +123 -98
- package/src/helpers/pause.ts +3 -11
- package/src/helpers/tests/network-adapter-tests.ts +30 -4
- package/src/helpers/withTimeout.ts +2 -0
- package/src/index.ts +4 -2
- package/src/network/NetworkAdapter.ts +9 -45
- package/src/network/NetworkAdapterInterface.ts +77 -0
- package/src/network/NetworkSubsystem.ts +16 -14
- package/src/network/messages.ts +60 -63
- package/src/storage/StorageAdapter.ts +3 -1
- package/src/storage/StorageAdapterInterface.ts +34 -0
- package/src/storage/StorageSubsystem.ts +3 -3
- package/src/synchronizer/CollectionSynchronizer.ts +1 -0
- package/src/synchronizer/DocSynchronizer.ts +22 -18
- package/src/synchronizer/Synchronizer.ts +11 -3
- package/test/CollectionSynchronizer.test.ts +7 -5
- package/test/DocHandle.test.ts +35 -3
- package/test/RemoteHeadsSubscriptions.test.ts +49 -49
- package/test/Repo.test.ts +71 -2
- package/test/StorageSubsystem.test.ts +1 -1
- package/test/helpers/DummyNetworkAdapter.ts +37 -5
- package/test/helpers/collectMessages.ts +19 -0
- package/test/remoteHeads.test.ts +142 -119
- package/.eslintrc +0 -28
- package/test/helpers/waitForMessages.ts +0 -22
|
@@ -1,27 +1,23 @@
|
|
|
1
|
+
/* c8 ignore start */
|
|
2
|
+
|
|
1
3
|
import { EventEmitter } from "eventemitter3"
|
|
4
|
+
import { NetworkAdapterEvents, PeerMetadata } from "../index.js"
|
|
2
5
|
import { PeerId } from "../types.js"
|
|
3
6
|
import { Message } from "./messages.js"
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Describes a peer intent to the system
|
|
8
|
-
* storageId: the key for syncState to decide what the other peer already has
|
|
9
|
-
* isEphemeral: to decide if we bother recording this peer's sync state
|
|
10
|
-
*
|
|
11
|
-
*/
|
|
12
|
-
export interface PeerMetadata {
|
|
13
|
-
storageId?: StorageId
|
|
14
|
-
isEphemeral?: boolean
|
|
15
|
-
}
|
|
7
|
+
import { NetworkAdapterInterface } from "./NetworkAdapterInterface.js"
|
|
16
8
|
|
|
17
9
|
/** An interface representing some way to connect to other peers
|
|
10
|
+
* @deprecated use {@link NetworkAdapterInterface}
|
|
18
11
|
*
|
|
19
12
|
* @remarks
|
|
20
13
|
* The {@link Repo} uses one or more `NetworkAdapter`s to connect to other peers.
|
|
21
14
|
* Because the network may take some time to be ready the {@link Repo} will wait
|
|
22
15
|
* until the adapter emits a `ready` event before it starts trying to use it
|
|
23
16
|
*/
|
|
24
|
-
export abstract class NetworkAdapter
|
|
17
|
+
export abstract class NetworkAdapter
|
|
18
|
+
extends EventEmitter<NetworkAdapterEvents>
|
|
19
|
+
implements NetworkAdapterInterface
|
|
20
|
+
{
|
|
25
21
|
peerId?: PeerId
|
|
26
22
|
peerMetadata?: PeerMetadata
|
|
27
23
|
|
|
@@ -41,35 +37,3 @@ export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents>
|
|
|
41
37
|
/** Called by the {@link Repo} to disconnect from the network */
|
|
42
38
|
abstract disconnect(): void
|
|
43
39
|
}
|
|
44
|
-
|
|
45
|
-
// events & payloads
|
|
46
|
-
|
|
47
|
-
export interface NetworkAdapterEvents {
|
|
48
|
-
/** Emitted when the network is ready to be used */
|
|
49
|
-
ready: (payload: OpenPayload) => void
|
|
50
|
-
|
|
51
|
-
/** Emitted when the network is closed */
|
|
52
|
-
close: () => void
|
|
53
|
-
|
|
54
|
-
/** Emitted when the network adapter learns about a new peer */
|
|
55
|
-
"peer-candidate": (payload: PeerCandidatePayload) => void
|
|
56
|
-
|
|
57
|
-
/** Emitted when the network adapter learns that a peer has disconnected */
|
|
58
|
-
"peer-disconnected": (payload: PeerDisconnectedPayload) => void
|
|
59
|
-
|
|
60
|
-
/** Emitted when the network adapter receives a message from a peer */
|
|
61
|
-
message: (payload: Message) => void
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface OpenPayload {
|
|
65
|
-
network: NetworkAdapter
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface PeerCandidatePayload {
|
|
69
|
-
peerId: PeerId
|
|
70
|
-
peerMetadata: PeerMetadata
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface PeerDisconnectedPayload {
|
|
74
|
-
peerId: PeerId
|
|
75
|
-
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* c8 ignore start */
|
|
2
|
+
|
|
3
|
+
import { EventEmitter } from "eventemitter3"
|
|
4
|
+
import { PeerId } from "../types.js"
|
|
5
|
+
import { Message } from "./messages.js"
|
|
6
|
+
import { StorageId } from "../storage/types.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Describes a peer intent to the system
|
|
10
|
+
* storageId: the key for syncState to decide what the other peer already has
|
|
11
|
+
* isEphemeral: to decide if we bother recording this peer's sync state
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
export interface PeerMetadata {
|
|
15
|
+
storageId?: StorageId
|
|
16
|
+
isEphemeral?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** An interface representing some way to connect to other peers
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* The {@link Repo} uses one or more `NetworkAdapter`s to connect to other peers.
|
|
23
|
+
* Because the network may take some time to be ready the {@link Repo} will wait
|
|
24
|
+
* until the adapter emits a `ready` event before it starts trying to use it
|
|
25
|
+
*/
|
|
26
|
+
export interface NetworkAdapterInterface extends EventEmitter<NetworkAdapterEvents> {
|
|
27
|
+
peerId?: PeerId
|
|
28
|
+
peerMetadata?: PeerMetadata
|
|
29
|
+
|
|
30
|
+
/** Called by the {@link Repo} to start the connection process
|
|
31
|
+
*
|
|
32
|
+
* @argument peerId - the peerId of this repo
|
|
33
|
+
* @argument peerMetadata - how this adapter should present itself to other peers
|
|
34
|
+
*/
|
|
35
|
+
connect(peerId: PeerId, peerMetadata?: PeerMetadata): void
|
|
36
|
+
|
|
37
|
+
/** Called by the {@link Repo} to send a message to a peer
|
|
38
|
+
*
|
|
39
|
+
* @argument message - the message to send
|
|
40
|
+
*/
|
|
41
|
+
send(message: Message): void
|
|
42
|
+
|
|
43
|
+
/** Called by the {@link Repo} to disconnect from the network */
|
|
44
|
+
disconnect(): void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// events & payloads
|
|
48
|
+
|
|
49
|
+
export interface NetworkAdapterEvents {
|
|
50
|
+
/** Emitted when the network is ready to be used */
|
|
51
|
+
ready: (payload: OpenPayload) => void
|
|
52
|
+
|
|
53
|
+
/** Emitted when the network is closed */
|
|
54
|
+
close: () => void
|
|
55
|
+
|
|
56
|
+
/** Emitted when the network adapter learns about a new peer */
|
|
57
|
+
"peer-candidate": (payload: PeerCandidatePayload) => void
|
|
58
|
+
|
|
59
|
+
/** Emitted when the network adapter learns that a peer has disconnected */
|
|
60
|
+
"peer-disconnected": (payload: PeerDisconnectedPayload) => void
|
|
61
|
+
|
|
62
|
+
/** Emitted when the network adapter receives a message from a peer */
|
|
63
|
+
message: (payload: Message) => void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface OpenPayload {
|
|
67
|
+
network: NetworkAdapterInterface
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PeerCandidatePayload {
|
|
71
|
+
peerId: PeerId
|
|
72
|
+
peerMetadata: PeerMetadata
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PeerDisconnectedPayload {
|
|
76
|
+
peerId: PeerId
|
|
77
|
+
}
|
|
@@ -2,16 +2,16 @@ import debug from "debug"
|
|
|
2
2
|
import { EventEmitter } from "eventemitter3"
|
|
3
3
|
import { PeerId, SessionId } from "../types.js"
|
|
4
4
|
import type {
|
|
5
|
-
|
|
5
|
+
NetworkAdapterInterface,
|
|
6
6
|
PeerDisconnectedPayload,
|
|
7
7
|
PeerMetadata,
|
|
8
|
-
} from "./
|
|
8
|
+
} from "./NetworkAdapterInterface.js"
|
|
9
9
|
import {
|
|
10
10
|
EphemeralMessage,
|
|
11
11
|
MessageContents,
|
|
12
12
|
RepoMessage,
|
|
13
13
|
isEphemeralMessage,
|
|
14
|
-
|
|
14
|
+
isRepoMessage,
|
|
15
15
|
} from "./messages.js"
|
|
16
16
|
|
|
17
17
|
type EphemeralMessageSource = `${PeerId}:${SessionId}`
|
|
@@ -21,16 +21,16 @@ const getEphemeralMessageSource = (message: EphemeralMessage) =>
|
|
|
21
21
|
|
|
22
22
|
export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
23
23
|
#log: debug.Debugger
|
|
24
|
-
#adaptersByPeer: Record<PeerId,
|
|
24
|
+
#adaptersByPeer: Record<PeerId, NetworkAdapterInterface> = {}
|
|
25
25
|
|
|
26
26
|
#count = 0
|
|
27
27
|
#sessionId: SessionId = Math.random().toString(36).slice(2) as SessionId
|
|
28
28
|
#ephemeralSessionCounts: Record<EphemeralMessageSource, number> = {}
|
|
29
29
|
#readyAdapterCount = 0
|
|
30
|
-
#adapters:
|
|
30
|
+
#adapters: NetworkAdapterInterface[] = []
|
|
31
31
|
|
|
32
32
|
constructor(
|
|
33
|
-
adapters:
|
|
33
|
+
adapters: NetworkAdapterInterface[],
|
|
34
34
|
public peerId = randomPeerId(),
|
|
35
35
|
private peerMetadata: Promise<PeerMetadata>
|
|
36
36
|
) {
|
|
@@ -39,7 +39,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
39
39
|
adapters.forEach(a => this.addNetworkAdapter(a))
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
addNetworkAdapter(networkAdapter:
|
|
42
|
+
addNetworkAdapter(networkAdapter: NetworkAdapterInterface) {
|
|
43
43
|
this.#adapters.push(networkAdapter)
|
|
44
44
|
networkAdapter.once("ready", () => {
|
|
45
45
|
this.#readyAdapterCount++
|
|
@@ -73,7 +73,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
networkAdapter.on("message", msg => {
|
|
76
|
-
if (!
|
|
76
|
+
if (!isRepoMessage(msg)) {
|
|
77
77
|
this.#log(`invalid message: ${JSON.stringify(msg)}`)
|
|
78
78
|
return
|
|
79
79
|
}
|
|
@@ -105,11 +105,13 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
105
105
|
})
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
this.peerMetadata
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
this.peerMetadata
|
|
109
|
+
.then(peerMetadata => {
|
|
110
|
+
networkAdapter.connect(this.peerId, peerMetadata)
|
|
111
|
+
})
|
|
112
|
+
.catch(err => {
|
|
113
|
+
this.#log("error connecting to network", err)
|
|
114
|
+
})
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
send(message: MessageContents) {
|
|
@@ -146,7 +148,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
const outbound = prepareMessage(message)
|
|
149
|
-
this.#log("sending message", outbound)
|
|
151
|
+
this.#log("sending message %o", outbound)
|
|
150
152
|
peer.send(outbound as RepoMessage)
|
|
151
153
|
}
|
|
152
154
|
|
package/src/network/messages.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { SyncState } from "@automerge/automerge"
|
|
2
|
-
import { DocumentId, PeerId, SessionId } from "../types.js"
|
|
3
2
|
import { StorageId } from "../storage/types.js"
|
|
3
|
+
import { DocumentId, PeerId, SessionId } from "../types.js"
|
|
4
|
+
|
|
5
|
+
export type Message = {
|
|
6
|
+
type: string
|
|
7
|
+
|
|
8
|
+
/** The peer ID of the sender of this message */
|
|
9
|
+
senderId: PeerId
|
|
10
|
+
|
|
11
|
+
/** The peer ID of the recipient of this message */
|
|
12
|
+
targetId: PeerId
|
|
13
|
+
|
|
14
|
+
data?: Uint8Array
|
|
15
|
+
|
|
16
|
+
documentId?: DocumentId
|
|
17
|
+
}
|
|
4
18
|
|
|
5
19
|
/**
|
|
6
20
|
* A sync message for a particular document
|
|
7
21
|
*/
|
|
8
22
|
export type SyncMessage = {
|
|
9
23
|
type: "sync"
|
|
10
|
-
|
|
11
|
-
/** The peer ID of the sender of this message */
|
|
12
24
|
senderId: PeerId
|
|
13
|
-
|
|
14
|
-
/** The peer ID of the recipient of this message */
|
|
15
25
|
targetId: PeerId
|
|
16
26
|
|
|
17
27
|
/** The automerge sync message */
|
|
@@ -21,53 +31,50 @@ export type SyncMessage = {
|
|
|
21
31
|
documentId: DocumentId
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* An ephemeral message.
|
|
25
36
|
*
|
|
26
37
|
* @remarks
|
|
27
|
-
* Ephemeral messages are not persisted anywhere
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* number
|
|
32
|
-
* we have already seen.
|
|
38
|
+
* Ephemeral messages are not persisted anywhere. The data property can be used by the application
|
|
39
|
+
* as needed. The repo gossips these around.
|
|
40
|
+
*
|
|
41
|
+
* In order to avoid infinite loops of ephemeral messages, every message has (a) a session ID, which
|
|
42
|
+
* is a random number generated by the sender at startup time; and (b) a sequence number. The
|
|
43
|
+
* combination of these two things allows us to discard messages we have already seen.
|
|
33
44
|
* */
|
|
34
45
|
export type EphemeralMessage = {
|
|
35
46
|
type: "ephemeral"
|
|
36
|
-
|
|
37
|
-
/** The peer ID of the sender of this message */
|
|
38
47
|
senderId: PeerId
|
|
39
|
-
|
|
40
|
-
/** The peer ID of the recipient of this message */
|
|
41
48
|
targetId: PeerId
|
|
42
49
|
|
|
43
|
-
/** A sequence number which must be incremented for each message sent by this peer */
|
|
50
|
+
/** A sequence number which must be incremented for each message sent by this peer. */
|
|
44
51
|
count: number
|
|
45
52
|
|
|
46
|
-
/** The ID of the session this message is part of. The sequence number for a given session always increases */
|
|
53
|
+
/** The ID of the session this message is part of. The sequence number for a given session always increases. */
|
|
47
54
|
sessionId: SessionId
|
|
48
55
|
|
|
49
|
-
/** The document ID this message pertains to */
|
|
56
|
+
/** The document ID this message pertains to. */
|
|
50
57
|
documentId: DocumentId
|
|
51
58
|
|
|
52
|
-
/** The actual data of the message */
|
|
59
|
+
/** The actual data of the message. */
|
|
53
60
|
data: Uint8Array
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Sent by a {@link Repo} to indicate that it does not have the document and none of its connected
|
|
65
|
+
* peers do either.
|
|
66
|
+
*/
|
|
57
67
|
export type DocumentUnavailableMessage = {
|
|
58
68
|
type: "doc-unavailable"
|
|
59
|
-
|
|
60
|
-
/** The peer ID of the sender of this message */
|
|
61
69
|
senderId: PeerId
|
|
62
|
-
|
|
63
|
-
/** The peer ID of the recipient of this message */
|
|
64
70
|
targetId: PeerId
|
|
65
71
|
|
|
66
72
|
/** The document which the peer claims it doesn't have */
|
|
67
73
|
documentId: DocumentId
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Sent by a {@link Repo} to request a document from a peer.
|
|
71
78
|
*
|
|
72
79
|
* @remarks
|
|
73
80
|
* This is identical to a {@link SyncMessage} except that it is sent by a {@link Repo}
|
|
@@ -75,47 +82,43 @@ export type DocumentUnavailableMessage = {
|
|
|
75
82
|
* */
|
|
76
83
|
export type RequestMessage = {
|
|
77
84
|
type: "request"
|
|
78
|
-
|
|
79
|
-
/** The peer ID of the sender of this message */
|
|
80
85
|
senderId: PeerId
|
|
81
|
-
|
|
82
|
-
/** The peer ID of the recipient of this message */
|
|
83
86
|
targetId: PeerId
|
|
84
87
|
|
|
85
|
-
/** The
|
|
88
|
+
/** The automerge sync message */
|
|
86
89
|
data: Uint8Array
|
|
87
90
|
|
|
88
|
-
/** The document ID this message
|
|
91
|
+
/** The document ID of the document this message is for */
|
|
89
92
|
documentId: DocumentId
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/** The peer ID of the sender of this message */
|
|
97
|
-
senderId: PeerId
|
|
98
|
-
|
|
99
|
-
/** The peer ID of the recipient of this message */
|
|
100
|
-
targetId: PeerId
|
|
101
|
-
|
|
102
|
-
/** The payload of the auth message (up to the specific auth provider) */
|
|
103
|
-
payload: TPayload
|
|
104
|
-
}
|
|
105
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Sent by a {@link Repo} to add or remove storage IDs from a remote peer's subscription.
|
|
97
|
+
*/
|
|
106
98
|
export type RemoteSubscriptionControlMessage = {
|
|
107
99
|
type: "remote-subscription-change"
|
|
108
100
|
senderId: PeerId
|
|
109
101
|
targetId: PeerId
|
|
102
|
+
|
|
103
|
+
/** The storage IDs to add to the subscription */
|
|
110
104
|
add?: StorageId[]
|
|
105
|
+
|
|
106
|
+
/** The storage IDs to remove from the subscription */
|
|
111
107
|
remove?: StorageId[]
|
|
112
108
|
}
|
|
113
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Sent by a {@link Repo} to indicate that the heads of a document have changed on a remote peer.
|
|
112
|
+
*/
|
|
114
113
|
export type RemoteHeadsChanged = {
|
|
115
114
|
type: "remote-heads-changed"
|
|
116
115
|
senderId: PeerId
|
|
117
116
|
targetId: PeerId
|
|
117
|
+
|
|
118
|
+
/** The document ID of the document that has changed */
|
|
118
119
|
documentId: DocumentId
|
|
120
|
+
|
|
121
|
+
/** The document's new heads */
|
|
119
122
|
newHeads: { [key: StorageId]: { heads: string[]; timestamp: number } }
|
|
120
123
|
}
|
|
121
124
|
|
|
@@ -128,19 +131,17 @@ export type RepoMessage =
|
|
|
128
131
|
| RemoteSubscriptionControlMessage
|
|
129
132
|
| RemoteHeadsChanged
|
|
130
133
|
|
|
134
|
+
/** These are message types that are handled by the {@link CollectionSynchronizer}.*/
|
|
131
135
|
export type DocMessage =
|
|
132
136
|
| SyncMessage
|
|
133
137
|
| EphemeralMessage
|
|
134
138
|
| RequestMessage
|
|
135
139
|
| DocumentUnavailableMessage
|
|
136
140
|
|
|
137
|
-
/** These are all the message types that a {@link NetworkAdapter} might see. */
|
|
138
|
-
export type Message = RepoMessage | AuthMessage
|
|
139
|
-
|
|
140
141
|
/**
|
|
141
142
|
* The contents of a message, without the sender ID or other properties added by the {@link NetworkSubsystem})
|
|
142
143
|
*/
|
|
143
|
-
export type MessageContents<T extends Message =
|
|
144
|
+
export type MessageContents<T extends Message = RepoMessage> =
|
|
144
145
|
T extends EphemeralMessage
|
|
145
146
|
? Omit<T, "senderId" | "count" | "sessionId">
|
|
146
147
|
: Omit<T, "senderId">
|
|
@@ -160,16 +161,13 @@ export interface OpenDocMessage {
|
|
|
160
161
|
|
|
161
162
|
// TYPE GUARDS
|
|
162
163
|
|
|
163
|
-
export const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
isDocumentUnavailableMessage(message) ||
|
|
171
|
-
isRemoteSubscriptionControlMessage(message) ||
|
|
172
|
-
isRemoteHeadsChanged(message))
|
|
164
|
+
export const isRepoMessage = (message: Message): message is RepoMessage =>
|
|
165
|
+
isSyncMessage(message) ||
|
|
166
|
+
isEphemeralMessage(message) ||
|
|
167
|
+
isRequestMessage(message) ||
|
|
168
|
+
isDocumentUnavailableMessage(message) ||
|
|
169
|
+
isRemoteSubscriptionControlMessage(message) ||
|
|
170
|
+
isRemoteHeadsChanged(message)
|
|
173
171
|
|
|
174
172
|
// prettier-ignore
|
|
175
173
|
export const isDocumentUnavailableMessage = (msg: Message): msg is DocumentUnavailableMessage =>
|
|
@@ -184,9 +182,8 @@ export const isSyncMessage = (msg: Message): msg is SyncMessage =>
|
|
|
184
182
|
export const isEphemeralMessage = (msg: Message): msg is EphemeralMessage =>
|
|
185
183
|
msg.type === "ephemeral"
|
|
186
184
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
): msg is RemoteSubscriptionControlMessage =>
|
|
185
|
+
// prettier-ignore
|
|
186
|
+
export const isRemoteSubscriptionControlMessage = (msg: Message): msg is RemoteSubscriptionControlMessage =>
|
|
190
187
|
msg.type === "remote-subscription-change"
|
|
191
188
|
|
|
192
189
|
export const isRemoteHeadsChanged = (msg: Message): msg is RemoteHeadsChanged =>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { StorageAdapterInterface } from "./StorageAdapterInterface.js"
|
|
1
2
|
import { StorageKey, Chunk } from "./types.js"
|
|
2
3
|
|
|
3
4
|
/** A storage adapter represents some way of storing binary data for a {@link Repo}
|
|
5
|
+
* @deprecated use {@link StorageAdapterInterface}
|
|
4
6
|
*
|
|
5
7
|
* @remarks
|
|
6
8
|
* `StorageAdapter`s provide a key/value storage interface. The keys are arrays of strings
|
|
7
9
|
* ({@link StorageKey}) and the values are binary blobs.
|
|
8
10
|
*/
|
|
9
|
-
export abstract class StorageAdapter {
|
|
11
|
+
export abstract class StorageAdapter implements StorageAdapterInterface {
|
|
10
12
|
/** Load the single value corresponding to `key` */
|
|
11
13
|
abstract load(key: StorageKey): Promise<Uint8Array | undefined>
|
|
12
14
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { StorageKey, Chunk } from "./types.js"
|
|
2
|
+
|
|
3
|
+
/** A storage adapter represents some way of storing binary data for a {@link Repo}
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* `StorageAdapter`s provide a key/value storage interface. The keys are arrays of strings
|
|
7
|
+
* ({@link StorageKey}) and the values are binary blobs.
|
|
8
|
+
*/
|
|
9
|
+
export interface StorageAdapterInterface {
|
|
10
|
+
/** Load the single value corresponding to `key` */
|
|
11
|
+
load(key: StorageKey): Promise<Uint8Array | undefined>
|
|
12
|
+
|
|
13
|
+
/** Save the value `data` to the key `key` */
|
|
14
|
+
save(key: StorageKey, data: Uint8Array): Promise<void>
|
|
15
|
+
|
|
16
|
+
/** Remove the value corresponding to `key` */
|
|
17
|
+
remove(key: StorageKey): Promise<void>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load all values with keys that start with `keyPrefix`.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* The `keyprefix` will match any key that starts with the given array. For example:
|
|
24
|
+
* - `[documentId, "incremental"]` will match all incremental saves
|
|
25
|
+
* - `[documentId]` will match all data for a given document.
|
|
26
|
+
*
|
|
27
|
+
* Be careful! `[documentId]` would also match something like `[documentId, "syncState"]`! We
|
|
28
|
+
* aren't using this yet but keep it in mind.)
|
|
29
|
+
*/
|
|
30
|
+
loadRange(keyPrefix: StorageKey): Promise<Chunk[]>
|
|
31
|
+
|
|
32
|
+
/** Remove all values with keys that start with `keyPrefix` */
|
|
33
|
+
removeRange(keyPrefix: StorageKey): Promise<void>
|
|
34
|
+
}
|
|
@@ -3,7 +3,7 @@ import debug from "debug"
|
|
|
3
3
|
import { headsAreSame } from "../helpers/headsAreSame.js"
|
|
4
4
|
import { mergeArrays } from "../helpers/mergeArrays.js"
|
|
5
5
|
import { type DocumentId } from "../types.js"
|
|
6
|
-
import {
|
|
6
|
+
import { StorageAdapterInterface } from "./StorageAdapterInterface.js"
|
|
7
7
|
import { ChunkInfo, StorageKey, StorageId } from "./types.js"
|
|
8
8
|
import { keyHash, headsHash } from "./keyHash.js"
|
|
9
9
|
import { chunkTypeFromKey } from "./chunkTypeFromKey.js"
|
|
@@ -15,7 +15,7 @@ import * as Uuid from "uuid"
|
|
|
15
15
|
*/
|
|
16
16
|
export class StorageSubsystem {
|
|
17
17
|
/** The storage adapter to use for saving and loading documents */
|
|
18
|
-
#storageAdapter:
|
|
18
|
+
#storageAdapter: StorageAdapterInterface
|
|
19
19
|
|
|
20
20
|
/** Record of the latest heads we've loaded or saved for each document */
|
|
21
21
|
#storedHeads: Map<DocumentId, A.Heads> = new Map()
|
|
@@ -28,7 +28,7 @@ export class StorageSubsystem {
|
|
|
28
28
|
|
|
29
29
|
#log = debug(`automerge-repo:storage-subsystem`)
|
|
30
30
|
|
|
31
|
-
constructor(storageAdapter:
|
|
31
|
+
constructor(storageAdapter: StorageAdapterInterface) {
|
|
32
32
|
this.#storageAdapter = storageAdapter
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -137,11 +137,13 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
137
137
|
|
|
138
138
|
let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
|
|
139
139
|
if (!pendingCallbacks) {
|
|
140
|
-
this.#onLoadSyncState(peerId)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
this.#onLoadSyncState(peerId)
|
|
141
|
+
.then(syncState => {
|
|
142
|
+
this.#initSyncState(peerId, syncState ?? A.initSyncState())
|
|
143
|
+
})
|
|
144
|
+
.catch(err => {
|
|
145
|
+
this.#log(`Error loading sync state for ${peerId}: ${err}`)
|
|
146
|
+
})
|
|
145
147
|
pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = []
|
|
146
148
|
}
|
|
147
149
|
|
|
@@ -225,8 +227,8 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
beginSync(peerIds: PeerId[]) {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
+
const noPeersWithDocument = peerIds.every(
|
|
231
|
+
(peerId) => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
|
|
230
232
|
)
|
|
231
233
|
|
|
232
234
|
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
@@ -240,7 +242,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
240
242
|
this.#checkDocUnavailable()
|
|
241
243
|
|
|
242
244
|
const wasUnavailable = doc === undefined
|
|
243
|
-
if (wasUnavailable &&
|
|
245
|
+
if (wasUnavailable && noPeersWithDocument) {
|
|
244
246
|
return
|
|
245
247
|
}
|
|
246
248
|
|
|
@@ -262,13 +264,15 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
262
264
|
)
|
|
263
265
|
this.#setSyncState(peerId, reparsedSyncState)
|
|
264
266
|
|
|
265
|
-
docPromise
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
docPromise
|
|
268
|
+
.then(doc => {
|
|
269
|
+
if (doc) {
|
|
270
|
+
this.#sendSyncMessage(peerId, doc)
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
.catch(err => {
|
|
274
|
+
this.#log(`Error loading doc for ${peerId}: ${err}`)
|
|
275
|
+
})
|
|
272
276
|
})
|
|
273
277
|
})
|
|
274
278
|
}
|
|
@@ -330,10 +334,10 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
330
334
|
}
|
|
331
335
|
|
|
332
336
|
this.#processAllPendingSyncMessages()
|
|
333
|
-
this.#processSyncMessage(message
|
|
337
|
+
this.#processSyncMessage(message)
|
|
334
338
|
}
|
|
335
339
|
|
|
336
|
-
#processSyncMessage(message: SyncMessage | RequestMessage
|
|
340
|
+
#processSyncMessage(message: SyncMessage | RequestMessage) {
|
|
337
341
|
if (isRequestMessage(message)) {
|
|
338
342
|
this.#peerDocumentStatuses[message.senderId] = "wants"
|
|
339
343
|
}
|
|
@@ -392,7 +396,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
392
396
|
|
|
393
397
|
#processAllPendingSyncMessages() {
|
|
394
398
|
for (const message of this.#pendingSyncMessages) {
|
|
395
|
-
this.#processSyncMessage(message.message
|
|
399
|
+
this.#processSyncMessage(message.message)
|
|
396
400
|
}
|
|
397
401
|
|
|
398
402
|
this.#pendingSyncMessages = []
|
|
@@ -3,15 +3,23 @@ import {
|
|
|
3
3
|
MessageContents,
|
|
4
4
|
OpenDocMessage,
|
|
5
5
|
RepoMessage,
|
|
6
|
-
SyncStateMessage,
|
|
7
6
|
} from "../network/messages.js"
|
|
7
|
+
import { SyncState } from "@automerge/automerge"
|
|
8
|
+
import { PeerId, DocumentId } from "../types.js"
|
|
8
9
|
|
|
9
10
|
export abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
|
|
10
11
|
abstract receiveMessage(message: RepoMessage): void
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export interface SynchronizerEvents {
|
|
14
|
-
message: (
|
|
15
|
-
"sync-state": (
|
|
15
|
+
message: (payload: MessageContents) => void
|
|
16
|
+
"sync-state": (payload: SyncStatePayload) => void
|
|
16
17
|
"open-doc": (arg: OpenDocMessage) => void
|
|
17
18
|
}
|
|
19
|
+
|
|
20
|
+
/** Notify the repo that the sync state has changed */
|
|
21
|
+
export interface SyncStatePayload {
|
|
22
|
+
peerId: PeerId
|
|
23
|
+
documentId: DocumentId
|
|
24
|
+
syncState: SyncState
|
|
25
|
+
}
|