@automerge/automerge-repo 1.0.0-alpha.2 → 1.0.0-alpha.4

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 (80) hide show
  1. package/dist/DocCollection.d.ts +4 -2
  2. package/dist/DocCollection.d.ts.map +1 -1
  3. package/dist/DocCollection.js +20 -11
  4. package/dist/DocHandle.d.ts +34 -6
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +69 -9
  7. package/dist/DocUrl.d.ts +4 -4
  8. package/dist/DocUrl.d.ts.map +1 -1
  9. package/dist/DocUrl.js +9 -9
  10. package/dist/EphemeralData.d.ts +8 -16
  11. package/dist/EphemeralData.d.ts.map +1 -1
  12. package/dist/EphemeralData.js +1 -28
  13. package/dist/Repo.d.ts +0 -2
  14. package/dist/Repo.d.ts.map +1 -1
  15. package/dist/Repo.js +37 -39
  16. package/dist/helpers/cbor.d.ts +4 -0
  17. package/dist/helpers/cbor.d.ts.map +1 -0
  18. package/dist/helpers/cbor.js +8 -0
  19. package/dist/helpers/eventPromise.d.ts +1 -1
  20. package/dist/helpers/eventPromise.d.ts.map +1 -1
  21. package/dist/helpers/headsAreSame.d.ts +0 -1
  22. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  23. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  24. package/dist/helpers/tests/network-adapter-tests.js +15 -13
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/dist/network/NetworkAdapter.d.ts +6 -15
  29. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  30. package/dist/network/NetworkAdapter.js +1 -1
  31. package/dist/network/NetworkSubsystem.d.ts +9 -6
  32. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  33. package/dist/network/NetworkSubsystem.js +69 -32
  34. package/dist/network/messages.d.ts +57 -0
  35. package/dist/network/messages.d.ts.map +1 -0
  36. package/dist/network/messages.js +21 -0
  37. package/dist/storage/StorageSubsystem.d.ts +1 -1
  38. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  39. package/dist/storage/StorageSubsystem.js +2 -2
  40. package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -2
  41. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  42. package/dist/synchronizer/CollectionSynchronizer.js +19 -13
  43. package/dist/synchronizer/DocSynchronizer.d.ts +9 -3
  44. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  45. package/dist/synchronizer/DocSynchronizer.js +149 -34
  46. package/dist/synchronizer/Synchronizer.d.ts +4 -5
  47. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  48. package/dist/synchronizer/Synchronizer.js +1 -1
  49. package/dist/types.d.ts +1 -3
  50. package/dist/types.d.ts.map +1 -1
  51. package/fuzz/fuzz.ts +5 -5
  52. package/package.json +3 -3
  53. package/src/DocCollection.ts +23 -12
  54. package/src/DocHandle.ts +120 -13
  55. package/src/DocUrl.ts +10 -10
  56. package/src/EphemeralData.ts +6 -36
  57. package/src/Repo.ts +37 -55
  58. package/src/helpers/cbor.ts +10 -0
  59. package/src/helpers/eventPromise.ts +1 -1
  60. package/src/helpers/headsAreSame.ts +1 -1
  61. package/src/helpers/tests/network-adapter-tests.ts +18 -14
  62. package/src/index.ts +14 -2
  63. package/src/network/NetworkAdapter.ts +6 -22
  64. package/src/network/NetworkSubsystem.ts +94 -44
  65. package/src/network/messages.ts +123 -0
  66. package/src/storage/StorageSubsystem.ts +2 -2
  67. package/src/synchronizer/CollectionSynchronizer.ts +38 -19
  68. package/src/synchronizer/DocSynchronizer.ts +201 -43
  69. package/src/synchronizer/Synchronizer.ts +4 -9
  70. package/src/types.ts +4 -1
  71. package/test/CollectionSynchronizer.test.ts +6 -7
  72. package/test/DocCollection.test.ts +2 -2
  73. package/test/DocHandle.test.ts +32 -17
  74. package/test/DocSynchronizer.test.ts +85 -9
  75. package/test/Repo.test.ts +267 -63
  76. package/test/StorageSubsystem.test.ts +4 -5
  77. package/test/helpers/DummyNetworkAdapter.ts +12 -3
  78. package/test/helpers/DummyStorageAdapter.ts +1 -1
  79. package/tsconfig.json +4 -3
  80. package/test/EphemeralData.test.ts +0 -44
@@ -1,6 +1,7 @@
1
1
  import { DocCollection } from "../DocCollection.js";
2
- import { ChannelId, PeerId, DocumentId } from "../types.js";
2
+ import { PeerId, DocumentId } from "../types.js";
3
3
  import { Synchronizer } from "./Synchronizer.js";
4
+ import { SynchronizerMessage } from "../network/messages.js";
4
5
  /** A CollectionSynchronizer is responsible for synchronizing a DocCollection with peers. */
5
6
  export declare class CollectionSynchronizer extends Synchronizer {
6
7
  #private;
@@ -10,7 +11,7 @@ export declare class CollectionSynchronizer extends Synchronizer {
10
11
  * When we receive a sync message for a document we haven't got in memory, we
11
12
  * register it with the repo and start synchronizing
12
13
  */
13
- receiveSyncMessage(peerId: PeerId, channelId: ChannelId, message: Uint8Array): Promise<void>;
14
+ receiveMessage(message: SynchronizerMessage): Promise<void>;
14
15
  /**
15
16
  * Starts synchronizing the given document with all peers that we share it generously with.
16
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAOnD,OAAO,EAAE,SAAS,EAAoB,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7E,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAO1C,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,aAAa;IAiCvC;;;OAGG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU;IAmBrB;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,UAAU;IAUlC,cAAc,CAAC,UAAU,EAAE,UAAU;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM;IAWtB,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM;CAQ1B"}
1
+ {"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAOnD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,OAAO,EAGL,mBAAmB,EAEpB,MAAM,wBAAwB,CAAA;AAG/B,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAU1C,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,aAAa;IAiCvC;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,mBAAmB;IAyBjD;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,UAAU;IAYlC,cAAc,CAAC,UAAU,EAAE,UAAU;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM;IAgBtB,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM;CAQ1B"}
@@ -10,6 +10,8 @@ export class CollectionSynchronizer extends Synchronizer {
10
10
  #peers = new Set();
11
11
  /** A map of documentIds to their synchronizers */
12
12
  #docSynchronizers = {};
13
+ /** Used to determine if the document is know to the Collection and a synchronizer exists or is being set up */
14
+ #docSetUp = {};
13
15
  constructor(repo) {
14
16
  super();
15
17
  this.repo = repo;
@@ -44,29 +46,30 @@ export class CollectionSynchronizer extends Synchronizer {
44
46
  * When we receive a sync message for a document we haven't got in memory, we
45
47
  * register it with the repo and start synchronizing
46
48
  */
47
- async receiveSyncMessage(peerId, channelId, message) {
48
- log(`onSyncMessage: ${peerId}, ${channelId}, ${message.byteLength}bytes`);
49
- const documentId = channelId;
49
+ async receiveMessage(message) {
50
+ log(`onSyncMessage: ${message.senderId}, ${message.documentId}, ${"data" in message ? message.data.byteLength + "bytes" : ""}`);
51
+ const documentId = message.documentId;
50
52
  if (!documentId) {
51
53
  throw new Error("received a message with an invalid documentId");
52
54
  }
53
- const docSynchronizer = await this.#fetchDocSynchronizer(documentId);
54
- await docSynchronizer.receiveSyncMessage(peerId, channelId, message);
55
+ this.#docSetUp[documentId] = true;
56
+ const docSynchronizer = this.#fetchDocSynchronizer(documentId);
57
+ docSynchronizer.receiveMessage(message);
55
58
  // Initiate sync with any new peers
56
59
  const peers = await this.#documentGenerousPeers(documentId);
57
- peers
58
- .filter(peerId => !docSynchronizer.hasPeer(peerId))
59
- .forEach(peerId => docSynchronizer.beginSync(peerId));
60
+ docSynchronizer.beginSync(peers.filter(peerId => !docSynchronizer.hasPeer(peerId)));
60
61
  }
61
62
  /**
62
63
  * Starts synchronizing the given document with all peers that we share it generously with.
63
64
  */
64
65
  addDocument(documentId) {
66
+ // HACK: this is a hack to prevent us from adding the same document twice
67
+ if (this.#docSetUp[documentId]) {
68
+ return;
69
+ }
65
70
  const docSynchronizer = this.#fetchDocSynchronizer(documentId);
66
71
  void this.#documentGenerousPeers(documentId).then(peers => {
67
- peers.forEach(peerId => {
68
- docSynchronizer.beginSync(peerId);
69
- });
72
+ docSynchronizer.beginSync(peers);
70
73
  });
71
74
  }
72
75
  // TODO: implement this
@@ -76,12 +79,15 @@ export class CollectionSynchronizer extends Synchronizer {
76
79
  /** Adds a peer and maybe starts synchronizing with them */
77
80
  addPeer(peerId) {
78
81
  log(`adding ${peerId} & synchronizing with them`);
82
+ if (this.#peers.has(peerId)) {
83
+ return;
84
+ }
79
85
  this.#peers.add(peerId);
80
86
  for (const docSynchronizer of Object.values(this.#docSynchronizers)) {
81
87
  const { documentId } = docSynchronizer;
82
- void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
88
+ this.repo.sharePolicy(peerId, documentId).then(okToShare => {
83
89
  if (okToShare)
84
- docSynchronizer.beginSync(peerId);
90
+ docSynchronizer.beginSync([peerId]);
85
91
  });
86
92
  }
87
93
  }
@@ -1,6 +1,8 @@
1
1
  import { DocHandle } from "../DocHandle.js";
2
- import { ChannelId, PeerId } from "../types.js";
2
+ import { PeerId } from "../types.js";
3
3
  import { Synchronizer } from "./Synchronizer.js";
4
+ import { EphemeralMessage, RequestMessage, SynchronizerMessage, SyncMessage } from "../network/messages.js";
5
+ type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants";
4
6
  /**
5
7
  * DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
6
8
  * to bring it inline with all other peers' versions.
@@ -9,10 +11,14 @@ export declare class DocSynchronizer extends Synchronizer {
9
11
  #private;
10
12
  private handle;
11
13
  constructor(handle: DocHandle<any>);
14
+ get peerStates(): Record<PeerId, PeerDocumentStatus>;
12
15
  get documentId(): import("../types.js").DocumentId;
13
16
  hasPeer(peerId: PeerId): boolean;
14
- beginSync(peerId: PeerId): void;
17
+ beginSync(peerIds: PeerId[]): void;
15
18
  endSync(peerId: PeerId): void;
16
- receiveSyncMessage(peerId: PeerId, channelId: ChannelId, message: Uint8Array): void;
19
+ receiveMessage(message: SynchronizerMessage): void;
20
+ receiveEphemeralMessage(message: EphemeralMessage): void;
21
+ receiveSyncMessage(message: SyncMessage | RequestMessage): void;
17
22
  }
23
+ export {};
18
24
  //# sourceMappingURL=DocSynchronizer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DocSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/DocSynchronizer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAqB,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,YAAY;;IAanC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC;IAgB1C,IAAI,UAAU,qCAEb;IAwED,OAAO,CAAC,MAAM,EAAE,MAAM;IAItB,SAAS,CAAC,MAAM,EAAE,MAAM;IAkBxB,OAAO,CAAC,MAAM,EAAE,MAAM;IAKtB,kBAAkB,CAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU;CAsCtB"}
1
+ {"version":3,"file":"DocSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/DocSynchronizer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,SAAS,EAKV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,OAAO,EACL,gBAAgB,EAIhB,cAAc,EACd,mBAAmB,EACnB,WAAW,EACZ,MAAM,wBAAwB,CAAA;AAE/B,KAAK,kBAAkB,GAAG,SAAS,GAAG,KAAK,GAAG,aAAa,GAAG,OAAO,CAAA;AAGrE;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,YAAY;;IAiBnC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC;IAoB1C,IAAI,UAAU,uCAEb;IAED,IAAI,UAAU,qCAEb;IAiHD,OAAO,CAAC,MAAM,EAAE,MAAM;IAItB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE;IA6B3B,OAAO,CAAC,MAAM,EAAE,MAAM;IAKtB,cAAc,CAAC,OAAO,EAAE,mBAAmB;IAkB3C,uBAAuB,CAAC,OAAO,EAAE,gBAAgB;IAuBjD,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc;CA2EzD"}
@@ -1,7 +1,9 @@
1
- import * as A from "@automerge/automerge";
2
- import { READY, REQUESTING } from "../DocHandle.js";
1
+ import * as A from "@automerge/automerge/next";
2
+ import { READY, REQUESTING, UNAVAILABLE, } from "../DocHandle.js";
3
3
  import { Synchronizer } from "./Synchronizer.js";
4
4
  import debug from "debug";
5
+ import { isRequestMessage, } from "../network/messages.js";
6
+ import { decode } from "cbor-x";
5
7
  /**
6
8
  * DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
7
9
  * to bring it inline with all other peers' versions.
@@ -13,9 +15,11 @@ export class DocSynchronizer extends Synchronizer {
13
15
  #opsLog;
14
16
  /** Active peers */
15
17
  #peers = [];
18
+ #peerDocumentStatuses = {};
16
19
  /** Sync state for each peer we've communicated with (including inactive peers) */
17
20
  #syncStates = {};
18
21
  #pendingSyncMessages = [];
22
+ #syncStarted = false;
19
23
  constructor(handle) {
20
24
  super();
21
25
  this.handle = handle;
@@ -24,12 +28,16 @@ export class DocSynchronizer extends Synchronizer {
24
28
  this.#log = debug(`automerge-repo:docsync:${docId}`);
25
29
  this.#opsLog = debug(`automerge-repo:ops:docsync:${docId}`); // Log list of ops of each message
26
30
  handle.on("change", () => this.#syncWithPeers());
31
+ handle.on("ephemeral-message-outbound", payload => this.#broadcastToPeers(payload));
27
32
  // Process pending sync messages immediately after the handle becomes ready.
28
33
  void (async () => {
29
34
  await handle.doc([READY, REQUESTING]);
30
35
  this.#processAllPendingSyncMessages();
31
36
  })();
32
37
  }
38
+ get peerStates() {
39
+ return this.#peerDocumentStatuses;
40
+ }
33
41
  get documentId() {
34
42
  return this.handle.documentId;
35
43
  }
@@ -37,13 +45,32 @@ export class DocSynchronizer extends Synchronizer {
37
45
  async #syncWithPeers() {
38
46
  this.#log(`syncWithPeers`);
39
47
  const doc = await this.handle.doc();
48
+ if (doc === undefined)
49
+ return;
40
50
  this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc));
41
51
  }
52
+ async #broadcastToPeers({ data }) {
53
+ this.#log(`broadcastToPeers`, this.#peers);
54
+ this.#peers.forEach(peerId => this.#sendEphemeralMessage(peerId, data));
55
+ }
56
+ #sendEphemeralMessage(peerId, data) {
57
+ this.#log(`sendEphemeralMessage ->${peerId}`);
58
+ this.emit("message", {
59
+ type: "ephemeral",
60
+ targetId: peerId,
61
+ documentId: this.handle.documentId,
62
+ data,
63
+ });
64
+ }
42
65
  #getSyncState(peerId) {
43
66
  if (!this.#peers.includes(peerId)) {
44
67
  this.#log("adding a new peer", peerId);
45
68
  this.#peers.push(peerId);
46
69
  }
70
+ // when a peer is added, we don't know if it has the document or not
71
+ if (!(peerId in this.#peerDocumentStatuses)) {
72
+ this.#peerDocumentStatuses[peerId] = "unknown";
73
+ }
47
74
  return this.#syncStates[peerId] ?? A.initSyncState();
48
75
  }
49
76
  #setSyncState(peerId, syncState) {
@@ -59,16 +86,32 @@ export class DocSynchronizer extends Synchronizer {
59
86
  this.#setSyncState(peerId, newSyncState);
60
87
  if (message) {
61
88
  this.#logMessage(`sendSyncMessage 🡒 ${peerId}`, message);
62
- const channelId = this.handle.documentId;
63
- this.emit("message", {
64
- targetId: peerId,
65
- channelId,
66
- message,
67
- broadcast: false,
68
- });
69
- }
70
- else {
71
- this.#log(`sendSyncMessage ->${peerId} [no message generated]`);
89
+ const decoded = A.decodeSyncMessage(message);
90
+ if (!this.handle.isReady() &&
91
+ decoded.heads.length === 0 &&
92
+ newSyncState.sharedHeads.length === 0 &&
93
+ !Object.values(this.#peerDocumentStatuses).includes("has") &&
94
+ this.#peerDocumentStatuses[peerId] === "unknown") {
95
+ // we don't have the document (or access to it), so we request it
96
+ this.emit("message", {
97
+ type: "request",
98
+ targetId: peerId,
99
+ documentId: this.handle.documentId,
100
+ data: message,
101
+ });
102
+ }
103
+ else {
104
+ this.emit("message", {
105
+ type: "sync",
106
+ targetId: peerId,
107
+ data: message,
108
+ documentId: this.handle.documentId,
109
+ });
110
+ }
111
+ // if we have sent heads, then the peer now has or will have the document
112
+ if (decoded.heads.length > 0) {
113
+ this.#peerDocumentStatuses[peerId] = "has";
114
+ }
72
115
  }
73
116
  }
74
117
  #logMessage = (label, message) => {
@@ -81,7 +124,7 @@ export class DocSynchronizer extends Synchronizer {
81
124
  this.#log(logText, decoded);
82
125
  // expanding is expensive, so only do it if we're logging at this level
83
126
  const expanded = this.#opsLog.enabled
84
- ? decoded.changes.flatMap(change => A.decodeChange(change).ops.map(op => JSON.stringify(op)))
127
+ ? decoded.changes.flatMap((change) => A.decodeChange(change).ops.map((op) => JSON.stringify(op)))
85
128
  : null;
86
129
  this.#opsLog(logText, expanded);
87
130
  };
@@ -89,48 +132,120 @@ export class DocSynchronizer extends Synchronizer {
89
132
  hasPeer(peerId) {
90
133
  return this.#peers.includes(peerId);
91
134
  }
92
- beginSync(peerId) {
93
- this.#log(`beginSync: ${peerId}`);
94
- // At this point if we don't have anything in our storage, we need to use an empty doc to sync
95
- // with; but we don't want to surface that state to the front end
96
- void this.handle.doc([READY, REQUESTING]).then(doc => {
97
- // HACK: if we have a sync state already, we round-trip it through the encoding system to make
98
- // sure state is preserved. This prevents an infinite loop caused by failed attempts to send
99
- // messages during disconnection.
100
- // TODO: cover that case with a test and remove this hack
135
+ beginSync(peerIds) {
136
+ this.#log(`beginSync: ${peerIds.join(", ")}`);
137
+ // HACK: if we have a sync state already, we round-trip it through the encoding system to make
138
+ // sure state is preserved. This prevents an infinite loop caused by failed attempts to send
139
+ // messages during disconnection.
140
+ // TODO: cover that case with a test and remove this hack
141
+ peerIds.forEach(peerId => {
101
142
  const syncStateRaw = this.#getSyncState(peerId);
102
143
  const syncState = A.decodeSyncState(A.encodeSyncState(syncStateRaw));
103
144
  this.#setSyncState(peerId, syncState);
104
- this.#sendSyncMessage(peerId, doc);
145
+ });
146
+ // At this point if we don't have anything in our storage, we need to use an empty doc to sync
147
+ // with; but we don't want to surface that state to the front end
148
+ void this.handle.doc([READY, REQUESTING, UNAVAILABLE]).then(doc => {
149
+ // we register out peers first, then say that sync has started
150
+ this.#syncStarted = true;
151
+ this.#checkDocUnavailable();
152
+ if (doc === undefined)
153
+ return;
154
+ peerIds.forEach(peerId => {
155
+ this.#sendSyncMessage(peerId, doc);
156
+ });
105
157
  });
106
158
  }
107
159
  endSync(peerId) {
108
160
  this.#log(`removing peer ${peerId}`);
109
161
  this.#peers = this.#peers.filter(p => p !== peerId);
110
162
  }
111
- receiveSyncMessage(peerId, channelId, message) {
112
- if (channelId !== this.handle.documentId)
163
+ receiveMessage(message) {
164
+ switch (message.type) {
165
+ case "sync":
166
+ case "request":
167
+ this.receiveSyncMessage(message);
168
+ break;
169
+ case "ephemeral":
170
+ this.receiveEphemeralMessage(message);
171
+ break;
172
+ case "doc-unavailable":
173
+ this.#peerDocumentStatuses[message.senderId] = "unavailable";
174
+ this.#checkDocUnavailable();
175
+ break;
176
+ default:
177
+ throw new Error(`unknown message type: ${message}`);
178
+ }
179
+ }
180
+ receiveEphemeralMessage(message) {
181
+ if (message.documentId !== this.handle.documentId)
182
+ throw new Error(`channelId doesn't match documentId`);
183
+ const { senderId, data } = message;
184
+ const contents = decode(data);
185
+ this.handle.emit("ephemeral-message", {
186
+ handle: this.handle,
187
+ senderId,
188
+ message: contents,
189
+ });
190
+ this.#peers.forEach(peerId => {
191
+ if (peerId === senderId)
192
+ return;
193
+ this.emit("message", {
194
+ ...message,
195
+ targetId: peerId,
196
+ });
197
+ });
198
+ }
199
+ receiveSyncMessage(message) {
200
+ if (message.documentId !== this.handle.documentId)
113
201
  throw new Error(`channelId doesn't match documentId`);
114
202
  // We need to block receiving the syncMessages until we've checked local storage
115
- if (!this.handle.inState([READY, REQUESTING])) {
116
- this.#pendingSyncMessages.push({ peerId, message });
203
+ if (!this.handle.inState([READY, REQUESTING, UNAVAILABLE])) {
204
+ this.#pendingSyncMessages.push(message);
117
205
  return;
118
206
  }
119
207
  this.#processAllPendingSyncMessages();
120
- this.#processSyncMessage(peerId, message);
208
+ this.#processSyncMessage(message);
121
209
  }
122
- #processSyncMessage(peerId, message) {
210
+ #processSyncMessage(message) {
211
+ if (isRequestMessage(message)) {
212
+ this.#peerDocumentStatuses[message.senderId] = "wants";
213
+ }
214
+ this.#checkDocUnavailable();
215
+ // if the message has heads, then the peer has the document
216
+ if (A.decodeSyncMessage(message.data).heads.length > 0) {
217
+ this.#peerDocumentStatuses[message.senderId] = "has";
218
+ }
123
219
  this.handle.update(doc => {
124
- const [newDoc, newSyncState] = A.receiveSyncMessage(doc, this.#getSyncState(peerId), message);
125
- this.#setSyncState(peerId, newSyncState);
220
+ const [newDoc, newSyncState] = A.receiveSyncMessage(doc, this.#getSyncState(message.senderId), message.data);
221
+ this.#setSyncState(message.senderId, newSyncState);
126
222
  // respond to just this peer (as required)
127
- this.#sendSyncMessage(peerId, doc);
223
+ this.#sendSyncMessage(message.senderId, doc);
128
224
  return newDoc;
129
225
  });
226
+ this.#checkDocUnavailable();
227
+ }
228
+ #checkDocUnavailable() {
229
+ // if we know none of the peers have the document, tell all our peers that we don't either
230
+ if (this.#syncStarted &&
231
+ this.handle.inState([REQUESTING]) &&
232
+ this.#peers.every(peerId => this.#peerDocumentStatuses[peerId] === "unavailable" ||
233
+ this.#peerDocumentStatuses[peerId] === "wants")) {
234
+ this.#peers
235
+ .filter(peerId => this.#peerDocumentStatuses[peerId] === "wants")
236
+ .forEach(peerId => {
237
+ this.emit("message", {
238
+ type: "doc-unavailable",
239
+ documentId: this.handle.documentId,
240
+ targetId: peerId,
241
+ });
242
+ });
243
+ this.handle.unavailable();
244
+ }
130
245
  }
131
246
  #processAllPendingSyncMessages() {
132
- for (const { peerId, message } of this.#pendingSyncMessages) {
133
- this.#processSyncMessage(peerId, message);
247
+ for (const message of this.#pendingSyncMessages) {
248
+ this.#processSyncMessage(message);
134
249
  }
135
250
  this.#pendingSyncMessages = [];
136
251
  }
@@ -1,10 +1,9 @@
1
- import EventEmitter from "eventemitter3";
2
- import { ChannelId, PeerId } from "../types.js";
3
- import { MessagePayload } from "../network/NetworkAdapter.js";
1
+ import { EventEmitter } from "eventemitter3";
2
+ import { Message, MessageContents } from "../network/messages.js";
4
3
  export declare abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
5
- abstract receiveSyncMessage(peerId: PeerId, channelId: ChannelId, message: Uint8Array): void;
4
+ abstract receiveMessage(message: Message): void;
6
5
  }
7
6
  export interface SynchronizerEvents {
8
- message: (arg: MessagePayload) => void;
7
+ message: (arg: MessageContents) => void;
9
8
  }
10
9
  //# sourceMappingURL=Synchronizer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Synchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/Synchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAE7D,8BAAsB,YAAa,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IACzE,QAAQ,CAAC,kBAAkB,CACzB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU,GAClB,IAAI;CACR;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAA;CACvC"}
1
+ {"version":3,"file":"Synchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/Synchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAEjE,8BAAsB,YAAa,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IACzE,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;CAChD;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAA;CACxC"}
@@ -1,3 +1,3 @@
1
- import EventEmitter from "eventemitter3";
1
+ import { EventEmitter } from "eventemitter3";
2
2
  export class Synchronizer extends EventEmitter {
3
3
  }
package/dist/types.d.ts CHANGED
@@ -10,7 +10,5 @@ export type BinaryDocumentId = Uint8Array & {
10
10
  export type PeerId = string & {
11
11
  __peerId: false;
12
12
  };
13
- export type ChannelId = string & {
14
- __channelId: false;
15
- };
13
+ export type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
16
14
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AACxD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAC3D,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAA;AACjD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,KAAK,CAAA;CAAE,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AACxD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAC3D,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAA;AAEjD,MAAM,MAAM,gBAAgB,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,IAAI,CAAC,SAAS,GAAG,GAChE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GACV,KAAK,CAAA"}
package/fuzz/fuzz.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import assert from "assert"
2
2
  import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"
3
- import * as Automerge from "@automerge/automerge"
3
+ import * as Automerge from "@automerge/automerge/next"
4
4
 
5
- import { ChannelId, DocHandle, DocumentId, PeerId, SharePolicy } from "../src"
5
+ import { DocHandle, DocumentId, PeerId, SharePolicy } from "../src"
6
6
  import { eventPromise } from "../src/helpers/eventPromise.js"
7
7
  import { pause } from "../src/helpers/pause.js"
8
8
  import { Repo } from "../src/Repo.js"
@@ -105,9 +105,9 @@ for (let i = 0; i < 100000; i++) {
105
105
  })
106
106
 
107
107
  await pause(0)
108
- const a = await aliceRepo.find(doc.documentId).value()
109
- const b = await bobRepo.find(doc.documentId).value()
110
- const c = await charlieRepo.find(doc.documentId).value()
108
+ const a = await aliceRepo.find(doc.url).doc()
109
+ const b = await bobRepo.find(doc.url).doc()
110
+ const c = await charlieRepo.find(doc.url).doc()
111
111
  assert.deepStrictEqual(a, b, "A and B should be equal")
112
112
  assert.deepStrictEqual(b, c, "B and C should be equal")
113
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "A repository object to manage a collection of automerge documents",
5
5
  "repository": "https://github.com/automerge/automerge-repo",
6
6
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -31,7 +31,7 @@
31
31
  "typescript": "^5.1.6"
32
32
  },
33
33
  "peerDependencies": {
34
- "@automerge/automerge": "^2.1.0-alpha.10"
34
+ "@automerge/automerge": "^2.1.0-alpha.12"
35
35
  },
36
36
  "dependencies": {
37
37
  "bs58check": "^3.0.1",
@@ -65,5 +65,5 @@
65
65
  "publishConfig": {
66
66
  "access": "public"
67
67
  },
68
- "gitHead": "b5830dde8f135b694809698aaad2a9fdc79a9898"
68
+ "gitHead": "fbf71f0c3aaa2786a4e279f336f01d665f53ce5b"
69
69
  }
@@ -1,4 +1,4 @@
1
- import EventEmitter from "eventemitter3"
1
+ import { EventEmitter } from "eventemitter3"
2
2
  import { DocHandle } from "./DocHandle.js"
3
3
  import { DocumentId, type BinaryDocumentId, AutomergeUrl } from "./types.js"
4
4
  import { type SharePolicy } from "./Repo.js"
@@ -72,9 +72,9 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
72
72
  // - pass a "reify" function that takes a `<any>` and returns `<T>`
73
73
 
74
74
  // Generate a new UUID and store it in the buffer
75
- const { encodedDocumentId } = parseAutomergeUrl(generateAutomergeUrl())
76
- const handle = this.#getHandle<T>(encodedDocumentId, true) as DocHandle<T>
77
- this.emit("document", { handle })
75
+ const { documentId } = parseAutomergeUrl(generateAutomergeUrl())
76
+ const handle = this.#getHandle<T>(documentId, true) as DocHandle<T>
77
+ this.emit("document", { handle, isNew: true })
78
78
  return handle
79
79
  }
80
80
 
@@ -90,13 +90,22 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
90
90
  throw new Error(`Invalid AutomergeUrl: '${automergeUrl}'`)
91
91
  }
92
92
 
93
- const { encodedDocumentId } = parseAutomergeUrl(automergeUrl)
93
+ const { documentId } = parseAutomergeUrl(automergeUrl)
94
94
  // If we have the handle cached, return it
95
- if (this.#handleCache[encodedDocumentId])
96
- return this.#handleCache[encodedDocumentId]
95
+ if (this.#handleCache[documentId]) {
96
+ if (this.#handleCache[documentId].isUnavailable()) {
97
+ // this ensures that the event fires after the handle has been returned
98
+ setTimeout(() => {
99
+ this.#handleCache[documentId].emit("unavailable", {
100
+ handle: this.#handleCache[documentId],
101
+ })
102
+ })
103
+ }
104
+ return this.#handleCache[documentId]
105
+ }
97
106
 
98
- const handle = this.#getHandle<T>(encodedDocumentId, false) as DocHandle<T>
99
- this.emit("document", { handle })
107
+ const handle = this.#getHandle<T>(documentId, false) as DocHandle<T>
108
+ this.emit("document", { handle, isNew: false })
100
109
  return handle
101
110
  }
102
111
 
@@ -105,7 +114,7 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
105
114
  id: DocumentId | AutomergeUrl
106
115
  ) {
107
116
  if (isValidAutomergeUrl(id)) {
108
- ;({ encodedDocumentId: id } = parseAutomergeUrl(id))
117
+ ;({ documentId: id } = parseAutomergeUrl(id))
109
118
  }
110
119
 
111
120
  const handle = this.#getHandle(id, false)
@@ -113,7 +122,7 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
113
122
 
114
123
  delete this.#handleCache[id]
115
124
  this.emit("delete-document", {
116
- encodedDocumentId: id,
125
+ documentId: id,
117
126
  })
118
127
  }
119
128
  }
@@ -122,12 +131,14 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
122
131
  interface DocCollectionEvents {
123
132
  document: (arg: DocumentPayload) => void
124
133
  "delete-document": (arg: DeleteDocumentPayload) => void
134
+ "unavailable-document": (arg: DeleteDocumentPayload) => void
125
135
  }
126
136
 
127
137
  interface DocumentPayload {
128
138
  handle: DocHandle<any>
139
+ isNew: boolean
129
140
  }
130
141
 
131
142
  interface DeleteDocumentPayload {
132
- encodedDocumentId: DocumentId
143
+ documentId: DocumentId
133
144
  }