@automerge/automerge-repo 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DocHandle.d.ts +47 -5
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +40 -3
- package/dist/DocUrl.d.ts +4 -5
- package/dist/DocUrl.d.ts.map +1 -1
- package/dist/DocUrl.js +9 -2
- package/dist/EphemeralData.d.ts +1 -0
- package/dist/EphemeralData.d.ts.map +1 -1
- package/dist/Repo.d.ts +25 -5
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +24 -4
- package/dist/index.d.ts +33 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -4
- package/dist/network/NetworkAdapter.d.ts +21 -0
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.js +7 -0
- package/dist/network/messages.d.ts +82 -7
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/storage/StorageAdapter.d.ts +21 -0
- package/dist/storage/StorageAdapter.d.ts.map +1 -1
- package/dist/storage/StorageAdapter.js +6 -0
- package/dist/types.d.ts +12 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/DocHandle.ts +47 -4
- package/src/DocUrl.ts +11 -7
- package/src/EphemeralData.ts +1 -0
- package/src/Repo.ts +50 -12
- package/src/index.ts +44 -6
- package/src/network/NetworkAdapter.ts +21 -0
- package/src/network/messages.ts +105 -8
- package/src/storage/StorageAdapter.ts +21 -0
- package/src/types.ts +12 -4
- package/test/Repo.test.ts +31 -0
- package/test/StorageSubsystem.test.ts +1 -1
- package/typedoc.json +5 -0
|
@@ -2,23 +2,44 @@ import { EventEmitter } from "eventemitter3"
|
|
|
2
2
|
import { PeerId } from "../types.js"
|
|
3
3
|
import { Message } from "./messages.js"
|
|
4
4
|
|
|
5
|
+
/** An interface representing some way to connect to other peers
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The {@link Repo} uses one or more `NetworkAdapter`s to connect to other peers.
|
|
9
|
+
* Because the network may take some time to be ready the {@link Repo} will wait
|
|
10
|
+
* until the adapter emits a `ready` event before it starts trying to use it
|
|
11
|
+
*/
|
|
5
12
|
export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> {
|
|
6
13
|
peerId?: PeerId // hmmm, maybe not
|
|
7
14
|
|
|
15
|
+
/** Called by the {@link Repo} to start the connection process
|
|
16
|
+
*
|
|
17
|
+
* @argument peerId - the peerId of this repo
|
|
18
|
+
*/
|
|
8
19
|
abstract connect(peerId: PeerId): void
|
|
9
20
|
|
|
21
|
+
/** Called by the {@link Repo} to send a message to a peer
|
|
22
|
+
*
|
|
23
|
+
* @argument message - the message to send
|
|
24
|
+
*/
|
|
10
25
|
abstract send(message: Message): void
|
|
11
26
|
|
|
27
|
+
/** Called by the {@link Repo} to disconnect from the network */
|
|
12
28
|
abstract disconnect(): void
|
|
13
29
|
}
|
|
14
30
|
|
|
15
31
|
// events & payloads
|
|
16
32
|
|
|
17
33
|
export interface NetworkAdapterEvents {
|
|
34
|
+
/** Emitted when the network is ready to be used */
|
|
18
35
|
ready: (payload: OpenPayload) => void
|
|
36
|
+
/** Emitted when the network is closed */
|
|
19
37
|
close: () => void
|
|
38
|
+
/** Emitted when the network adapter learns about a new peer */
|
|
20
39
|
"peer-candidate": (payload: PeerCandidatePayload) => void
|
|
40
|
+
/** Emitted when the network adapter learns that a peer has disconnected */
|
|
21
41
|
"peer-disconnected": (payload: PeerDisconnectedPayload) => void
|
|
42
|
+
/** Emitted when the network adapter receives a message from a peer */
|
|
22
43
|
message: (payload: Message) => void
|
|
23
44
|
}
|
|
24
45
|
|
package/src/network/messages.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// utilities
|
|
2
2
|
import { SessionId } from "../EphemeralData.js"
|
|
3
|
+
export { type SessionId } from "../EphemeralData.js"
|
|
3
4
|
import { DocumentId, PeerId } from "../types.js"
|
|
4
5
|
|
|
5
6
|
export function isValidMessage(
|
|
@@ -55,7 +56,28 @@ export interface SyncMessageContents {
|
|
|
55
56
|
documentId: DocumentId
|
|
56
57
|
}
|
|
57
58
|
|
|
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
|
+
/**
|
|
67
|
+
* A sync message for a particular document
|
|
68
|
+
*/
|
|
69
|
+
export type SyncMessage = {
|
|
70
|
+
/** The peer ID of the sender of this message */
|
|
71
|
+
senderId: PeerId
|
|
72
|
+
type: "sync"
|
|
73
|
+
/** The automerge sync message */
|
|
74
|
+
data: Uint8Array
|
|
75
|
+
/** The peer ID of the recipient of this message */
|
|
76
|
+
targetId: PeerId
|
|
77
|
+
/** The document ID of the document this message is for */
|
|
78
|
+
documentId: DocumentId
|
|
79
|
+
}
|
|
80
|
+
|
|
59
81
|
|
|
60
82
|
export interface EphemeralMessageEnvelope {
|
|
61
83
|
senderId: PeerId
|
|
@@ -70,8 +92,35 @@ export interface EphemeralMessageContents {
|
|
|
70
92
|
data: Uint8Array
|
|
71
93
|
}
|
|
72
94
|
|
|
73
|
-
export type EphemeralMessage = EphemeralMessageEnvelope &
|
|
74
|
-
|
|
95
|
+
// export type EphemeralMessage = EphemeralMessageEnvelope & EphemeralMessageContents
|
|
96
|
+
// Inline definitions to get good docs, but check the types line up
|
|
97
|
+
type _check_ephemeral_message = static_assert<EphemeralMessage extends EphemeralMessageEnvelope & EphemeralMessageContents ? true : false>
|
|
98
|
+
|
|
99
|
+
/** An ephemeral message
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* Ephemeral messages are not persisted anywhere and have no particular
|
|
103
|
+
* structure. `automerge-repo` will gossip them around, in order to avoid
|
|
104
|
+
* eternal loops of ephemeral messages every message has a session ID, which
|
|
105
|
+
* is a random number generated by the sender at startup time, and a sequence
|
|
106
|
+
* number. The combination of these two things allows us to discard messages
|
|
107
|
+
* we have already seen.
|
|
108
|
+
* */
|
|
109
|
+
export type EphemeralMessage = {
|
|
110
|
+
/** The ID of the peer who sent this message */
|
|
111
|
+
senderId: PeerId
|
|
112
|
+
/** A sequence number which must be incremented for each message sent by this peer */
|
|
113
|
+
count: number
|
|
114
|
+
/** The ID of the session this message is part of. The sequence number for a given session always increases */
|
|
115
|
+
sessionId: SessionId
|
|
116
|
+
type: "ephemeral"
|
|
117
|
+
/** The peer this message is for */
|
|
118
|
+
targetId: PeerId
|
|
119
|
+
/** The document ID this message pertains to */
|
|
120
|
+
documentId: DocumentId
|
|
121
|
+
/** The actual data of the message */
|
|
122
|
+
data: Uint8Array
|
|
123
|
+
}
|
|
75
124
|
|
|
76
125
|
export interface DocumentUnavailableMessageContents {
|
|
77
126
|
type: "doc-unavailable"
|
|
@@ -79,8 +128,20 @@ export interface DocumentUnavailableMessageContents {
|
|
|
79
128
|
targetId: PeerId
|
|
80
129
|
}
|
|
81
130
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
/** Sent by a {@link Repo} to indicate that it does not have the document and none of it's connected peers do either */
|
|
136
|
+
export type DocumentUnavailableMessage = {
|
|
137
|
+
/** The peer who sent this message */
|
|
138
|
+
senderId: PeerId
|
|
139
|
+
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
|
+
}
|
|
84
145
|
|
|
85
146
|
export interface RequestMessageContents {
|
|
86
147
|
type: "request"
|
|
@@ -89,7 +150,29 @@ export interface RequestMessageContents {
|
|
|
89
150
|
documentId: DocumentId
|
|
90
151
|
}
|
|
91
152
|
|
|
92
|
-
export type RequestMessage = SyncMessageEnvelope & RequestMessageContents
|
|
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
|
+
/** Sent by a {@link Repo} to request a document from a peer
|
|
160
|
+
*
|
|
161
|
+
* @remarks
|
|
162
|
+
* This is identical to a {@link SyncMessage} except that it is sent by a {@link Repo}
|
|
163
|
+
* as the initial sync message when asking the other peer if it has the document.
|
|
164
|
+
* */
|
|
165
|
+
export type RequestMessage = {
|
|
166
|
+
/** The peer who sent this message */
|
|
167
|
+
senderId: PeerId
|
|
168
|
+
type: "request"
|
|
169
|
+
/** The initial automerge sync message */
|
|
170
|
+
data: Uint8Array
|
|
171
|
+
/** The peer this message is for */
|
|
172
|
+
targetId: PeerId
|
|
173
|
+
/** The document ID this message requests */
|
|
174
|
+
documentId: DocumentId
|
|
175
|
+
}
|
|
93
176
|
|
|
94
177
|
export type MessageContents =
|
|
95
178
|
| SyncMessageContents
|
|
@@ -97,6 +180,7 @@ export type MessageContents =
|
|
|
97
180
|
| RequestMessageContents
|
|
98
181
|
| DocumentUnavailableMessageContents
|
|
99
182
|
|
|
183
|
+
/** The type of messages that {@link Repo} sends and receive to {@link NetworkAdapter}s */
|
|
100
184
|
export type Message =
|
|
101
185
|
| SyncMessage
|
|
102
186
|
| EphemeralMessage
|
|
@@ -109,15 +193,28 @@ export type SynchronizerMessage =
|
|
|
109
193
|
| DocumentUnavailableMessage
|
|
110
194
|
| EphemeralMessage
|
|
111
195
|
|
|
112
|
-
|
|
196
|
+
|
|
197
|
+
/** Notify the network that we have arrived so everyone knows our peer ID */
|
|
198
|
+
export type ArriveMessage = {
|
|
199
|
+
/** Our peer ID */
|
|
113
200
|
senderId: PeerId
|
|
114
201
|
type: "arrive"
|
|
115
202
|
}
|
|
116
203
|
|
|
117
|
-
|
|
204
|
+
/** Respond to an arriving peer with our peer ID */
|
|
205
|
+
export type WelcomeMessage = {
|
|
206
|
+
/** Our peer ID */
|
|
118
207
|
senderId: PeerId
|
|
208
|
+
/** The ID of the peer who sent the {@link ArriveMessage} we are responding to */
|
|
119
209
|
targetId: PeerId
|
|
120
210
|
type: "welcome"
|
|
121
211
|
}
|
|
122
212
|
|
|
213
|
+
/** The type of messages that {@link NetworkAdapter}s send and receive to each other
|
|
214
|
+
*
|
|
215
|
+
* @remarks
|
|
216
|
+
* It is not _required_ that a {@link NetworkAdapter} use this message type.
|
|
217
|
+
* NetworkAdapters are free to use whatever message type makes sense for their
|
|
218
|
+
* transport. However, this type is a useful default.
|
|
219
|
+
* */
|
|
123
220
|
export type NetworkAdapterMessage = ArriveMessage | WelcomeMessage | Message
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
/** A storage adapter represents some way of storing binary data for a {@link Repo}
|
|
2
|
+
*
|
|
3
|
+
* @remarks
|
|
4
|
+
* `StorageAdapter`s are a little like a key/value store. The keys are arrays
|
|
5
|
+
* of strings ({@link StorageKey}) and the values are binary blobs.
|
|
6
|
+
*/
|
|
1
7
|
export abstract class StorageAdapter {
|
|
2
8
|
// load, store, or remove a single binary blob based on an array key
|
|
3
9
|
// automerge-repo mostly uses keys in the following form:
|
|
4
10
|
// [documentId, "snapshot"] or [documentId, "incremental", "0"]
|
|
5
11
|
// but the storage adapter is agnostic to the meaning of the key
|
|
6
12
|
// and we expect to store other data in the future such as syncstates
|
|
13
|
+
/** Load the single blob correspongind to `key` */
|
|
7
14
|
abstract load(key: StorageKey): Promise<Uint8Array | undefined>
|
|
15
|
+
/** save the blod `data` to the key `key` */
|
|
8
16
|
abstract save(key: StorageKey, data: Uint8Array): Promise<void>
|
|
17
|
+
/** remove the blob corresponding to `key` */
|
|
9
18
|
abstract remove(key: StorageKey): Promise<void>
|
|
10
19
|
|
|
11
20
|
// the keyprefix will match any key that starts with the given array
|
|
@@ -13,8 +22,20 @@ export abstract class StorageAdapter {
|
|
|
13
22
|
// or [documentId] will match all data for a given document
|
|
14
23
|
// be careful! this will also match [documentId, "syncState"]!
|
|
15
24
|
// (we aren't using this yet but keep it in mind.)
|
|
25
|
+
/** Load all blobs with keys that start with `keyPrefix` */
|
|
16
26
|
abstract loadRange(keyPrefix: StorageKey): Promise<{key: StorageKey, data: Uint8Array}[]>
|
|
27
|
+
/** Remove all blobs with keys that start with `keyPrefix` */
|
|
17
28
|
abstract removeRange(keyPrefix: StorageKey): Promise<void>
|
|
18
29
|
}
|
|
19
30
|
|
|
31
|
+
/** The type of keys for a {@link StorageAdapter}
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* Storage keys are arrays because they are hierarchical and the storage
|
|
35
|
+
* subsystem will need to be able to do range queries for all keys that
|
|
36
|
+
* have a particular prefix. For example, incremental changes for a given
|
|
37
|
+
* document might be stored under `[<documentId>, "incremental", <SHA256>]`.
|
|
38
|
+
* `StorageAdapter` implementations should not assume any particular structure
|
|
39
|
+
* though.
|
|
40
|
+
**/
|
|
20
41
|
export type StorageKey = string[]
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
/** The ID of a document. Typically you should use a {@link AutomergeUrl} instead.
|
|
2
|
+
*/
|
|
1
3
|
export type DocumentId = string & { __documentId: true } // for logging
|
|
4
|
+
/** A branded string representing a URL for a document
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* An automerge URL has the form `automerge:<base58 encoded string>`. This
|
|
8
|
+
* type is returned from various routines which validate a url.
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
2
11
|
export type AutomergeUrl = string & { __documentUrl: true } // for opening / linking
|
|
12
|
+
/** A document ID as a Uint8Array instead of a bas58 encoded string. Typically you should use a {@link AutomergeUrl} instead.
|
|
13
|
+
*/
|
|
3
14
|
export type BinaryDocumentId = Uint8Array & { __binaryDocumentId: true } // for storing / syncing
|
|
4
15
|
|
|
16
|
+
/** A branded type for peer IDs */
|
|
5
17
|
export type PeerId = string & { __peerId: false }
|
|
6
|
-
|
|
7
|
-
export type DistributiveOmit<T, K extends keyof any> = T extends any
|
|
8
|
-
? Omit<T, K>
|
|
9
|
-
: never
|
package/test/Repo.test.ts
CHANGED
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
generateLargeObject,
|
|
23
23
|
LargeObject,
|
|
24
24
|
} from "./helpers/generate-large-object.js"
|
|
25
|
+
import { parseAutomergeUrl } from "../dist/DocUrl.js"
|
|
26
|
+
|
|
27
|
+
import * as Uuid from "uuid"
|
|
25
28
|
|
|
26
29
|
describe("Repo", () => {
|
|
27
30
|
describe("single repo", () => {
|
|
@@ -50,6 +53,34 @@ describe("Repo", () => {
|
|
|
50
53
|
assert.equal(handle.isReady(), true)
|
|
51
54
|
})
|
|
52
55
|
|
|
56
|
+
it("can find a document once it's created", () => {
|
|
57
|
+
const { repo } = setup()
|
|
58
|
+
const handle = repo.create<TestDoc>()
|
|
59
|
+
handle.change((d: TestDoc) => {
|
|
60
|
+
d.foo = "bar"
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const handle2 = repo.find(handle.url)
|
|
64
|
+
assert.equal(handle, handle2)
|
|
65
|
+
assert.deepEqual(handle2.docSync(), { foo: "bar" })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("can find a document using a legacy UUID (for now)", () => {
|
|
69
|
+
const { repo } = setup()
|
|
70
|
+
const handle = repo.create<TestDoc>()
|
|
71
|
+
handle.change((d: TestDoc) => {
|
|
72
|
+
d.foo = "bar"
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const url = handle.url
|
|
76
|
+
const { binaryDocumentId } = parseAutomergeUrl(url)
|
|
77
|
+
const legacyDocumentId = Uuid.stringify(binaryDocumentId) as AutomergeUrl // a white lie
|
|
78
|
+
|
|
79
|
+
const handle2 = repo.find(legacyDocumentId)
|
|
80
|
+
assert.equal(handle, handle2)
|
|
81
|
+
assert.deepEqual(handle2.docSync(), { foo: "bar" })
|
|
82
|
+
})
|
|
83
|
+
|
|
53
84
|
it("can change a document", async () => {
|
|
54
85
|
const { repo } = setup()
|
|
55
86
|
const handle = repo.create<TestDoc>()
|
|
@@ -9,7 +9,7 @@ import * as A from "@automerge/automerge/next"
|
|
|
9
9
|
import { DummyStorageAdapter } from "./helpers/DummyStorageAdapter.js"
|
|
10
10
|
import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs"
|
|
11
11
|
|
|
12
|
-
import { StorageSubsystem } from "../src/
|
|
12
|
+
import { StorageSubsystem } from "../src/storage/StorageSubsystem.js"
|
|
13
13
|
import { generateAutomergeUrl, parseAutomergeUrl } from "../src/DocUrl.js"
|
|
14
14
|
|
|
15
15
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "automerge-repo-tests"))
|