@automerge/automerge-repo 1.0.19 → 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/DocHandle.d.ts +6 -5
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +7 -7
- package/dist/RemoteHeadsSubscriptions.d.ts +42 -0
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -0
- package/dist/RemoteHeadsSubscriptions.js +284 -0
- package/dist/Repo.d.ts +29 -2
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +168 -9
- 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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/network/NetworkAdapter.d.ts +15 -1
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.js +3 -1
- package/dist/network/NetworkSubsystem.d.ts +4 -2
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +13 -7
- package/dist/network/messages.d.ts +68 -35
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +9 -7
- package/dist/storage/StorageSubsystem.d.ts +5 -3
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +23 -5
- package/dist/storage/keyHash.d.ts.map +1 -1
- package/dist/storage/types.d.ts +4 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts +2 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +9 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +20 -17
- 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/DocHandle.ts +10 -9
- package/src/RemoteHeadsSubscriptions.ts +375 -0
- package/src/Repo.ts +241 -16
- 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 +3 -1
- package/src/network/NetworkAdapter.ts +19 -2
- package/src/network/NetworkSubsystem.ts +21 -9
- package/src/network/messages.ts +88 -50
- package/src/storage/StorageSubsystem.ts +30 -7
- package/src/storage/keyHash.ts +2 -0
- package/src/storage/types.ts +3 -0
- package/src/synchronizer/CollectionSynchronizer.ts +13 -5
- package/src/synchronizer/DocSynchronizer.ts +27 -27
- package/src/synchronizer/Synchronizer.ts +13 -3
- package/test/DocHandle.test.ts +0 -17
- package/test/RemoteHeadsSubscriptions.test.ts +353 -0
- package/test/Repo.test.ts +108 -17
- package/test/StorageSubsystem.test.ts +29 -7
- package/test/helpers/waitForMessages.ts +22 -0
- package/test/remoteHeads.test.ts +260 -0
- package/.eslintrc +0 -28
package/src/helpers/debounce.ts
CHANGED
package/src/helpers/pause.ts
CHANGED
package/src/helpers/throttle.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* c8 ignore start */
|
|
1
2
|
/**
|
|
2
3
|
* If `promise` is resolved before `t` ms elapse, the timeout is cleared and the result of the
|
|
3
4
|
* promise is returned. If the timeout ends first, a `TimeoutError` is thrown.
|
|
@@ -26,3 +27,4 @@ export class TimeoutError extends Error {
|
|
|
26
27
|
this.name = "TimeoutError"
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
/* c8 ignore end */
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ export {
|
|
|
34
34
|
} from "./AutomergeUrl.js"
|
|
35
35
|
export { Repo } from "./Repo.js"
|
|
36
36
|
export { NetworkAdapter } from "./network/NetworkAdapter.js"
|
|
37
|
-
export {
|
|
37
|
+
export { isRepoMessage } from "./network/messages.js"
|
|
38
38
|
export { StorageAdapter } from "./storage/StorageAdapter.js"
|
|
39
39
|
|
|
40
40
|
/** @hidden **/
|
|
@@ -67,6 +67,7 @@ export type {
|
|
|
67
67
|
OpenPayload,
|
|
68
68
|
PeerCandidatePayload,
|
|
69
69
|
PeerDisconnectedPayload,
|
|
70
|
+
PeerMetadata,
|
|
70
71
|
} from "./network/NetworkAdapter.js"
|
|
71
72
|
|
|
72
73
|
export type {
|
|
@@ -83,6 +84,7 @@ export type {
|
|
|
83
84
|
ChunkInfo,
|
|
84
85
|
ChunkType,
|
|
85
86
|
StorageKey,
|
|
87
|
+
StorageId,
|
|
86
88
|
} from "./storage/types.js"
|
|
87
89
|
|
|
88
90
|
export * from "./types.js"
|
|
@@ -1,6 +1,20 @@
|
|
|
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"
|
|
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
|
+
}
|
|
4
18
|
|
|
5
19
|
/** An interface representing some way to connect to other peers
|
|
6
20
|
*
|
|
@@ -10,13 +24,15 @@ import { Message } from "./messages.js"
|
|
|
10
24
|
* until the adapter emits a `ready` event before it starts trying to use it
|
|
11
25
|
*/
|
|
12
26
|
export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> {
|
|
13
|
-
peerId?: PeerId
|
|
27
|
+
peerId?: PeerId
|
|
28
|
+
peerMetadata?: PeerMetadata
|
|
14
29
|
|
|
15
30
|
/** Called by the {@link Repo} to start the connection process
|
|
16
31
|
*
|
|
17
32
|
* @argument peerId - the peerId of this repo
|
|
33
|
+
* @argument peerMetadata - how this adapter should present itself to other peers
|
|
18
34
|
*/
|
|
19
|
-
abstract connect(peerId: PeerId): void
|
|
35
|
+
abstract connect(peerId: PeerId, peerMetadata?: PeerMetadata): void
|
|
20
36
|
|
|
21
37
|
/** Called by the {@link Repo} to send a message to a peer
|
|
22
38
|
*
|
|
@@ -53,6 +69,7 @@ export interface OpenPayload {
|
|
|
53
69
|
|
|
54
70
|
export interface PeerCandidatePayload {
|
|
55
71
|
peerId: PeerId
|
|
72
|
+
peerMetadata: PeerMetadata
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
export interface PeerDisconnectedPayload {
|
|
@@ -1,13 +1,17 @@
|
|
|
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
16
|
|
|
13
17
|
type EphemeralMessageSource = `${PeerId}:${SessionId}`
|
|
@@ -25,7 +29,11 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
25
29
|
#readyAdapterCount = 0
|
|
26
30
|
#adapters: NetworkAdapter[] = []
|
|
27
31
|
|
|
28
|
-
constructor(
|
|
32
|
+
constructor(
|
|
33
|
+
adapters: NetworkAdapter[],
|
|
34
|
+
public peerId = randomPeerId(),
|
|
35
|
+
private peerMetadata: Promise<PeerMetadata>
|
|
36
|
+
) {
|
|
29
37
|
super()
|
|
30
38
|
this.#log = debug(`automerge-repo:network:${this.peerId}`)
|
|
31
39
|
adapters.forEach(a => this.addNetworkAdapter(a))
|
|
@@ -46,9 +54,8 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
46
54
|
}
|
|
47
55
|
})
|
|
48
56
|
|
|
49
|
-
networkAdapter.on("peer-candidate", ({ peerId }) => {
|
|
57
|
+
networkAdapter.on("peer-candidate", ({ peerId, peerMetadata }) => {
|
|
50
58
|
this.#log(`peer candidate: ${peerId} `)
|
|
51
|
-
|
|
52
59
|
// TODO: This is where authentication would happen
|
|
53
60
|
|
|
54
61
|
if (!this.#adaptersByPeer[peerId]) {
|
|
@@ -56,7 +63,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
56
63
|
this.#adaptersByPeer[peerId] = networkAdapter
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
this.emit("peer", { peerId })
|
|
66
|
+
this.emit("peer", { peerId, peerMetadata })
|
|
60
67
|
})
|
|
61
68
|
|
|
62
69
|
networkAdapter.on("peer-disconnected", ({ peerId }) => {
|
|
@@ -66,7 +73,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
66
73
|
})
|
|
67
74
|
|
|
68
75
|
networkAdapter.on("message", msg => {
|
|
69
|
-
if (!
|
|
76
|
+
if (!isRepoMessage(msg)) {
|
|
70
77
|
this.#log(`invalid message: ${JSON.stringify(msg)}`)
|
|
71
78
|
return
|
|
72
79
|
}
|
|
@@ -98,7 +105,11 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
98
105
|
})
|
|
99
106
|
})
|
|
100
107
|
|
|
101
|
-
|
|
108
|
+
this.peerMetadata.then(peerMetadata => {
|
|
109
|
+
networkAdapter.connect(this.peerId, peerMetadata)
|
|
110
|
+
}).catch(err => {
|
|
111
|
+
this.#log("error connecting to network", err)
|
|
112
|
+
})
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
send(message: MessageContents) {
|
|
@@ -135,7 +146,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
const outbound = prepareMessage(message)
|
|
138
|
-
this.#log("sending message", outbound)
|
|
149
|
+
this.#log("sending message %o", outbound)
|
|
139
150
|
peer.send(outbound as RepoMessage)
|
|
140
151
|
}
|
|
141
152
|
|
|
@@ -171,4 +182,5 @@ export interface NetworkSubsystemEvents {
|
|
|
171
182
|
|
|
172
183
|
export interface PeerPayload {
|
|
173
184
|
peerId: PeerId
|
|
185
|
+
peerMetadata: PeerMetadata
|
|
174
186
|
}
|
package/src/network/messages.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { SyncState } from "@automerge/automerge"
|
|
2
|
+
import { StorageId } from "../storage/types.js"
|
|
2
3
|
import { DocumentId, PeerId, SessionId } from "../types.js"
|
|
3
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
|
+
}
|
|
18
|
+
|
|
4
19
|
/**
|
|
5
20
|
* A sync message for a particular document
|
|
6
21
|
*/
|
|
7
22
|
export type SyncMessage = {
|
|
8
23
|
type: "sync"
|
|
9
|
-
|
|
10
|
-
/** The peer ID of the sender of this message */
|
|
11
24
|
senderId: PeerId
|
|
12
|
-
|
|
13
|
-
/** The peer ID of the recipient of this message */
|
|
14
25
|
targetId: PeerId
|
|
15
26
|
|
|
16
27
|
/** The automerge sync message */
|
|
@@ -20,53 +31,50 @@ export type SyncMessage = {
|
|
|
20
31
|
documentId: DocumentId
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* An ephemeral message.
|
|
24
36
|
*
|
|
25
37
|
* @remarks
|
|
26
|
-
* Ephemeral messages are not persisted anywhere
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* number
|
|
31
|
-
* 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.
|
|
32
44
|
* */
|
|
33
45
|
export type EphemeralMessage = {
|
|
34
46
|
type: "ephemeral"
|
|
35
|
-
|
|
36
|
-
/** The peer ID of the sender of this message */
|
|
37
47
|
senderId: PeerId
|
|
38
|
-
|
|
39
|
-
/** The peer ID of the recipient of this message */
|
|
40
48
|
targetId: PeerId
|
|
41
49
|
|
|
42
|
-
/** 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. */
|
|
43
51
|
count: number
|
|
44
52
|
|
|
45
|
-
/** 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. */
|
|
46
54
|
sessionId: SessionId
|
|
47
55
|
|
|
48
|
-
/** The document ID this message pertains to */
|
|
56
|
+
/** The document ID this message pertains to. */
|
|
49
57
|
documentId: DocumentId
|
|
50
58
|
|
|
51
|
-
/** The actual data of the message */
|
|
59
|
+
/** The actual data of the message. */
|
|
52
60
|
data: Uint8Array
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
/**
|
|
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
|
+
*/
|
|
56
67
|
export type DocumentUnavailableMessage = {
|
|
57
68
|
type: "doc-unavailable"
|
|
58
|
-
|
|
59
|
-
/** The peer ID of the sender of this message */
|
|
60
69
|
senderId: PeerId
|
|
61
|
-
|
|
62
|
-
/** The peer ID of the recipient of this message */
|
|
63
70
|
targetId: PeerId
|
|
64
71
|
|
|
65
72
|
/** The document which the peer claims it doesn't have */
|
|
66
73
|
documentId: DocumentId
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Sent by a {@link Repo} to request a document from a peer.
|
|
70
78
|
*
|
|
71
79
|
* @remarks
|
|
72
80
|
* This is identical to a {@link SyncMessage} except that it is sent by a {@link Repo}
|
|
@@ -74,32 +82,44 @@ export type DocumentUnavailableMessage = {
|
|
|
74
82
|
* */
|
|
75
83
|
export type RequestMessage = {
|
|
76
84
|
type: "request"
|
|
77
|
-
|
|
78
|
-
/** The peer ID of the sender of this message */
|
|
79
85
|
senderId: PeerId
|
|
80
|
-
|
|
81
|
-
/** The peer ID of the recipient of this message */
|
|
82
86
|
targetId: PeerId
|
|
83
87
|
|
|
84
|
-
/** The
|
|
88
|
+
/** The automerge sync message */
|
|
85
89
|
data: Uint8Array
|
|
86
90
|
|
|
87
|
-
/** The document ID this message
|
|
91
|
+
/** The document ID of the document this message is for */
|
|
88
92
|
documentId: DocumentId
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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"
|
|
96
100
|
senderId: PeerId
|
|
101
|
+
targetId: PeerId
|
|
97
102
|
|
|
98
|
-
/** The
|
|
103
|
+
/** The storage IDs to add to the subscription */
|
|
104
|
+
add?: StorageId[]
|
|
105
|
+
|
|
106
|
+
/** The storage IDs to remove from the subscription */
|
|
107
|
+
remove?: StorageId[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sent by a {@link Repo} to indicate that the heads of a document have changed on a remote peer.
|
|
112
|
+
*/
|
|
113
|
+
export type RemoteHeadsChanged = {
|
|
114
|
+
type: "remote-heads-changed"
|
|
115
|
+
senderId: PeerId
|
|
99
116
|
targetId: PeerId
|
|
100
117
|
|
|
101
|
-
/** The
|
|
102
|
-
|
|
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 } }
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
/** These are message types that a {@link NetworkAdapter} surfaces to a {@link Repo}. */
|
|
@@ -108,14 +128,20 @@ export type RepoMessage =
|
|
|
108
128
|
| EphemeralMessage
|
|
109
129
|
| RequestMessage
|
|
110
130
|
| DocumentUnavailableMessage
|
|
131
|
+
| RemoteSubscriptionControlMessage
|
|
132
|
+
| RemoteHeadsChanged
|
|
111
133
|
|
|
112
|
-
/** These are
|
|
113
|
-
export type
|
|
134
|
+
/** These are message types that are handled by the {@link CollectionSynchronizer}.*/
|
|
135
|
+
export type DocMessage =
|
|
136
|
+
| SyncMessage
|
|
137
|
+
| EphemeralMessage
|
|
138
|
+
| RequestMessage
|
|
139
|
+
| DocumentUnavailableMessage
|
|
114
140
|
|
|
115
141
|
/**
|
|
116
142
|
* The contents of a message, without the sender ID or other properties added by the {@link NetworkSubsystem})
|
|
117
143
|
*/
|
|
118
|
-
export type MessageContents<T extends Message =
|
|
144
|
+
export type MessageContents<T extends Message = RepoMessage> =
|
|
119
145
|
T extends EphemeralMessage
|
|
120
146
|
? Omit<T, "senderId" | "count" | "sessionId">
|
|
121
147
|
: Omit<T, "senderId">
|
|
@@ -127,16 +153,21 @@ export interface SyncStateMessage {
|
|
|
127
153
|
syncState: SyncState
|
|
128
154
|
}
|
|
129
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
|
+
|
|
130
162
|
// TYPE GUARDS
|
|
131
163
|
|
|
132
|
-
export const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
isDocumentUnavailableMessage(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)
|
|
140
171
|
|
|
141
172
|
// prettier-ignore
|
|
142
173
|
export const isDocumentUnavailableMessage = (msg: Message): msg is DocumentUnavailableMessage =>
|
|
@@ -150,3 +181,10 @@ export const isSyncMessage = (msg: Message): msg is SyncMessage =>
|
|
|
150
181
|
|
|
151
182
|
export const isEphemeralMessage = (msg: Message): msg is EphemeralMessage =>
|
|
152
183
|
msg.type === "ephemeral"
|
|
184
|
+
|
|
185
|
+
// prettier-ignore
|
|
186
|
+
export const isRemoteSubscriptionControlMessage = (msg: Message): msg is RemoteSubscriptionControlMessage =>
|
|
187
|
+
msg.type === "remote-subscription-change"
|
|
188
|
+
|
|
189
|
+
export const isRemoteHeadsChanged = (msg: Message): msg is RemoteHeadsChanged =>
|
|
190
|
+
msg.type === "remote-heads-changed"
|
|
@@ -2,11 +2,12 @@ import * as A from "@automerge/automerge/next"
|
|
|
2
2
|
import debug from "debug"
|
|
3
3
|
import { headsAreSame } from "../helpers/headsAreSame.js"
|
|
4
4
|
import { mergeArrays } from "../helpers/mergeArrays.js"
|
|
5
|
-
import {
|
|
5
|
+
import { type DocumentId } from "../types.js"
|
|
6
6
|
import { StorageAdapter } from "./StorageAdapter.js"
|
|
7
|
-
import { ChunkInfo, StorageKey } from "./types.js"
|
|
7
|
+
import { ChunkInfo, StorageKey, StorageId } from "./types.js"
|
|
8
8
|
import { keyHash, headsHash } from "./keyHash.js"
|
|
9
9
|
import { chunkTypeFromKey } from "./chunkTypeFromKey.js"
|
|
10
|
+
import * as Uuid from "uuid"
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The storage subsystem is responsible for saving and loading Automerge documents to and from
|
|
@@ -31,6 +32,23 @@ export class StorageSubsystem {
|
|
|
31
32
|
this.#storageAdapter = storageAdapter
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
async id(): Promise<StorageId> {
|
|
36
|
+
const storedId = await this.#storageAdapter.load(["storage-adapter-id"])
|
|
37
|
+
|
|
38
|
+
let id: StorageId
|
|
39
|
+
if (storedId) {
|
|
40
|
+
id = new TextDecoder().decode(storedId) as StorageId
|
|
41
|
+
} else {
|
|
42
|
+
id = Uuid.v4() as StorageId
|
|
43
|
+
await this.#storageAdapter.save(
|
|
44
|
+
["storage-adapter-id"],
|
|
45
|
+
new TextEncoder().encode(id)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return id
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
// ARBITRARY KEY/VALUE STORAGE
|
|
35
53
|
|
|
36
54
|
// The `load`, `save`, and `remove` methods are for generic key/value storage, as opposed to
|
|
@@ -211,19 +229,19 @@ export class StorageSubsystem {
|
|
|
211
229
|
|
|
212
230
|
async loadSyncState(
|
|
213
231
|
documentId: DocumentId,
|
|
214
|
-
|
|
232
|
+
storageId: StorageId
|
|
215
233
|
): Promise<A.SyncState | undefined> {
|
|
216
|
-
const key = [documentId, "sync-state",
|
|
234
|
+
const key = [documentId, "sync-state", storageId]
|
|
217
235
|
const loaded = await this.#storageAdapter.load(key)
|
|
218
236
|
return loaded ? A.decodeSyncState(loaded) : undefined
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
async saveSyncState(
|
|
222
240
|
documentId: DocumentId,
|
|
223
|
-
|
|
241
|
+
storageId: StorageId,
|
|
224
242
|
syncState: A.SyncState
|
|
225
243
|
): Promise<void> {
|
|
226
|
-
const key = [documentId, "sync-state",
|
|
244
|
+
const key = [documentId, "sync-state", storageId]
|
|
227
245
|
await this.#storageAdapter.save(key, A.encodeSyncState(syncState))
|
|
228
246
|
}
|
|
229
247
|
|
|
@@ -261,6 +279,11 @@ export class StorageSubsystem {
|
|
|
261
279
|
incrementalSize += chunk.size
|
|
262
280
|
}
|
|
263
281
|
}
|
|
264
|
-
|
|
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
|
|
265
288
|
}
|
|
266
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
|
}
|
package/src/storage/types.ts
CHANGED
|
@@ -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 {
|
|
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"
|
|
@@ -37,18 +37,25 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
37
37
|
#initDocSynchronizer(handle: DocHandle<unknown>): DocSynchronizer {
|
|
38
38
|
const docSynchronizer = new DocSynchronizer({
|
|
39
39
|
handle,
|
|
40
|
-
onLoadSyncState: peerId => {
|
|
40
|
+
onLoadSyncState: async peerId => {
|
|
41
41
|
if (!this.repo.storageSubsystem) {
|
|
42
|
-
return
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { storageId, isEphemeral } =
|
|
46
|
+
this.repo.peerMetadataByPeerId[peerId] || {}
|
|
47
|
+
if (!storageId || isEphemeral) {
|
|
48
|
+
return
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
return this.repo.storageSubsystem.loadSyncState(
|
|
46
52
|
handle.documentId,
|
|
47
|
-
|
|
53
|
+
storageId
|
|
48
54
|
)
|
|
49
55
|
},
|
|
50
56
|
})
|
|
51
57
|
docSynchronizer.on("message", event => this.emit("message", event))
|
|
58
|
+
docSynchronizer.on("open-doc", event => this.emit("open-doc", event))
|
|
52
59
|
docSynchronizer.on("sync-state", event => this.emit("sync-state", event))
|
|
53
60
|
return docSynchronizer
|
|
54
61
|
}
|
|
@@ -70,7 +77,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
70
77
|
* When we receive a sync message for a document we haven't got in memory, we
|
|
71
78
|
* register it with the repo and start synchronizing
|
|
72
79
|
*/
|
|
73
|
-
async receiveMessage(message:
|
|
80
|
+
async receiveMessage(message: DocMessage) {
|
|
74
81
|
log(
|
|
75
82
|
`onSyncMessage: ${message.senderId}, ${message.documentId}, ${
|
|
76
83
|
"data" in message ? message.data.byteLength + "bytes" : ""
|
|
@@ -110,6 +117,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
// TODO: implement this
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
113
121
|
removeDocument(documentId: DocumentId) {
|
|
114
122
|
throw new Error("not implemented")
|
|
115
123
|
}
|