@automerge/automerge-repo 1.0.0-alpha.0 → 1.0.0-alpha.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.
Files changed (68) hide show
  1. package/dist/DocCollection.d.ts +2 -1
  2. package/dist/DocCollection.d.ts.map +1 -1
  3. package/dist/DocCollection.js +17 -8
  4. package/dist/DocHandle.d.ts +27 -7
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +47 -23
  7. package/dist/DocUrl.d.ts +3 -3
  8. package/dist/DocUrl.js +9 -9
  9. package/dist/EphemeralData.d.ts +8 -16
  10. package/dist/EphemeralData.d.ts.map +1 -1
  11. package/dist/EphemeralData.js +1 -28
  12. package/dist/Repo.d.ts +0 -2
  13. package/dist/Repo.d.ts.map +1 -1
  14. package/dist/Repo.js +18 -36
  15. package/dist/helpers/headsAreSame.d.ts +2 -2
  16. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  17. package/dist/helpers/headsAreSame.js +1 -4
  18. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  19. package/dist/helpers/tests/network-adapter-tests.js +15 -13
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/network/NetworkAdapter.d.ts +4 -13
  23. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  24. package/dist/network/NetworkSubsystem.d.ts +5 -4
  25. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  26. package/dist/network/NetworkSubsystem.js +39 -25
  27. package/dist/network/messages.d.ts +57 -0
  28. package/dist/network/messages.d.ts.map +1 -0
  29. package/dist/network/messages.js +21 -0
  30. package/dist/storage/StorageSubsystem.d.ts +2 -2
  31. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  32. package/dist/storage/StorageSubsystem.js +36 -6
  33. package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -2
  34. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  35. package/dist/synchronizer/CollectionSynchronizer.js +19 -13
  36. package/dist/synchronizer/DocSynchronizer.d.ts +9 -3
  37. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  38. package/dist/synchronizer/DocSynchronizer.js +145 -29
  39. package/dist/synchronizer/Synchronizer.d.ts +3 -4
  40. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  41. package/dist/types.d.ts +1 -3
  42. package/dist/types.d.ts.map +1 -1
  43. package/fuzz/fuzz.ts +4 -4
  44. package/package.json +3 -3
  45. package/src/DocCollection.ts +19 -9
  46. package/src/DocHandle.ts +82 -37
  47. package/src/DocUrl.ts +9 -9
  48. package/src/EphemeralData.ts +6 -36
  49. package/src/Repo.ts +20 -52
  50. package/src/helpers/headsAreSame.ts +3 -5
  51. package/src/helpers/tests/network-adapter-tests.ts +18 -14
  52. package/src/index.ts +12 -2
  53. package/src/network/NetworkAdapter.ts +4 -20
  54. package/src/network/NetworkSubsystem.ts +61 -38
  55. package/src/network/messages.ts +123 -0
  56. package/src/storage/StorageSubsystem.ts +42 -6
  57. package/src/synchronizer/CollectionSynchronizer.ts +38 -19
  58. package/src/synchronizer/DocSynchronizer.ts +196 -38
  59. package/src/synchronizer/Synchronizer.ts +3 -8
  60. package/src/types.ts +4 -1
  61. package/test/CollectionSynchronizer.test.ts +6 -7
  62. package/test/DocHandle.test.ts +36 -22
  63. package/test/DocSynchronizer.test.ts +85 -9
  64. package/test/Repo.test.ts +279 -59
  65. package/test/StorageSubsystem.test.ts +9 -9
  66. package/test/helpers/DummyNetworkAdapter.ts +1 -1
  67. package/tsconfig.json +2 -1
  68. package/test/EphemeralData.test.ts +0 -44
@@ -1,7 +1,9 @@
1
1
  import * as A from "@automerge/automerge";
2
- import { READY, REQUESTING } from "../DocHandle.js";
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) => {
@@ -89,48 +132,121 @@ 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}`);
135
+ beginSync(peerIds) {
136
+ this.#log(`beginSync: ${peerIds.join(", ")}`);
94
137
  // At this point if we don't have anything in our storage, we need to use an empty doc to sync
95
138
  // with; but we don't want to surface that state to the front end
96
- void this.handle.doc([READY, REQUESTING]).then(doc => {
139
+ void this.handle.doc([READY, REQUESTING, UNAVAILABLE]).then(doc => {
140
+ // if we don't have any peers, then we can say the document is unavailable
97
141
  // HACK: if we have a sync state already, we round-trip it through the encoding system to make
98
142
  // sure state is preserved. This prevents an infinite loop caused by failed attempts to send
99
143
  // messages during disconnection.
100
144
  // TODO: cover that case with a test and remove this hack
101
- const syncStateRaw = this.#getSyncState(peerId);
102
- const syncState = A.decodeSyncState(A.encodeSyncState(syncStateRaw));
103
- this.#setSyncState(peerId, syncState);
104
- this.#sendSyncMessage(peerId, doc);
145
+ peerIds.forEach(peerId => {
146
+ const syncStateRaw = this.#getSyncState(peerId);
147
+ const syncState = A.decodeSyncState(A.encodeSyncState(syncStateRaw));
148
+ this.#setSyncState(peerId, syncState);
149
+ });
150
+ // we register out peers first, then say that sync has started
151
+ this.#syncStarted = true;
152
+ this.#checkDocUnavailable();
153
+ if (doc === undefined)
154
+ return;
155
+ peerIds.forEach(peerId => {
156
+ this.#sendSyncMessage(peerId, doc);
157
+ });
105
158
  });
106
159
  }
107
160
  endSync(peerId) {
108
161
  this.#log(`removing peer ${peerId}`);
109
162
  this.#peers = this.#peers.filter(p => p !== peerId);
110
163
  }
111
- receiveSyncMessage(peerId, channelId, message) {
112
- if (channelId !== this.handle.documentId)
164
+ receiveMessage(message) {
165
+ switch (message.type) {
166
+ case "sync":
167
+ case "request":
168
+ this.receiveSyncMessage(message);
169
+ break;
170
+ case "ephemeral":
171
+ this.receiveEphemeralMessage(message);
172
+ break;
173
+ case "doc-unavailable":
174
+ this.#peerDocumentStatuses[message.senderId] = "unavailable";
175
+ this.#checkDocUnavailable();
176
+ break;
177
+ default:
178
+ throw new Error(`unknown message type: ${message}`);
179
+ }
180
+ }
181
+ receiveEphemeralMessage(message) {
182
+ if (message.documentId !== this.handle.documentId)
183
+ throw new Error(`channelId doesn't match documentId`);
184
+ const { senderId, data } = message;
185
+ const contents = decode(data);
186
+ this.handle.emit("ephemeral-message", {
187
+ handle: this.handle,
188
+ senderId,
189
+ message: contents,
190
+ });
191
+ this.#peers.forEach(peerId => {
192
+ if (peerId === senderId)
193
+ return;
194
+ this.emit("message", {
195
+ ...message,
196
+ targetId: peerId,
197
+ });
198
+ });
199
+ }
200
+ receiveSyncMessage(message) {
201
+ if (message.documentId !== this.handle.documentId)
113
202
  throw new Error(`channelId doesn't match documentId`);
114
203
  // 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 });
204
+ if (!this.handle.inState([READY, REQUESTING, UNAVAILABLE])) {
205
+ this.#pendingSyncMessages.push(message);
117
206
  return;
118
207
  }
119
208
  this.#processAllPendingSyncMessages();
120
- this.#processSyncMessage(peerId, message);
209
+ this.#processSyncMessage(message);
121
210
  }
122
- #processSyncMessage(peerId, message) {
211
+ #processSyncMessage(message) {
212
+ if (isRequestMessage(message)) {
213
+ this.#peerDocumentStatuses[message.senderId] = "wants";
214
+ }
215
+ this.#checkDocUnavailable();
216
+ // if the message has heads, then the peer has the document
217
+ if (A.decodeSyncMessage(message.data).heads.length > 0) {
218
+ this.#peerDocumentStatuses[message.senderId] = "has";
219
+ }
123
220
  this.handle.update(doc => {
124
- const [newDoc, newSyncState] = A.receiveSyncMessage(doc, this.#getSyncState(peerId), message);
125
- this.#setSyncState(peerId, newSyncState);
221
+ const [newDoc, newSyncState] = A.receiveSyncMessage(doc, this.#getSyncState(message.senderId), message.data);
222
+ this.#setSyncState(message.senderId, newSyncState);
126
223
  // respond to just this peer (as required)
127
- this.#sendSyncMessage(peerId, doc);
224
+ this.#sendSyncMessage(message.senderId, doc);
128
225
  return newDoc;
129
226
  });
227
+ this.#checkDocUnavailable();
228
+ }
229
+ #checkDocUnavailable() {
230
+ // if we know none of the peers have the document, tell all our peers that we don't either
231
+ if (this.#syncStarted &&
232
+ this.handle.inState([REQUESTING]) &&
233
+ this.#peers.every(peerId => this.#peerDocumentStatuses[peerId] === "unavailable" ||
234
+ this.#peerDocumentStatuses[peerId] === "wants")) {
235
+ this.#peers
236
+ .filter(peerId => this.#peerDocumentStatuses[peerId] === "wants")
237
+ .forEach(peerId => {
238
+ this.emit("message", {
239
+ type: "doc-unavailable",
240
+ documentId: this.handle.documentId,
241
+ targetId: peerId,
242
+ });
243
+ });
244
+ this.handle.unavailable();
245
+ }
130
246
  }
131
247
  #processAllPendingSyncMessages() {
132
- for (const { peerId, message } of this.#pendingSyncMessages) {
133
- this.#processSyncMessage(peerId, message);
248
+ for (const message of this.#pendingSyncMessages) {
249
+ this.#processSyncMessage(message);
134
250
  }
135
251
  this.#pendingSyncMessages = [];
136
252
  }
@@ -1,10 +1,9 @@
1
1
  import EventEmitter from "eventemitter3";
2
- import { ChannelId, PeerId } from "../types.js";
3
- import { MessagePayload } from "../network/NetworkAdapter.js";
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,YAAY,MAAM,eAAe,CAAA;AACxC,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"}
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
@@ -2,7 +2,7 @@ import assert from "assert"
2
2
  import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"
3
3
  import * as Automerge from "@automerge/automerge"
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.0",
3
+ "version": "1.0.0-alpha.3",
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.9"
34
+ "@automerge/automerge": "^2.1.0-alpha.10"
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": "38c0c32796ddca5f86a2e55ab0f1202a2ce107c8"
68
+ "gitHead": "0ed108273084319aeea64ceccb49c3d58709f107"
69
69
  }
@@ -72,8 +72,8 @@ 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>
75
+ const { documentId } = parseAutomergeUrl(generateAutomergeUrl())
76
+ const handle = this.#getHandle<T>(documentId, true) as DocHandle<T>
77
77
  this.emit("document", { handle })
78
78
  return handle
79
79
  }
@@ -90,12 +90,21 @@ 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>
107
+ const handle = this.#getHandle<T>(documentId, false) as DocHandle<T>
99
108
  this.emit("document", { handle })
100
109
  return handle
101
110
  }
@@ -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,6 +131,7 @@ 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 {
@@ -129,5 +139,5 @@ interface DocumentPayload {
129
139
  }
130
140
 
131
141
  interface DeleteDocumentPayload {
132
- encodedDocumentId: DocumentId
142
+ documentId: DocumentId
133
143
  }