@automerge/automerge-repo 1.1.0-alpha.7 → 1.1.1

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