@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.
Files changed (68) hide show
  1. package/README.md +12 -7
  2. package/dist/AutomergeUrl.js +2 -2
  3. package/dist/DocHandle.d.ts +6 -5
  4. package/dist/DocHandle.d.ts.map +1 -1
  5. package/dist/DocHandle.js +7 -7
  6. package/dist/RemoteHeadsSubscriptions.d.ts +42 -0
  7. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -0
  8. package/dist/RemoteHeadsSubscriptions.js +284 -0
  9. package/dist/Repo.d.ts +29 -2
  10. package/dist/Repo.d.ts.map +1 -1
  11. package/dist/Repo.js +168 -9
  12. package/dist/helpers/debounce.js +1 -1
  13. package/dist/helpers/pause.d.ts.map +1 -1
  14. package/dist/helpers/pause.js +2 -0
  15. package/dist/helpers/throttle.js +1 -1
  16. package/dist/helpers/withTimeout.d.ts.map +1 -1
  17. package/dist/helpers/withTimeout.js +2 -0
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/network/NetworkAdapter.d.ts +15 -1
  22. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  23. package/dist/network/NetworkAdapter.js +3 -1
  24. package/dist/network/NetworkSubsystem.d.ts +4 -2
  25. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  26. package/dist/network/NetworkSubsystem.js +13 -7
  27. package/dist/network/messages.d.ts +68 -35
  28. package/dist/network/messages.d.ts.map +1 -1
  29. package/dist/network/messages.js +9 -7
  30. package/dist/storage/StorageSubsystem.d.ts +5 -3
  31. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  32. package/dist/storage/StorageSubsystem.js +23 -5
  33. package/dist/storage/keyHash.d.ts.map +1 -1
  34. package/dist/storage/types.d.ts +4 -0
  35. package/dist/storage/types.d.ts.map +1 -1
  36. package/dist/synchronizer/CollectionSynchronizer.d.ts +2 -2
  37. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  38. package/dist/synchronizer/CollectionSynchronizer.js +9 -3
  39. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  40. package/dist/synchronizer/DocSynchronizer.js +20 -17
  41. package/dist/synchronizer/Synchronizer.d.ts +12 -3
  42. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  43. package/package.json +6 -6
  44. package/src/AutomergeUrl.ts +2 -2
  45. package/src/DocHandle.ts +10 -9
  46. package/src/RemoteHeadsSubscriptions.ts +375 -0
  47. package/src/Repo.ts +241 -16
  48. package/src/helpers/debounce.ts +1 -1
  49. package/src/helpers/pause.ts +4 -0
  50. package/src/helpers/throttle.ts +1 -1
  51. package/src/helpers/withTimeout.ts +2 -0
  52. package/src/index.ts +3 -1
  53. package/src/network/NetworkAdapter.ts +19 -2
  54. package/src/network/NetworkSubsystem.ts +21 -9
  55. package/src/network/messages.ts +88 -50
  56. package/src/storage/StorageSubsystem.ts +30 -7
  57. package/src/storage/keyHash.ts +2 -0
  58. package/src/storage/types.ts +3 -0
  59. package/src/synchronizer/CollectionSynchronizer.ts +13 -5
  60. package/src/synchronizer/DocSynchronizer.ts +27 -27
  61. package/src/synchronizer/Synchronizer.ts +13 -3
  62. package/test/DocHandle.test.ts +0 -17
  63. package/test/RemoteHeadsSubscriptions.test.ts +353 -0
  64. package/test/Repo.test.ts +108 -17
  65. package/test/StorageSubsystem.test.ts +29 -7
  66. package/test/helpers/waitForMessages.ts +22 -0
  67. package/test/remoteHeads.test.ts +260 -0
  68. package/.eslintrc +0 -28
@@ -19,7 +19,7 @@ export const throttle = <F extends (...args: Parameters<F>) => ReturnType<F>>(
19
19
  return function (...args: Parameters<F>) {
20
20
  clearTimeout(timeout)
21
21
  timeout = setTimeout(() => {
22
- fn.apply(null, args)
22
+ fn(...args)
23
23
  }, rate)
24
24
  }
25
25
  }
@@ -1,3 +1,5 @@
1
+ /* c8 ignore start */
2
+
1
3
  export const pause = (t = 0) =>
2
4
  new Promise<void>(resolve => setTimeout(() => resolve(), t))
3
5
 
@@ -12,3 +14,5 @@ export function rejectOnTimeout<T>(
12
14
  }),
13
15
  ])
14
16
  }
17
+
18
+ /* c8 ignore end */
@@ -36,7 +36,7 @@ export const throttle = <F extends (...args: Parameters<F>) => ReturnType<F>>(
36
36
  wait = lastCall + delay - Date.now()
37
37
  clearTimeout(timeout)
38
38
  timeout = setTimeout(() => {
39
- fn.apply(null, args)
39
+ fn(...args)
40
40
  lastCall = Date.now()
41
41
  }, wait)
42
42
  }
@@ -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 { isValidRepoMessage } from "./network/messages.js"
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 // hmmm, maybe not
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 { NetworkAdapter, PeerDisconnectedPayload } from "./NetworkAdapter.js"
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
- isValidRepoMessage,
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(adapters: NetworkAdapter[], public peerId = randomPeerId()) {
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 (!isValidRepoMessage(msg)) {
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
- networkAdapter.connect(this.peerId)
108
+ this.peerMetadata.then(peerMetadata => {
109
+ networkAdapter.connect(this.peerId, peerMetadata)
110
+ }).catch(err => {
111
+ this.#log("error connecting to network", err)
112
+ })
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
  }
@@ -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
- /** An ephemeral message
34
+ /**
35
+ * An ephemeral message.
24
36
  *
25
37
  * @remarks
26
- * Ephemeral messages are not persisted anywhere and have no particular
27
- * structure. `automerge-repo` will gossip them around, in order to avoid
28
- * eternal loops of ephemeral messages every message has a session ID, which
29
- * is a random number generated by the sender at startup time, and a sequence
30
- * number. The combination of these two things allows us to discard messages
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
- /** 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
+ */
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
- /** 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.
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 initial automerge sync message */
88
+ /** The automerge sync message */
85
89
  data: Uint8Array
86
90
 
87
- /** The document ID this message requests */
91
+ /** The document ID of the document this message is for */
88
92
  documentId: DocumentId
89
93
  }
90
94
 
91
- /** (anticipating work in progress) */
92
- export type AuthMessage<TPayload = any> = {
93
- type: "auth"
94
-
95
- /** The peer ID of the sender of this message */
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 peer ID of the recipient of this message */
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 payload of the auth message (up to the specific auth provider) */
102
- payload: TPayload
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 all the message types that a {@link NetworkAdapter} might see. */
113
- export type Message = RepoMessage | AuthMessage
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 = 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 isValidRepoMessage = (message: Message): message is RepoMessage =>
133
- typeof message === "object" &&
134
- typeof message.type === "string" &&
135
- typeof message.senderId === "string" &&
136
- (isSyncMessage(message) ||
137
- isEphemeralMessage(message) ||
138
- isRequestMessage(message) ||
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 { PeerId, type DocumentId } from "../types.js"
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
- peerId: PeerId
232
+ storageId: StorageId
215
233
  ): Promise<A.SyncState | undefined> {
216
- const key = [documentId, "sync-state", peerId]
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
- peerId: PeerId,
241
+ storageId: StorageId,
224
242
  syncState: A.SyncState
225
243
  ): Promise<void> {
226
- const key = [documentId, "sync-state", peerId]
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
- return incrementalSize >= snapshotSize
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
  }
@@ -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
  }
@@ -37,3 +37,6 @@ export type ChunkType = "snapshot" | "incremental"
37
37
  * should not assume any particular structure.
38
38
  **/
39
39
  export type StorageKey = string[]
40
+
41
+ /** A branded type for storage IDs */
42
+ export type StorageId = string & { __storageId: true }
@@ -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 { RepoMessage } from "../network/messages.js"
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 Promise.resolve(undefined)
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
- peerId
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: RepoMessage) {
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
  }