@automerge/automerge-repo 1.0.6 → 1.0.8
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/.eslintrc +1 -1
- package/dist/DocHandle.d.ts +8 -8
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +4 -8
- package/dist/DocUrl.d.ts.map +1 -1
- package/dist/Repo.d.ts +6 -3
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +20 -19
- package/dist/helpers/cbor.d.ts +2 -2
- package/dist/helpers/cbor.d.ts.map +1 -1
- package/dist/helpers/cbor.js +1 -1
- package/dist/helpers/debounce.d.ts +14 -0
- package/dist/helpers/debounce.d.ts.map +1 -0
- package/dist/helpers/debounce.js +21 -0
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/pause.js +3 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +2 -2
- package/dist/helpers/throttle.d.ts +28 -0
- package/dist/helpers/throttle.d.ts.map +1 -0
- package/dist/helpers/throttle.js +39 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/network/NetworkAdapter.d.ts +3 -3
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.d.ts +2 -2
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +30 -18
- package/dist/network/messages.d.ts +38 -68
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +13 -21
- package/dist/storage/StorageSubsystem.d.ts +1 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +9 -9
- package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -3
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +3 -3
- package/dist/synchronizer/DocSynchronizer.d.ts +4 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +25 -34
- package/dist/synchronizer/Synchronizer.d.ts +2 -2
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +8 -6
- package/fuzz/tsconfig.json +8 -0
- package/package.json +5 -13
- package/src/DocHandle.ts +12 -15
- package/src/DocUrl.ts +3 -1
- package/src/Repo.ts +36 -29
- package/src/helpers/cbor.ts +4 -4
- package/src/helpers/debounce.ts +25 -0
- package/src/helpers/headsAreSame.ts +1 -1
- package/src/helpers/pause.ts +7 -2
- package/src/helpers/tests/network-adapter-tests.ts +3 -3
- package/src/helpers/throttle.ts +43 -0
- package/src/helpers/withTimeout.ts +2 -2
- package/src/index.ts +36 -29
- package/src/network/NetworkAdapter.ts +7 -3
- package/src/network/NetworkSubsystem.ts +31 -23
- package/src/network/messages.ts +88 -151
- package/src/storage/StorageSubsystem.ts +12 -12
- package/src/synchronizer/CollectionSynchronizer.ts +7 -16
- package/src/synchronizer/DocSynchronizer.ts +42 -53
- package/src/synchronizer/Synchronizer.ts +2 -2
- package/src/types.ts +8 -3
- package/test/CollectionSynchronizer.test.ts +58 -53
- package/test/DocHandle.test.ts +35 -36
- package/test/DocSynchronizer.test.ts +5 -8
- package/test/Network.test.ts +1 -0
- package/test/Repo.test.ts +229 -158
- package/test/StorageSubsystem.test.ts +6 -9
- package/test/helpers/DummyNetworkAdapter.ts +9 -4
- package/test/helpers/DummyStorageAdapter.ts +4 -2
- package/test/tsconfig.json +8 -0
- package/typedoc.json +3 -3
- package/.mocharc.json +0 -5
- package/dist/EphemeralData.d.ts +0 -20
- package/dist/EphemeralData.d.ts.map +0 -1
- package/dist/EphemeralData.js +0 -1
- package/src/EphemeralData.ts +0 -17
package/src/network/messages.ts
CHANGED
|
@@ -1,161 +1,70 @@
|
|
|
1
|
-
|
|
2
|
-
import { SessionId } from "../EphemeralData.js"
|
|
3
|
-
export { type SessionId } from "../EphemeralData.js"
|
|
4
|
-
import { DocumentId, PeerId } from "../types.js"
|
|
5
|
-
|
|
6
|
-
export function isValidMessage(
|
|
7
|
-
message: NetworkAdapterMessage
|
|
8
|
-
): message is
|
|
9
|
-
| SyncMessage
|
|
10
|
-
| EphemeralMessage
|
|
11
|
-
| RequestMessage
|
|
12
|
-
| DocumentUnavailableMessage {
|
|
13
|
-
return (
|
|
14
|
-
typeof message === "object" &&
|
|
15
|
-
typeof message.type === "string" &&
|
|
16
|
-
typeof message.senderId === "string" &&
|
|
17
|
-
(isSyncMessage(message) ||
|
|
18
|
-
isEphemeralMessage(message) ||
|
|
19
|
-
isRequestMessage(message) ||
|
|
20
|
-
isDocumentUnavailableMessage(message))
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function isDocumentUnavailableMessage(
|
|
25
|
-
message: NetworkAdapterMessage
|
|
26
|
-
): message is DocumentUnavailableMessage {
|
|
27
|
-
return message.type === "doc-unavailable"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function isRequestMessage(
|
|
31
|
-
message: NetworkAdapterMessage
|
|
32
|
-
): message is RequestMessage {
|
|
33
|
-
return message.type === "request"
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function isSyncMessage(
|
|
37
|
-
message: NetworkAdapterMessage
|
|
38
|
-
): message is SyncMessage {
|
|
39
|
-
return message.type === "sync"
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function isEphemeralMessage(
|
|
43
|
-
message: NetworkAdapterMessage | MessageContents
|
|
44
|
-
): message is EphemeralMessage | EphemeralMessageContents {
|
|
45
|
-
return message.type === "ephemeral"
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface SyncMessageEnvelope {
|
|
49
|
-
senderId: PeerId
|
|
50
|
-
}
|
|
1
|
+
import { DocumentId, PeerId, SessionId } from "../types.js"
|
|
51
2
|
|
|
52
|
-
export interface SyncMessageContents {
|
|
53
|
-
type: "sync"
|
|
54
|
-
data: Uint8Array
|
|
55
|
-
targetId: PeerId
|
|
56
|
-
documentId: DocumentId
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type static_assert<T extends true> = never
|
|
60
|
-
|
|
61
|
-
// export type SyncMessage = SyncMessageEnvelope & SyncMessageContents
|
|
62
|
-
// we inline the definitions here rather than using the above type alias because
|
|
63
|
-
// otherwise typedoc can't produce nice docs. We use this `static_assert` thing
|
|
64
|
-
// to make sure the types continue to line up though.
|
|
65
|
-
type _check_sync_message = static_assert<SyncMessage extends SyncMessageEnvelope & SyncMessageContents ? true : false>
|
|
66
3
|
/**
|
|
67
4
|
* A sync message for a particular document
|
|
68
5
|
*/
|
|
69
6
|
export type SyncMessage = {
|
|
7
|
+
type: "sync"
|
|
8
|
+
|
|
70
9
|
/** The peer ID of the sender of this message */
|
|
71
10
|
senderId: PeerId
|
|
72
|
-
|
|
73
|
-
/** The automerge sync message */
|
|
74
|
-
data: Uint8Array
|
|
11
|
+
|
|
75
12
|
/** The peer ID of the recipient of this message */
|
|
76
13
|
targetId: PeerId
|
|
77
|
-
/** The document ID of the document this message is for */
|
|
78
|
-
documentId: DocumentId
|
|
79
|
-
}
|
|
80
14
|
|
|
15
|
+
/** The automerge sync message */
|
|
16
|
+
data: Uint8Array
|
|
81
17
|
|
|
82
|
-
|
|
83
|
-
senderId: PeerId
|
|
84
|
-
count: number
|
|
85
|
-
sessionId: SessionId
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface EphemeralMessageContents {
|
|
89
|
-
type: "ephemeral"
|
|
90
|
-
targetId: PeerId
|
|
18
|
+
/** The document ID of the document this message is for */
|
|
91
19
|
documentId: DocumentId
|
|
92
|
-
data: Uint8Array
|
|
93
20
|
}
|
|
94
21
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
type _check_ephemeral_message = static_assert<EphemeralMessage extends EphemeralMessageEnvelope & EphemeralMessageContents ? true : false>
|
|
98
|
-
|
|
99
|
-
/** An ephemeral message
|
|
100
|
-
*
|
|
22
|
+
/** An ephemeral message
|
|
23
|
+
*
|
|
101
24
|
* @remarks
|
|
102
25
|
* Ephemeral messages are not persisted anywhere and have no particular
|
|
103
|
-
* structure. `automerge-repo` will gossip them around, in order to avoid
|
|
26
|
+
* structure. `automerge-repo` will gossip them around, in order to avoid
|
|
104
27
|
* eternal loops of ephemeral messages every message has a session ID, which
|
|
105
28
|
* is a random number generated by the sender at startup time, and a sequence
|
|
106
29
|
* number. The combination of these two things allows us to discard messages
|
|
107
30
|
* we have already seen.
|
|
108
31
|
* */
|
|
109
32
|
export type EphemeralMessage = {
|
|
110
|
-
|
|
33
|
+
type: "ephemeral"
|
|
34
|
+
|
|
35
|
+
/** The peer ID of the sender of this message */
|
|
111
36
|
senderId: PeerId
|
|
37
|
+
|
|
38
|
+
/** The peer ID of the recipient of this message */
|
|
39
|
+
targetId: PeerId
|
|
40
|
+
|
|
112
41
|
/** A sequence number which must be incremented for each message sent by this peer */
|
|
113
42
|
count: number
|
|
43
|
+
|
|
114
44
|
/** The ID of the session this message is part of. The sequence number for a given session always increases */
|
|
115
45
|
sessionId: SessionId
|
|
116
|
-
|
|
117
|
-
/** The peer this message is for */
|
|
118
|
-
targetId: PeerId
|
|
46
|
+
|
|
119
47
|
/** The document ID this message pertains to */
|
|
120
48
|
documentId: DocumentId
|
|
49
|
+
|
|
121
50
|
/** The actual data of the message */
|
|
122
51
|
data: Uint8Array
|
|
123
52
|
}
|
|
124
53
|
|
|
125
|
-
export interface DocumentUnavailableMessageContents {
|
|
126
|
-
type: "doc-unavailable"
|
|
127
|
-
documentId: DocumentId
|
|
128
|
-
targetId: PeerId
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// export type DocumentUnavailableMessage = SyncMessageEnvelope & DocumentUnavailableMessageContents
|
|
133
|
-
// Inline definitions to get good docs, but check the types line up
|
|
134
|
-
type _check_doc_unavailable = static_assert<DocumentUnavailableMessage extends SyncMessageEnvelope & DocumentUnavailableMessageContents ? true : false>
|
|
135
54
|
/** Sent by a {@link Repo} to indicate that it does not have the document and none of it's connected peers do either */
|
|
136
55
|
export type DocumentUnavailableMessage = {
|
|
137
|
-
/** The peer who sent this message */
|
|
138
|
-
senderId: PeerId
|
|
139
56
|
type: "doc-unavailable"
|
|
140
|
-
/** The document which the peer claims it doesn't have */
|
|
141
|
-
documentId: DocumentId
|
|
142
|
-
/** The peer this message is for */
|
|
143
|
-
targetId: PeerId
|
|
144
|
-
}
|
|
145
57
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
58
|
+
/** The peer ID of the sender of this message */
|
|
59
|
+
senderId: PeerId
|
|
60
|
+
|
|
61
|
+
/** The peer ID of the recipient of this message */
|
|
149
62
|
targetId: PeerId
|
|
63
|
+
|
|
64
|
+
/** The document which the peer claims it doesn't have */
|
|
150
65
|
documentId: DocumentId
|
|
151
66
|
}
|
|
152
67
|
|
|
153
|
-
// export type RequestMessage = SyncMessageEnvelope & RequestMessageContents
|
|
154
|
-
// Inline definitions to get good docs, but check the types line up
|
|
155
|
-
type _check_request_message = static_assert<RequestMessage extends SyncMessageEnvelope & RequestMessageContents ? true : false>
|
|
156
|
-
// We inline the definitions here rather than using the above type alias because
|
|
157
|
-
// otherwise typedoc can't produce nice docs without exporting SyncMessageEnvelope
|
|
158
|
-
// and RequestMessageContents
|
|
159
68
|
/** Sent by a {@link Repo} to request a document from a peer
|
|
160
69
|
*
|
|
161
70
|
* @remarks
|
|
@@ -163,58 +72,86 @@ type _check_request_message = static_assert<RequestMessage extends SyncMessageEn
|
|
|
163
72
|
* as the initial sync message when asking the other peer if it has the document.
|
|
164
73
|
* */
|
|
165
74
|
export type RequestMessage = {
|
|
166
|
-
/** The peer who sent this message */
|
|
167
|
-
senderId: PeerId
|
|
168
75
|
type: "request"
|
|
76
|
+
|
|
77
|
+
/** The peer ID of the sender of this message */
|
|
78
|
+
senderId: PeerId
|
|
79
|
+
|
|
80
|
+
/** The peer ID of the recipient of this message */
|
|
81
|
+
targetId: PeerId
|
|
82
|
+
|
|
169
83
|
/** The initial automerge sync message */
|
|
170
84
|
data: Uint8Array
|
|
171
|
-
|
|
172
|
-
targetId: PeerId
|
|
85
|
+
|
|
173
86
|
/** The document ID this message requests */
|
|
174
87
|
documentId: DocumentId
|
|
175
88
|
}
|
|
176
89
|
|
|
177
|
-
export type MessageContents =
|
|
178
|
-
| SyncMessageContents
|
|
179
|
-
| EphemeralMessageContents
|
|
180
|
-
| RequestMessageContents
|
|
181
|
-
| DocumentUnavailableMessageContents
|
|
182
|
-
|
|
183
|
-
/** The type of messages that {@link Repo} sends and receive to {@link NetworkAdapter}s */
|
|
184
|
-
export type Message =
|
|
185
|
-
| SyncMessage
|
|
186
|
-
| EphemeralMessage
|
|
187
|
-
| RequestMessage
|
|
188
|
-
| DocumentUnavailableMessage
|
|
189
|
-
|
|
190
|
-
export type SynchronizerMessage =
|
|
191
|
-
| SyncMessage
|
|
192
|
-
| RequestMessage
|
|
193
|
-
| DocumentUnavailableMessage
|
|
194
|
-
| EphemeralMessage
|
|
195
|
-
|
|
196
|
-
|
|
197
90
|
/** Notify the network that we have arrived so everyone knows our peer ID */
|
|
198
91
|
export type ArriveMessage = {
|
|
199
|
-
/** Our peer ID */
|
|
200
|
-
senderId: PeerId
|
|
201
92
|
type: "arrive"
|
|
93
|
+
|
|
94
|
+
/** The peer ID of the sender of this message */
|
|
95
|
+
senderId: PeerId
|
|
96
|
+
|
|
97
|
+
/** Arrive messages don't have a targetId */
|
|
98
|
+
targetId: never
|
|
202
99
|
}
|
|
203
100
|
|
|
204
101
|
/** Respond to an arriving peer with our peer ID */
|
|
205
102
|
export type WelcomeMessage = {
|
|
206
|
-
|
|
103
|
+
type: "welcome"
|
|
104
|
+
|
|
105
|
+
/** The peer ID of the recipient sender this message */
|
|
207
106
|
senderId: PeerId
|
|
208
|
-
|
|
107
|
+
|
|
108
|
+
/** The peer ID of the recipient of this message */
|
|
209
109
|
targetId: PeerId
|
|
210
|
-
type: "welcome"
|
|
211
110
|
}
|
|
212
111
|
|
|
213
|
-
/**
|
|
112
|
+
/** These are message types that a {@link NetworkAdapter} surfaces to a {@link Repo}. */
|
|
113
|
+
export type RepoMessage =
|
|
114
|
+
| SyncMessage
|
|
115
|
+
| EphemeralMessage
|
|
116
|
+
| RequestMessage
|
|
117
|
+
| DocumentUnavailableMessage
|
|
118
|
+
|
|
119
|
+
/** These are all the message types that a {@link NetworkAdapter} might see.
|
|
214
120
|
*
|
|
215
121
|
* @remarks
|
|
216
|
-
* It is not _required_ that a {@link NetworkAdapter} use
|
|
217
|
-
*
|
|
218
|
-
* transport. However, this type is a useful default.
|
|
122
|
+
* It is not _required_ that a {@link NetworkAdapter} use these types: They are free to use
|
|
123
|
+
* whatever message type makes sense for their transport. However, this type is a useful default.
|
|
219
124
|
* */
|
|
220
|
-
export type
|
|
125
|
+
export type Message = RepoMessage | ArriveMessage | WelcomeMessage
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The contents of a message, without the sender ID or other properties added by the {@link NetworkSubsystem})
|
|
129
|
+
*/
|
|
130
|
+
export type MessageContents<T extends Message = Message> =
|
|
131
|
+
T extends EphemeralMessage
|
|
132
|
+
? Omit<T, "senderId" | "count" | "sessionId">
|
|
133
|
+
: Omit<T, "senderId">
|
|
134
|
+
|
|
135
|
+
// TYPE GUARDS
|
|
136
|
+
|
|
137
|
+
export const isValidRepoMessage = (message: Message): message is RepoMessage =>
|
|
138
|
+
typeof message === "object" &&
|
|
139
|
+
typeof message.type === "string" &&
|
|
140
|
+
typeof message.senderId === "string" &&
|
|
141
|
+
(isSyncMessage(message) ||
|
|
142
|
+
isEphemeralMessage(message) ||
|
|
143
|
+
isRequestMessage(message) ||
|
|
144
|
+
isDocumentUnavailableMessage(message))
|
|
145
|
+
|
|
146
|
+
// prettier-ignore
|
|
147
|
+
export const isDocumentUnavailableMessage = (msg: Message): msg is DocumentUnavailableMessage =>
|
|
148
|
+
msg.type === "doc-unavailable"
|
|
149
|
+
|
|
150
|
+
export const isRequestMessage = (msg: Message): msg is RequestMessage =>
|
|
151
|
+
msg.type === "request"
|
|
152
|
+
|
|
153
|
+
export const isSyncMessage = (msg: Message): msg is SyncMessage =>
|
|
154
|
+
msg.type === "sync"
|
|
155
|
+
|
|
156
|
+
export const isEphemeralMessage = (msg: Message): msg is EphemeralMessage =>
|
|
157
|
+
msg.type === "ephemeral"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as A from "@automerge/automerge/next"
|
|
2
|
-
import { StorageAdapter, StorageKey } from "./StorageAdapter.js"
|
|
3
|
-
import * as sha256 from "fast-sha256"
|
|
4
|
-
import { type DocumentId } from "../types.js"
|
|
5
|
-
import { mergeArrays } from "../helpers/mergeArrays.js"
|
|
6
2
|
import debug from "debug"
|
|
3
|
+
import * as sha256 from "fast-sha256"
|
|
7
4
|
import { headsAreSame } from "../helpers/headsAreSame.js"
|
|
5
|
+
import { mergeArrays } from "../helpers/mergeArrays.js"
|
|
6
|
+
import { type DocumentId } from "../types.js"
|
|
7
|
+
import { StorageAdapter, StorageKey } from "./StorageAdapter.js"
|
|
8
8
|
|
|
9
9
|
// Metadata about a chunk of data loaded from storage. This is stored on the
|
|
10
10
|
// StorageSubsystem so when we are compacting we know what chunks we can safely delete
|
|
@@ -24,8 +24,8 @@ function keyHash(binary: Uint8Array) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function headsHash(heads: A.Heads): string {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const encoder = new TextEncoder()
|
|
28
|
+
const headsbinary = mergeArrays(heads.map((h: string) => encoder.encode(h)))
|
|
29
29
|
return keyHash(headsbinary)
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -53,7 +53,7 @@ export class StorageSubsystem {
|
|
|
53
53
|
if (!this.#chunkInfos.has(documentId)) {
|
|
54
54
|
this.#chunkInfos.set(documentId, [])
|
|
55
55
|
}
|
|
56
|
-
this.#chunkInfos.get(documentId)
|
|
56
|
+
this.#chunkInfos.get(documentId)!.push({
|
|
57
57
|
key,
|
|
58
58
|
type: "incremental",
|
|
59
59
|
size: binary.length,
|
|
@@ -122,18 +122,18 @@ export class StorageSubsystem {
|
|
|
122
122
|
if (!this.#shouldSave(documentId, doc)) {
|
|
123
123
|
return
|
|
124
124
|
}
|
|
125
|
-
|
|
125
|
+
const sourceChunks = this.#chunkInfos.get(documentId) ?? []
|
|
126
126
|
if (this.#shouldCompact(sourceChunks)) {
|
|
127
|
-
this.#saveTotal(documentId, doc, sourceChunks)
|
|
127
|
+
void this.#saveTotal(documentId, doc, sourceChunks)
|
|
128
128
|
} else {
|
|
129
|
-
this.#saveIncremental(documentId, doc)
|
|
129
|
+
void this.#saveIncremental(documentId, doc)
|
|
130
130
|
}
|
|
131
131
|
this.#storedHeads.set(documentId, A.getHeads(doc))
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
async remove(documentId: DocumentId) {
|
|
135
|
-
this.#storageAdapter.removeRange([documentId, "snapshot"])
|
|
136
|
-
this.#storageAdapter.removeRange([documentId, "incremental"])
|
|
135
|
+
void this.#storageAdapter.removeRange([documentId, "snapshot"])
|
|
136
|
+
void this.#storageAdapter.removeRange([documentId, "incremental"])
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
#shouldSave(documentId: DocumentId, doc: A.Doc<unknown>): boolean {
|
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import debug from "debug"
|
|
2
2
|
import { DocHandle } from "../DocHandle.js"
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "../DocUrl.js"
|
|
8
|
-
import { PeerId, DocumentId } from "../types.js"
|
|
3
|
+
import { stringifyAutomergeUrl } from "../DocUrl.js"
|
|
4
|
+
import { Repo } from "../Repo.js"
|
|
5
|
+
import { RepoMessage } from "../network/messages.js"
|
|
6
|
+
import { DocumentId, PeerId } from "../types.js"
|
|
9
7
|
import { DocSynchronizer } from "./DocSynchronizer.js"
|
|
10
8
|
import { Synchronizer } from "./Synchronizer.js"
|
|
11
9
|
|
|
12
|
-
import debug from "debug"
|
|
13
|
-
import {
|
|
14
|
-
DocumentUnavailableMessage,
|
|
15
|
-
RequestMessage,
|
|
16
|
-
SynchronizerMessage,
|
|
17
|
-
SyncMessage,
|
|
18
|
-
} from "../network/messages.js"
|
|
19
10
|
const log = debug("automerge-repo:collectionsync")
|
|
20
11
|
|
|
21
12
|
/** A CollectionSynchronizer is responsible for synchronizing a DocCollection with peers. */
|
|
@@ -66,7 +57,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
66
57
|
* When we receive a sync message for a document we haven't got in memory, we
|
|
67
58
|
* register it with the repo and start synchronizing
|
|
68
59
|
*/
|
|
69
|
-
async receiveMessage(message:
|
|
60
|
+
async receiveMessage(message: RepoMessage) {
|
|
70
61
|
log(
|
|
71
62
|
`onSyncMessage: ${message.senderId}, ${message.documentId}, ${
|
|
72
63
|
"data" in message ? message.data.byteLength + "bytes" : ""
|
|
@@ -121,7 +112,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
121
112
|
this.#peers.add(peerId)
|
|
122
113
|
for (const docSynchronizer of Object.values(this.#docSynchronizers)) {
|
|
123
114
|
const { documentId } = docSynchronizer
|
|
124
|
-
this.repo.sharePolicy(peerId, documentId).then(okToShare => {
|
|
115
|
+
void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
|
|
125
116
|
if (okToShare) docSynchronizer.beginSync([peerId])
|
|
126
117
|
})
|
|
127
118
|
}
|
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
import * as A from "@automerge/automerge/next"
|
|
2
|
+
import { decode } from "cbor-x"
|
|
3
|
+
import debug from "debug"
|
|
2
4
|
import {
|
|
3
|
-
AWAITING_NETWORK,
|
|
4
5
|
DocHandle,
|
|
5
6
|
DocHandleOutboundEphemeralMessagePayload,
|
|
6
7
|
READY,
|
|
7
8
|
REQUESTING,
|
|
8
9
|
UNAVAILABLE,
|
|
9
10
|
} from "../DocHandle.js"
|
|
10
|
-
import { PeerId } from "../types.js"
|
|
11
|
-
import { Synchronizer } from "./Synchronizer.js"
|
|
12
|
-
|
|
13
|
-
import debug from "debug"
|
|
14
11
|
import {
|
|
12
|
+
DocumentUnavailableMessage,
|
|
15
13
|
EphemeralMessage,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Message,
|
|
14
|
+
MessageContents,
|
|
15
|
+
RepoMessage,
|
|
19
16
|
RequestMessage,
|
|
20
|
-
SynchronizerMessage,
|
|
21
17
|
SyncMessage,
|
|
18
|
+
isRequestMessage,
|
|
22
19
|
} from "../network/messages.js"
|
|
20
|
+
import { PeerId } from "../types.js"
|
|
21
|
+
import { Synchronizer } from "./Synchronizer.js"
|
|
22
|
+
import { throttle } from "../helpers/throttle.js"
|
|
23
23
|
|
|
24
24
|
type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
|
|
25
|
-
import { decode } from "cbor-x"
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
|
|
@@ -30,8 +29,7 @@ import { decode } from "cbor-x"
|
|
|
30
29
|
*/
|
|
31
30
|
export class DocSynchronizer extends Synchronizer {
|
|
32
31
|
#log: debug.Debugger
|
|
33
|
-
|
|
34
|
-
#opsLog: debug.Debugger
|
|
32
|
+
syncDebounceRate = 100
|
|
35
33
|
|
|
36
34
|
/** Active peers */
|
|
37
35
|
#peers: PeerId[] = []
|
|
@@ -45,14 +43,15 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
45
43
|
|
|
46
44
|
#syncStarted = false
|
|
47
45
|
|
|
48
|
-
constructor(private handle: DocHandle<
|
|
46
|
+
constructor(private handle: DocHandle<unknown>) {
|
|
49
47
|
super()
|
|
50
48
|
const docId = handle.documentId.slice(0, 5)
|
|
51
|
-
this.#conciseLog = debug(`automerge-repo:concise:docsync:${docId}`) // Only logs one line per receive/send
|
|
52
49
|
this.#log = debug(`automerge-repo:docsync:${docId}`)
|
|
53
|
-
this.#opsLog = debug(`automerge-repo:ops:docsync:${docId}`) // Log list of ops of each message
|
|
54
50
|
|
|
55
|
-
handle.on(
|
|
51
|
+
handle.on(
|
|
52
|
+
"change",
|
|
53
|
+
throttle(() => this.#syncWithPeers(), this.syncDebounceRate)
|
|
54
|
+
)
|
|
56
55
|
|
|
57
56
|
handle.on("ephemeral-message-outbound", payload =>
|
|
58
57
|
this.#broadcastToPeers(payload)
|
|
@@ -82,7 +81,9 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
82
81
|
this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
async #broadcastToPeers({
|
|
84
|
+
async #broadcastToPeers({
|
|
85
|
+
data,
|
|
86
|
+
}: DocHandleOutboundEphemeralMessagePayload<unknown>) {
|
|
86
87
|
this.#log(`broadcastToPeers`, this.#peers)
|
|
87
88
|
this.#peers.forEach(peerId => this.#sendEphemeralMessage(peerId, data))
|
|
88
89
|
}
|
|
@@ -90,12 +91,13 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
90
91
|
#sendEphemeralMessage(peerId: PeerId, data: Uint8Array) {
|
|
91
92
|
this.#log(`sendEphemeralMessage ->${peerId}`)
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
const message: MessageContents<EphemeralMessage> = {
|
|
94
95
|
type: "ephemeral",
|
|
95
96
|
targetId: peerId,
|
|
96
97
|
documentId: this.handle.documentId,
|
|
97
98
|
data,
|
|
98
|
-
}
|
|
99
|
+
}
|
|
100
|
+
this.emit("message", message)
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
#getSyncState(peerId: PeerId) {
|
|
@@ -128,13 +130,11 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
128
130
|
const [newSyncState, message] = A.generateSyncMessage(doc, syncState)
|
|
129
131
|
this.#setSyncState(peerId, newSyncState)
|
|
130
132
|
if (message) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const decoded = A.decodeSyncMessage(message)
|
|
133
|
+
const isNew = A.getHeads(doc).length === 0
|
|
134
134
|
|
|
135
135
|
if (
|
|
136
136
|
!this.handle.isReady() &&
|
|
137
|
-
|
|
137
|
+
isNew &&
|
|
138
138
|
newSyncState.sharedHeads.length === 0 &&
|
|
139
139
|
!Object.values(this.#peerDocumentStatuses).includes("has") &&
|
|
140
140
|
this.#peerDocumentStatuses[peerId] === "unknown"
|
|
@@ -145,43 +145,23 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
145
145
|
targetId: peerId,
|
|
146
146
|
documentId: this.handle.documentId,
|
|
147
147
|
data: message,
|
|
148
|
-
})
|
|
148
|
+
} as RequestMessage)
|
|
149
149
|
} else {
|
|
150
150
|
this.emit("message", {
|
|
151
151
|
type: "sync",
|
|
152
152
|
targetId: peerId,
|
|
153
153
|
data: message,
|
|
154
154
|
documentId: this.handle.documentId,
|
|
155
|
-
})
|
|
155
|
+
} as SyncMessage)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// if we have sent heads, then the peer now has or will have the document
|
|
159
|
-
if (
|
|
159
|
+
if (!isNew) {
|
|
160
160
|
this.#peerDocumentStatuses[peerId] = "has"
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
#logMessage = (label: string, message: Uint8Array) => {
|
|
166
|
-
// This is real expensive...
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
const size = message.byteLength
|
|
170
|
-
const logText = `${label} ${size}b`
|
|
171
|
-
const decoded = A.decodeSyncMessage(message)
|
|
172
|
-
|
|
173
|
-
this.#conciseLog(logText)
|
|
174
|
-
this.#log(logText, decoded)
|
|
175
|
-
|
|
176
|
-
// expanding is expensive, so only do it if we're logging at this level
|
|
177
|
-
const expanded = this.#opsLog.enabled
|
|
178
|
-
? decoded.changes.flatMap((change: A.Change) =>
|
|
179
|
-
A.decodeChange(change).ops.map((op: any) => JSON.stringify(op))
|
|
180
|
-
)
|
|
181
|
-
: null
|
|
182
|
-
this.#opsLog(logText, expanded)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
165
|
/// PUBLIC
|
|
186
166
|
|
|
187
167
|
hasPeer(peerId: PeerId) {
|
|
@@ -189,6 +169,9 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
189
169
|
}
|
|
190
170
|
|
|
191
171
|
beginSync(peerIds: PeerId[]) {
|
|
172
|
+
const newPeers = new Set(
|
|
173
|
+
peerIds.filter(peerId => !this.#peers.includes(peerId))
|
|
174
|
+
)
|
|
192
175
|
this.#log(`beginSync: ${peerIds.join(", ")}`)
|
|
193
176
|
|
|
194
177
|
// HACK: if we have a sync state already, we round-trip it through the encoding system to make
|
|
@@ -204,15 +187,20 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
204
187
|
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
205
188
|
// with; but we don't want to surface that state to the front end
|
|
206
189
|
void this.handle.doc([READY, REQUESTING, UNAVAILABLE]).then(doc => {
|
|
207
|
-
|
|
208
190
|
// we register out peers first, then say that sync has started
|
|
209
191
|
this.#syncStarted = true
|
|
210
192
|
this.#checkDocUnavailable()
|
|
211
193
|
|
|
212
|
-
|
|
194
|
+
const wasUnavailable = doc === undefined
|
|
195
|
+
if (wasUnavailable && newPeers.size == 0) {
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
// If the doc is unavailable we still need a blank document to generate
|
|
199
|
+
// the sync message from
|
|
200
|
+
const theDoc = doc ?? A.init<unknown>()
|
|
213
201
|
|
|
214
202
|
peerIds.forEach(peerId => {
|
|
215
|
-
this.#sendSyncMessage(peerId,
|
|
203
|
+
this.#sendSyncMessage(peerId, theDoc)
|
|
216
204
|
})
|
|
217
205
|
})
|
|
218
206
|
}
|
|
@@ -222,7 +210,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
222
210
|
this.#peers = this.#peers.filter(p => p !== peerId)
|
|
223
211
|
}
|
|
224
212
|
|
|
225
|
-
receiveMessage(message:
|
|
213
|
+
receiveMessage(message: RepoMessage) {
|
|
226
214
|
switch (message.type) {
|
|
227
215
|
case "sync":
|
|
228
216
|
case "request":
|
|
@@ -246,7 +234,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
246
234
|
|
|
247
235
|
const { senderId, data } = message
|
|
248
236
|
|
|
249
|
-
const contents = decode(data)
|
|
237
|
+
const contents = decode(new Uint8Array(data))
|
|
250
238
|
|
|
251
239
|
this.handle.emit("ephemeral-message", {
|
|
252
240
|
handle: this.handle,
|
|
@@ -320,11 +308,12 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
320
308
|
this.#peers
|
|
321
309
|
.filter(peerId => this.#peerDocumentStatuses[peerId] === "wants")
|
|
322
310
|
.forEach(peerId => {
|
|
323
|
-
|
|
311
|
+
const message: MessageContents<DocumentUnavailableMessage> = {
|
|
324
312
|
type: "doc-unavailable",
|
|
325
313
|
documentId: this.handle.documentId,
|
|
326
314
|
targetId: peerId,
|
|
327
|
-
}
|
|
315
|
+
}
|
|
316
|
+
this.emit("message", message)
|
|
328
317
|
})
|
|
329
318
|
|
|
330
319
|
this.handle.unavailable()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "eventemitter3"
|
|
2
|
-
import {
|
|
2
|
+
import { MessageContents, RepoMessage } from "../network/messages.js"
|
|
3
3
|
|
|
4
4
|
export abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
|
|
5
|
-
abstract receiveMessage(message:
|
|
5
|
+
abstract receiveMessage(message: RepoMessage): void
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface SynchronizerEvents {
|
package/src/types.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/** The ID of a document. Typically you should use a {@link AutomergeUrl} instead.
|
|
2
2
|
*/
|
|
3
3
|
export type DocumentId = string & { __documentId: true } // for logging
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
/** A branded string representing a URL for a document
|
|
5
6
|
*
|
|
6
7
|
* @remarks
|
|
7
|
-
* An automerge URL has the form `automerge:<base58 encoded string>`. This
|
|
8
|
+
* An automerge URL has the form `automerge:<base58 encoded string>`. This
|
|
8
9
|
* type is returned from various routines which validate a url.
|
|
9
10
|
*
|
|
10
11
|
*/
|
|
11
12
|
export type AutomergeUrl = string & { __documentUrl: true } // for opening / linking
|
|
13
|
+
|
|
12
14
|
/** A document ID as a Uint8Array instead of a bas58 encoded string. Typically you should use a {@link AutomergeUrl} instead.
|
|
13
15
|
*/
|
|
14
16
|
export type BinaryDocumentId = Uint8Array & { __binaryDocumentId: true } // for storing / syncing
|
|
15
17
|
|
|
16
18
|
/** A branded type for peer IDs */
|
|
17
|
-
export type PeerId = string & { __peerId:
|
|
19
|
+
export type PeerId = string & { __peerId: true }
|
|
20
|
+
|
|
21
|
+
/** A randomly generated string created when the {@link Repo} starts up */
|
|
22
|
+
export type SessionId = string & { __SessionId: true }
|