@automerge/automerge-repo 1.1.0-alpha.1 → 1.1.0-alpha.13
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 +12 -7
- package/dist/AutomergeUrl.js +2 -2
- package/dist/RemoteHeadsSubscriptions.d.ts +1 -0
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
- package/dist/RemoteHeadsSubscriptions.js +76 -16
- package/dist/Repo.d.ts +23 -10
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +103 -54
- package/dist/helpers/debounce.js +1 -1
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/pause.js +2 -0
- package/dist/helpers/throttle.js +1 -1
- package/dist/helpers/withTimeout.d.ts.map +1 -1
- package/dist/helpers/withTimeout.js +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/network/NetworkAdapter.d.ts +14 -7
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.js +3 -3
- package/dist/network/NetworkSubsystem.d.ts +4 -8
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +12 -13
- package/dist/network/messages.d.ts +48 -38
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +7 -9
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +7 -2
- package/dist/storage/keyHash.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +5 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +20 -8
- package/dist/synchronizer/Synchronizer.d.ts +12 -3
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/AutomergeUrl.ts +2 -2
- package/src/RemoteHeadsSubscriptions.ts +85 -16
- package/src/Repo.ts +131 -68
- package/src/helpers/debounce.ts +1 -1
- package/src/helpers/pause.ts +4 -0
- package/src/helpers/throttle.ts +1 -1
- package/src/helpers/withTimeout.ts +2 -0
- package/src/index.ts +2 -1
- package/src/network/NetworkAdapter.ts +18 -12
- package/src/network/NetworkSubsystem.ts +23 -24
- package/src/network/messages.ts +77 -68
- package/src/storage/StorageSubsystem.ts +7 -2
- package/src/storage/keyHash.ts +2 -0
- package/src/synchronizer/CollectionSynchronizer.ts +7 -4
- package/src/synchronizer/DocSynchronizer.ts +27 -15
- package/src/synchronizer/Synchronizer.ts +13 -3
- package/test/RemoteHeadsSubscriptions.test.ts +34 -24
- package/test/Repo.test.ts +57 -2
- package/test/StorageSubsystem.test.ts +1 -1
- package/test/helpers/waitForMessages.ts +22 -0
- package/test/remoteHeads.test.ts +197 -72
- package/.eslintrc +0 -28
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
/* c8 ignore start */
|
|
2
|
+
|
|
1
3
|
import { EventEmitter } from "eventemitter3"
|
|
2
4
|
import { PeerId } from "../types.js"
|
|
3
5
|
import { Message } from "./messages.js"
|
|
4
6
|
import { StorageId } from "../storage/types.js"
|
|
5
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
|
+
|
|
6
19
|
/** An interface representing some way to connect to other peers
|
|
7
20
|
*
|
|
8
21
|
* @remarks
|
|
@@ -11,21 +24,15 @@ import { StorageId } from "../storage/types.js"
|
|
|
11
24
|
* until the adapter emits a `ready` event before it starts trying to use it
|
|
12
25
|
*/
|
|
13
26
|
export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> {
|
|
14
|
-
peerId?: PeerId
|
|
15
|
-
|
|
16
|
-
isEphemeral = true
|
|
27
|
+
peerId?: PeerId
|
|
28
|
+
peerMetadata?: PeerMetadata
|
|
17
29
|
|
|
18
30
|
/** Called by the {@link Repo} to start the connection process
|
|
19
31
|
*
|
|
20
32
|
* @argument peerId - the peerId of this repo
|
|
21
|
-
* @argument
|
|
22
|
-
* @argument isEphemeral - weather or not the other end should persist our sync state
|
|
33
|
+
* @argument peerMetadata - how this adapter should present itself to other peers
|
|
23
34
|
*/
|
|
24
|
-
abstract connect(
|
|
25
|
-
peerId: PeerId,
|
|
26
|
-
storageId: StorageId | undefined,
|
|
27
|
-
isEphemeral: boolean
|
|
28
|
-
): void
|
|
35
|
+
abstract connect(peerId: PeerId, peerMetadata?: PeerMetadata): void
|
|
29
36
|
|
|
30
37
|
/** Called by the {@link Repo} to send a message to a peer
|
|
31
38
|
*
|
|
@@ -62,8 +69,7 @@ export interface OpenPayload {
|
|
|
62
69
|
|
|
63
70
|
export interface PeerCandidatePayload {
|
|
64
71
|
peerId: PeerId
|
|
65
|
-
|
|
66
|
-
isEphemeral: boolean
|
|
72
|
+
peerMetadata: PeerMetadata
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
export interface PeerDisconnectedPayload {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import debug from "debug"
|
|
2
2
|
import { EventEmitter } from "eventemitter3"
|
|
3
3
|
import { PeerId, SessionId } from "../types.js"
|
|
4
|
-
import {
|
|
4
|
+
import type {
|
|
5
|
+
NetworkAdapter,
|
|
6
|
+
PeerDisconnectedPayload,
|
|
7
|
+
PeerMetadata,
|
|
8
|
+
} from "./NetworkAdapter.js"
|
|
5
9
|
import {
|
|
6
10
|
EphemeralMessage,
|
|
7
11
|
MessageContents,
|
|
8
12
|
RepoMessage,
|
|
9
13
|
isEphemeralMessage,
|
|
10
|
-
|
|
14
|
+
isRepoMessage,
|
|
11
15
|
} from "./messages.js"
|
|
12
|
-
import { StorageId } from "../storage/types.js"
|
|
13
16
|
|
|
14
17
|
type EphemeralMessageSource = `${PeerId}:${SessionId}`
|
|
15
18
|
|
|
@@ -29,8 +32,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
29
32
|
constructor(
|
|
30
33
|
adapters: NetworkAdapter[],
|
|
31
34
|
public peerId = randomPeerId(),
|
|
32
|
-
private
|
|
33
|
-
private isEphemeral: boolean
|
|
35
|
+
private peerMetadata: Promise<PeerMetadata>
|
|
34
36
|
) {
|
|
35
37
|
super()
|
|
36
38
|
this.#log = debug(`automerge-repo:network:${this.peerId}`)
|
|
@@ -52,21 +54,17 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
52
54
|
}
|
|
53
55
|
})
|
|
54
56
|
|
|
55
|
-
networkAdapter.on(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.#log(`peer candidate: ${peerId} `)
|
|
57
|
+
networkAdapter.on("peer-candidate", ({ peerId, peerMetadata }) => {
|
|
58
|
+
this.#log(`peer candidate: ${peerId} `)
|
|
59
|
+
// TODO: This is where authentication would happen
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// TODO: handle losing a server here
|
|
64
|
-
this.#adaptersByPeer[peerId] = networkAdapter
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.emit("peer", { peerId, storageId, isEphemeral })
|
|
61
|
+
if (!this.#adaptersByPeer[peerId]) {
|
|
62
|
+
// TODO: handle losing a server here
|
|
63
|
+
this.#adaptersByPeer[peerId] = networkAdapter
|
|
68
64
|
}
|
|
69
|
-
|
|
65
|
+
|
|
66
|
+
this.emit("peer", { peerId, peerMetadata })
|
|
67
|
+
})
|
|
70
68
|
|
|
71
69
|
networkAdapter.on("peer-disconnected", ({ peerId }) => {
|
|
72
70
|
this.#log(`peer disconnected: ${peerId} `)
|
|
@@ -75,7 +73,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
75
73
|
})
|
|
76
74
|
|
|
77
75
|
networkAdapter.on("message", msg => {
|
|
78
|
-
if (!
|
|
76
|
+
if (!isRepoMessage(msg)) {
|
|
79
77
|
this.#log(`invalid message: ${JSON.stringify(msg)}`)
|
|
80
78
|
return
|
|
81
79
|
}
|
|
@@ -107,8 +105,10 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
107
105
|
})
|
|
108
106
|
})
|
|
109
107
|
|
|
110
|
-
this.
|
|
111
|
-
networkAdapter.connect(this.peerId,
|
|
108
|
+
this.peerMetadata.then(peerMetadata => {
|
|
109
|
+
networkAdapter.connect(this.peerId, peerMetadata)
|
|
110
|
+
}).catch(err => {
|
|
111
|
+
this.#log("error connecting to network", err)
|
|
112
112
|
})
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -146,7 +146,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const outbound = prepareMessage(message)
|
|
149
|
-
this.#log("sending message", outbound)
|
|
149
|
+
this.#log("sending message %o", outbound)
|
|
150
150
|
peer.send(outbound as RepoMessage)
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -182,6 +182,5 @@ export interface NetworkSubsystemEvents {
|
|
|
182
182
|
|
|
183
183
|
export interface PeerPayload {
|
|
184
184
|
peerId: PeerId
|
|
185
|
-
|
|
186
|
-
isEphemeral: boolean
|
|
185
|
+
peerMetadata: PeerMetadata
|
|
187
186
|
}
|
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,48 +82,44 @@ 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
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Sent by a {@link Repo} to add or remove storage IDs from a remote peer's subscription.
|
|
97
|
+
*/
|
|
98
|
+
export type RemoteSubscriptionControlMessage = {
|
|
99
|
+
type: "remote-subscription-change"
|
|
97
100
|
senderId: PeerId
|
|
98
|
-
|
|
99
|
-
/** The peer ID of the recipient of this message */
|
|
100
101
|
targetId: PeerId
|
|
101
102
|
|
|
102
|
-
/** The
|
|
103
|
-
|
|
104
|
-
}
|
|
103
|
+
/** The storage IDs to add to the subscription */
|
|
104
|
+
add?: StorageId[]
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
senderId: PeerId,
|
|
109
|
-
targetId: PeerId,
|
|
110
|
-
add?: StorageId[],
|
|
111
|
-
remove?: StorageId[],
|
|
106
|
+
/** The storage IDs to remove from the subscription */
|
|
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
|
-
type: "remote-heads-changed"
|
|
116
|
-
senderId: PeerId
|
|
117
|
-
targetId: PeerId
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
type: "remote-heads-changed"
|
|
115
|
+
senderId: PeerId
|
|
116
|
+
targetId: PeerId
|
|
117
|
+
|
|
118
|
+
/** The document ID of the document that has changed */
|
|
119
|
+
documentId: DocumentId
|
|
120
|
+
|
|
121
|
+
/** The document's new heads */
|
|
122
|
+
newHeads: { [key: StorageId]: { heads: string[]; timestamp: number } }
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
/** These are message types that a {@link NetworkAdapter} surfaces to a {@link Repo}. */
|
|
@@ -128,15 +131,17 @@ export type RepoMessage =
|
|
|
128
131
|
| RemoteSubscriptionControlMessage
|
|
129
132
|
| RemoteHeadsChanged
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
/** These are message types that are handled by the {@link CollectionSynchronizer}.*/
|
|
135
|
+
export type DocMessage =
|
|
136
|
+
| SyncMessage
|
|
137
|
+
| EphemeralMessage
|
|
138
|
+
| RequestMessage
|
|
139
|
+
| DocumentUnavailableMessage
|
|
135
140
|
|
|
136
141
|
/**
|
|
137
142
|
* The contents of a message, without the sender ID or other properties added by the {@link NetworkSubsystem})
|
|
138
143
|
*/
|
|
139
|
-
export type MessageContents<T extends Message =
|
|
144
|
+
export type MessageContents<T extends Message = RepoMessage> =
|
|
140
145
|
T extends EphemeralMessage
|
|
141
146
|
? Omit<T, "senderId" | "count" | "sessionId">
|
|
142
147
|
: Omit<T, "senderId">
|
|
@@ -148,18 +153,21 @@ export interface SyncStateMessage {
|
|
|
148
153
|
syncState: SyncState
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
/** Notify the repo that a peer started syncing with a doc */
|
|
157
|
+
export interface OpenDocMessage {
|
|
158
|
+
peerId: PeerId
|
|
159
|
+
documentId: DocumentId
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
// TYPE GUARDS
|
|
152
163
|
|
|
153
|
-
export const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
isDocumentUnavailableMessage(message) ||
|
|
161
|
-
isRemoteSubscriptionControlMessage(message) ||
|
|
162
|
-
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)
|
|
163
171
|
|
|
164
172
|
// prettier-ignore
|
|
165
173
|
export const isDocumentUnavailableMessage = (msg: Message): msg is DocumentUnavailableMessage =>
|
|
@@ -174,6 +182,7 @@ export const isSyncMessage = (msg: Message): msg is SyncMessage =>
|
|
|
174
182
|
export const isEphemeralMessage = (msg: Message): msg is EphemeralMessage =>
|
|
175
183
|
msg.type === "ephemeral"
|
|
176
184
|
|
|
185
|
+
// prettier-ignore
|
|
177
186
|
export const isRemoteSubscriptionControlMessage = (msg: Message): msg is RemoteSubscriptionControlMessage =>
|
|
178
187
|
msg.type === "remote-subscription-change"
|
|
179
188
|
|
|
@@ -33,7 +33,7 @@ export class StorageSubsystem {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async id(): Promise<StorageId> {
|
|
36
|
-
|
|
36
|
+
const storedId = await this.#storageAdapter.load(["storage-adapter-id"])
|
|
37
37
|
|
|
38
38
|
let id: StorageId
|
|
39
39
|
if (storedId) {
|
|
@@ -279,6 +279,11 @@ export class StorageSubsystem {
|
|
|
279
279
|
incrementalSize += chunk.size
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
-
|
|
282
|
+
// if the file is currently small, don't worry, just compact
|
|
283
|
+
// this might seem a bit arbitrary (1k is arbitrary) but is designed to ensure compaction
|
|
284
|
+
// for documents with only a single large change on top of an empty (or nearly empty) document
|
|
285
|
+
// for example: imported NPM modules, images, etc.
|
|
286
|
+
// if we have even more incrementals (so far) than the snapshot, compact
|
|
287
|
+
return snapshotSize < 1024 || incrementalSize >= snapshotSize
|
|
283
288
|
}
|
|
284
289
|
}
|
package/src/storage/keyHash.ts
CHANGED
|
@@ -7,11 +7,13 @@ export function keyHash(binary: Uint8Array) {
|
|
|
7
7
|
const hash = sha256.hash(binary)
|
|
8
8
|
return bufferToHexString(hash)
|
|
9
9
|
}
|
|
10
|
+
|
|
10
11
|
export function headsHash(heads: A.Heads): string {
|
|
11
12
|
const encoder = new TextEncoder()
|
|
12
13
|
const headsbinary = mergeArrays(heads.map((h: string) => encoder.encode(h)))
|
|
13
14
|
return keyHash(headsbinary)
|
|
14
15
|
}
|
|
16
|
+
|
|
15
17
|
function bufferToHexString(data: Uint8Array) {
|
|
16
18
|
return Array.from(data, byte => byte.toString(16).padStart(2, "0")).join("")
|
|
17
19
|
}
|
|
@@ -2,7 +2,7 @@ import debug from "debug"
|
|
|
2
2
|
import { DocHandle } from "../DocHandle.js"
|
|
3
3
|
import { stringifyAutomergeUrl } from "../AutomergeUrl.js"
|
|
4
4
|
import { Repo } from "../Repo.js"
|
|
5
|
-
import { DocMessage
|
|
5
|
+
import { DocMessage } from "../network/messages.js"
|
|
6
6
|
import { DocumentId, PeerId } from "../types.js"
|
|
7
7
|
import { DocSynchronizer } from "./DocSynchronizer.js"
|
|
8
8
|
import { Synchronizer } from "./Synchronizer.js"
|
|
@@ -42,18 +42,20 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
42
42
|
return
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const
|
|
46
|
-
|
|
45
|
+
const { storageId, isEphemeral } =
|
|
46
|
+
this.repo.peerMetadataByPeerId[peerId] || {}
|
|
47
|
+
if (!storageId || isEphemeral) {
|
|
47
48
|
return
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
return this.repo.storageSubsystem.loadSyncState(
|
|
51
52
|
handle.documentId,
|
|
52
|
-
|
|
53
|
+
storageId
|
|
53
54
|
)
|
|
54
55
|
},
|
|
55
56
|
})
|
|
56
57
|
docSynchronizer.on("message", event => this.emit("message", event))
|
|
58
|
+
docSynchronizer.on("open-doc", event => this.emit("open-doc", event))
|
|
57
59
|
docSynchronizer.on("sync-state", event => this.emit("sync-state", event))
|
|
58
60
|
return docSynchronizer
|
|
59
61
|
}
|
|
@@ -115,6 +117,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
// TODO: implement this
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
118
121
|
removeDocument(documentId: DocumentId) {
|
|
119
122
|
throw new Error("not implemented")
|
|
120
123
|
}
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
import { PeerId } from "../types.js"
|
|
21
21
|
import { Synchronizer } from "./Synchronizer.js"
|
|
22
22
|
import { throttle } from "../helpers/throttle.js"
|
|
23
|
-
import { headsAreSame } from "../helpers/headsAreSame.js"
|
|
24
23
|
|
|
25
24
|
type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
|
|
26
25
|
|
|
@@ -124,9 +123,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
#withSyncState(peerId: PeerId, callback: (syncState: A.SyncState) => void) {
|
|
127
|
-
|
|
128
|
-
this.#peers.push(peerId)
|
|
129
|
-
}
|
|
126
|
+
this.#addPeer(peerId)
|
|
130
127
|
|
|
131
128
|
if (!(peerId in this.#peerDocumentStatuses)) {
|
|
132
129
|
this.#peerDocumentStatuses[peerId] = "unknown"
|
|
@@ -140,15 +137,26 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
140
137
|
|
|
141
138
|
let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
|
|
142
139
|
if (!pendingCallbacks) {
|
|
143
|
-
this.#onLoadSyncState(peerId)
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
})
|
|
146
147
|
pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = []
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
pendingCallbacks.push(callback)
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
#addPeer(peerId: PeerId) {
|
|
154
|
+
if (!this.#peers.includes(peerId)) {
|
|
155
|
+
this.#peers.push(peerId)
|
|
156
|
+
this.emit("open-doc", { documentId: this.documentId, peerId })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
#initSyncState(peerId: PeerId, syncState: A.SyncState) {
|
|
153
161
|
const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
|
|
154
162
|
if (pendingCallbacks) {
|
|
@@ -256,11 +264,15 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
256
264
|
)
|
|
257
265
|
this.#setSyncState(peerId, reparsedSyncState)
|
|
258
266
|
|
|
259
|
-
docPromise
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
})
|
|
264
276
|
})
|
|
265
277
|
})
|
|
266
278
|
}
|
|
@@ -322,10 +334,10 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
322
334
|
}
|
|
323
335
|
|
|
324
336
|
this.#processAllPendingSyncMessages()
|
|
325
|
-
this.#processSyncMessage(message
|
|
337
|
+
this.#processSyncMessage(message)
|
|
326
338
|
}
|
|
327
339
|
|
|
328
|
-
#processSyncMessage(message: SyncMessage | RequestMessage
|
|
340
|
+
#processSyncMessage(message: SyncMessage | RequestMessage) {
|
|
329
341
|
if (isRequestMessage(message)) {
|
|
330
342
|
this.#peerDocumentStatuses[message.senderId] = "wants"
|
|
331
343
|
}
|
|
@@ -384,7 +396,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
384
396
|
|
|
385
397
|
#processAllPendingSyncMessages() {
|
|
386
398
|
for (const message of this.#pendingSyncMessages) {
|
|
387
|
-
this.#processSyncMessage(message.message
|
|
399
|
+
this.#processSyncMessage(message.message)
|
|
388
400
|
}
|
|
389
401
|
|
|
390
402
|
this.#pendingSyncMessages = []
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { EventEmitter } from "eventemitter3"
|
|
2
2
|
import {
|
|
3
3
|
MessageContents,
|
|
4
|
+
OpenDocMessage,
|
|
4
5
|
RepoMessage,
|
|
5
|
-
SyncStateMessage,
|
|
6
6
|
} from "../network/messages.js"
|
|
7
|
+
import { SyncState } from "@automerge/automerge"
|
|
8
|
+
import { PeerId, DocumentId } from "../types.js"
|
|
7
9
|
|
|
8
10
|
export abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
|
|
9
11
|
abstract receiveMessage(message: RepoMessage): void
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export interface SynchronizerEvents {
|
|
13
|
-
message: (
|
|
14
|
-
"sync-state": (
|
|
15
|
+
message: (payload: MessageContents) => void
|
|
16
|
+
"sync-state": (payload: SyncStatePayload) => void
|
|
17
|
+
"open-doc": (arg: OpenDocMessage) => void
|
|
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
|
|
15
25
|
}
|