@dxos/echo-pipeline 0.6.1-main.ff751ec → 0.6.1-next.f8e935e

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 (36) hide show
  1. package/dist/lib/browser/{chunk-O3MAK5PY.mjs → chunk-DMUP426Q.mjs} +17 -5
  2. package/dist/lib/browser/chunk-DMUP426Q.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +284 -184
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-QATBVT6S.cjs → chunk-NH5WJKOW.cjs} +20 -8
  8. package/dist/lib/node/chunk-NH5WJKOW.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +327 -231
  10. package/dist/lib/node/index.cjs.map +4 -4
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +11 -11
  13. package/dist/types/src/automerge/automerge-host.d.ts +6 -2
  14. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  15. package/dist/types/src/automerge/echo-network-adapter.d.ts +2 -2
  16. package/dist/types/src/automerge/echo-network-adapter.test.d.ts +2 -0
  17. package/dist/types/src/automerge/echo-network-adapter.test.d.ts.map +1 -0
  18. package/dist/types/src/automerge/echo-replicator.d.ts +5 -6
  19. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  20. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +35 -0
  21. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -0
  22. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +2 -2
  23. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  24. package/dist/types/src/db-host/data-service.d.ts +11 -3
  25. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  26. package/package.json +33 -33
  27. package/src/automerge/automerge-host.ts +57 -11
  28. package/src/automerge/automerge-repo.test.ts +117 -1
  29. package/src/automerge/echo-network-adapter.test.ts +131 -0
  30. package/src/automerge/echo-network-adapter.ts +4 -4
  31. package/src/automerge/echo-replicator.ts +6 -9
  32. package/src/automerge/mesh-echo-replicator-connection.ts +130 -0
  33. package/src/automerge/mesh-echo-replicator.ts +15 -123
  34. package/src/db-host/data-service.ts +38 -5
  35. package/dist/lib/browser/chunk-O3MAK5PY.mjs.map +0 -7
  36. package/dist/lib/node/chunk-QATBVT6S.cjs.map +0 -7
@@ -0,0 +1,130 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { cbor, type Message } from '@dxos/automerge/automerge-repo';
6
+ import { Resource } from '@dxos/context';
7
+ import { invariant } from '@dxos/invariant';
8
+ import { type PublicKey } from '@dxos/keys';
9
+ import { log } from '@dxos/log';
10
+ import { AutomergeReplicator, type AutomergeReplicatorFactory } from '@dxos/teleport-extension-automerge-replicator';
11
+
12
+ import type { ReplicatorConnection, ShouldAdvertiseParams } from './echo-replicator';
13
+
14
+ const DEFAULT_FACTORY: AutomergeReplicatorFactory = (params) => new AutomergeReplicator(...params);
15
+
16
+ export type MeshReplicatorConnectionParams = {
17
+ ownPeerId: string;
18
+ onRemoteConnected: () => void;
19
+ onRemoteDisconnected: () => void;
20
+ shouldAdvertise: (params: ShouldAdvertiseParams) => Promise<boolean>;
21
+ replicatorFactory?: AutomergeReplicatorFactory;
22
+ };
23
+
24
+ export class MeshReplicatorConnection extends Resource implements ReplicatorConnection {
25
+ public readable: ReadableStream<Message>;
26
+ public writable: WritableStream<Message>;
27
+ public remoteDeviceKey: PublicKey | null = null;
28
+
29
+ public readonly replicatorExtension: AutomergeReplicator;
30
+
31
+ private _remotePeerId: string | null = null;
32
+ private _isEnabled = false;
33
+
34
+ constructor(private readonly _params: MeshReplicatorConnectionParams) {
35
+ super();
36
+
37
+ let readableStreamController!: ReadableStreamDefaultController<Message>;
38
+ this.readable = new ReadableStream<Message>({
39
+ start: (controller) => {
40
+ readableStreamController = controller;
41
+ this._ctx.onDispose(() => controller.close());
42
+ },
43
+ });
44
+
45
+ this.writable = new WritableStream<Message>({
46
+ write: async (message: Message, controller) => {
47
+ invariant(this._isEnabled, 'Writing to a disabled connection');
48
+ try {
49
+ await this.replicatorExtension.sendSyncMessage({ payload: cbor.encode(message) });
50
+ } catch (err) {
51
+ controller.error(err);
52
+ this._disconnectIfEnabled();
53
+ }
54
+ },
55
+ });
56
+
57
+ const createAutomergeReplicator = this._params.replicatorFactory ?? DEFAULT_FACTORY;
58
+ this.replicatorExtension = createAutomergeReplicator([
59
+ {
60
+ peerId: this._params.ownPeerId,
61
+ },
62
+ {
63
+ onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
64
+ // Note: We store only one extension per peer.
65
+ // There can be a case where two connected peers have more than one teleport connection between them
66
+ // and each of them uses different teleport connections to send messages.
67
+ // It works because we receive messages from all teleport connections and Automerge Repo dedup them.
68
+ // TODO(mykola): Use only one teleport connection per peer.
69
+
70
+ // TODO(dmaretskyi): Critical bug.
71
+ // - two peers get connected via swarm 1
72
+ // - they get connected via swarm 2
73
+ // - swarm 1 gets disconnected
74
+ // - automerge repo thinks that peer 2 got disconnected even though swarm 2 is still active
75
+
76
+ this.remoteDeviceKey = remotePeerId;
77
+
78
+ // Set automerge id.
79
+ this._remotePeerId = info.id;
80
+
81
+ log('onStartReplication', { id: info.id, thisPeerId: this.peerId, remotePeerId: remotePeerId.toHex() });
82
+
83
+ this._params.onRemoteConnected();
84
+ },
85
+ onSyncMessage: async ({ payload }) => {
86
+ if (!this._isEnabled) {
87
+ return;
88
+ }
89
+ const message = cbor.decode(payload) as Message;
90
+ // Note: automerge Repo dedup messages.
91
+ readableStreamController.enqueue(message);
92
+ },
93
+ onClose: async () => {
94
+ this._disconnectIfEnabled();
95
+ },
96
+ },
97
+ ]);
98
+ }
99
+
100
+ private _disconnectIfEnabled() {
101
+ if (this._isEnabled) {
102
+ this._params.onRemoteDisconnected();
103
+ }
104
+ }
105
+
106
+ get peerId(): string {
107
+ invariant(this._remotePeerId != null, 'Remote peer has not connected yet.');
108
+ return this._remotePeerId;
109
+ }
110
+
111
+ async shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean> {
112
+ return this._params.shouldAdvertise(params);
113
+ }
114
+
115
+ /**
116
+ * Start exchanging messages with the remote peer.
117
+ * Call after the remote peer has connected.
118
+ */
119
+ enable() {
120
+ invariant(this._remotePeerId != null, 'Remote peer has not connected yet.');
121
+ this._isEnabled = true;
122
+ }
123
+
124
+ /**
125
+ * Stop exchanging messages with the remote peer.
126
+ */
127
+ disable() {
128
+ this._isEnabled = false;
129
+ }
130
+ }
@@ -2,20 +2,17 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Message, cbor } from '@dxos/automerge/automerge-repo';
6
- import { Resource } from '@dxos/context';
7
5
  import { invariant } from '@dxos/invariant';
8
6
  import { PublicKey } from '@dxos/keys';
9
7
  import { log } from '@dxos/log';
10
- import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
8
+ import {
9
+ type AutomergeReplicator,
10
+ type AutomergeReplicatorFactory,
11
+ } from '@dxos/teleport-extension-automerge-replicator';
11
12
  import { ComplexMap, ComplexSet, defaultMap } from '@dxos/util';
12
13
 
13
- import {
14
- type EchoReplicator,
15
- type EchoReplicatorContext,
16
- type ReplicatorConnection,
17
- type ShouldAdvertizeParams,
18
- } from './echo-replicator';
14
+ import { type EchoReplicator, type EchoReplicatorContext, type ShouldAdvertiseParams } from './echo-replicator';
15
+ import { MeshReplicatorConnection } from './mesh-echo-replicator-connection';
19
16
 
20
17
  // TODO(dmaretskyi): Move out of @dxos/echo-pipeline.
21
18
 
@@ -50,11 +47,12 @@ export class MeshEchoReplicator implements EchoReplicator {
50
47
  this._context = null;
51
48
  }
52
49
 
53
- createExtension(): AutomergeReplicator {
50
+ createExtension(extensionFactory?: AutomergeReplicatorFactory): AutomergeReplicator {
54
51
  invariant(this._context);
55
52
 
56
53
  const connection: MeshReplicatorConnection = new MeshReplicatorConnection({
57
54
  ownPeerId: this._context.peerId,
55
+ replicatorFactory: extensionFactory,
58
56
  onRemoteConnected: async () => {
59
57
  log('onRemoteConnected', { peerId: connection.peerId });
60
58
  invariant(this._context);
@@ -64,18 +62,18 @@ export class MeshEchoReplicator implements EchoReplicator {
64
62
  } else {
65
63
  this._connectionsPerPeer.set(connection.peerId, connection);
66
64
  this._context.onConnectionOpen(connection);
67
- await connection.enable();
65
+ connection.enable();
68
66
  }
69
67
  },
70
68
  onRemoteDisconnected: async () => {
71
69
  log('onRemoteDisconnected', { peerId: connection.peerId });
72
70
  this._context?.onConnectionClosed(connection);
73
71
  this._connectionsPerPeer.delete(connection.peerId);
74
- await connection.disable();
72
+ connection.disable();
75
73
  this._connections.delete(connection);
76
74
  },
77
- shouldAdvertize: async (params: ShouldAdvertizeParams) => {
78
- log('shouldAdvertize', { peerId: connection.peerId, documentId: params.documentId });
75
+ shouldAdvertise: async (params: ShouldAdvertiseParams) => {
76
+ log('shouldAdvertise', { peerId: connection.peerId, documentId: params.documentId });
79
77
  invariant(this._context);
80
78
  try {
81
79
  const spaceKey = await this._context.getContainingSpaceForDocument(params.documentId);
@@ -123,116 +121,10 @@ export class MeshEchoReplicator implements EchoReplicator {
123
121
  defaultMap(this._authorizedDevices, spaceKey, () => new ComplexSet(PublicKey.hash)).add(deviceKey);
124
122
  for (const connection of this._connections) {
125
123
  if (connection.remoteDeviceKey && connection.remoteDeviceKey.equals(deviceKey)) {
126
- this._context?.onConnectionAuthScopeChanged(connection);
124
+ if (this._connectionsPerPeer.has(connection.peerId)) {
125
+ this._context?.onConnectionAuthScopeChanged(connection);
126
+ }
127
127
  }
128
128
  }
129
129
  }
130
130
  }
131
-
132
- type MeshReplicatorConnectionParams = {
133
- ownPeerId: string;
134
- onRemoteConnected: () => Promise<void>;
135
- onRemoteDisconnected: () => Promise<void>;
136
- shouldAdvertize: (params: ShouldAdvertizeParams) => Promise<boolean>;
137
- };
138
-
139
- class MeshReplicatorConnection extends Resource implements ReplicatorConnection {
140
- public readable: ReadableStream<Message>;
141
- public writable: WritableStream<Message>;
142
- public remoteDeviceKey: PublicKey | null = null;
143
-
144
- public readonly replicatorExtension: AutomergeReplicator;
145
-
146
- private _remotePeerId: string | null = null;
147
- private _isEnabled = false;
148
-
149
- constructor(private readonly _params: MeshReplicatorConnectionParams) {
150
- super();
151
-
152
- let readableStreamController!: ReadableStreamDefaultController<Message>;
153
- this.readable = new ReadableStream<Message>({
154
- start: (controller) => {
155
- readableStreamController = controller;
156
- this._ctx.onDispose(() => controller.close());
157
- },
158
- });
159
-
160
- this.writable = new WritableStream<Message>({
161
- write: async (message: Message, controller) => {
162
- // TODO(dmaretskyi): Show we block on RPC completing here?
163
- this.replicatorExtension.sendSyncMessage({ payload: cbor.encode(message) }).catch((err) => {
164
- controller.error(err);
165
- });
166
- },
167
- });
168
-
169
- this.replicatorExtension = new AutomergeReplicator(
170
- {
171
- peerId: this._params.ownPeerId,
172
- },
173
- {
174
- onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
175
- // Note: We store only one extension per peer.
176
- // There can be a case where two connected peers have more than one teleport connection between them
177
- // and each of them uses different teleport connections to send messages.
178
- // It works because we receive messages from all teleport connections and Automerge Repo dedup them.
179
- // TODO(mykola): Use only one teleport connection per peer.
180
-
181
- // TODO(dmaretskyi): Critical bug.
182
- // - two peers get connected via swarm 1
183
- // - they get connected via swarm 2
184
- // - swarm 1 gets disconnected
185
- // - automerge repo thinks that peer 2 got disconnected even though swarm 2 is still active
186
-
187
- this.remoteDeviceKey = remotePeerId;
188
-
189
- // Set automerge id.
190
- this._remotePeerId = info.id;
191
-
192
- log('onStartReplication', { id: info.id, thisPeerId: this.peerId, remotePeerId: remotePeerId.toHex() });
193
-
194
- await this._params.onRemoteConnected();
195
- },
196
- onSyncMessage: async ({ payload }) => {
197
- if (!this._isEnabled) {
198
- return;
199
- }
200
- const message = cbor.decode(payload) as Message;
201
- // Note: automerge Repo dedup messages.
202
- readableStreamController.enqueue(message);
203
- },
204
- onClose: async () => {
205
- if (!this._isEnabled) {
206
- return;
207
- }
208
- await this._params.onRemoteDisconnected();
209
- },
210
- },
211
- );
212
- }
213
-
214
- get peerId(): string {
215
- invariant(this._remotePeerId != null, 'Remote peer has not connected yet.');
216
- return this._remotePeerId;
217
- }
218
-
219
- async shouldAdvertize(params: ShouldAdvertizeParams): Promise<boolean> {
220
- return this._params.shouldAdvertize(params);
221
- }
222
-
223
- /**
224
- * Start exchanging messages with the remote peer.
225
- * Call after the remote peer has connected.
226
- */
227
- async enable() {
228
- invariant(this._remotePeerId != null, 'Remote peer has not connected yet.');
229
- this._isEnabled = true;
230
- }
231
-
232
- /**
233
- * Stop exchanging messages with the remote peer.
234
- */
235
- async disable() {
236
- this._isEnabled = false;
237
- }
238
- }
@@ -2,29 +2,43 @@
2
2
  // Copyright 2021 DXOS.org
3
3
  //
4
4
 
5
- import { type Stream } from '@dxos/codec-protobuf';
5
+ import { type RequestOptions, type Stream } from '@dxos/codec-protobuf';
6
6
  import {
7
7
  type DataService,
8
+ type DocHeadsList,
8
9
  type EchoEvent,
9
10
  type FlushRequest,
10
11
  type GetDocumentHeadsRequest,
11
12
  type GetDocumentHeadsResponse,
12
13
  type HostInfo,
13
14
  type MutationReceipt,
15
+ type ReIndexHeadsRequest,
14
16
  type SubscribeRequest,
15
17
  type SyncRepoRequest,
16
18
  type SyncRepoResponse,
19
+ type WaitUntilHeadsReplicatedRequest,
17
20
  type WriteRequest,
18
21
  } from '@dxos/protocols/proto/dxos/echo/service';
19
22
 
20
23
  import { type AutomergeHost, type DocumentId } from '../automerge';
21
24
 
25
+ export type DataServiceParams = {
26
+ automergeHost: AutomergeHost;
27
+ updateIndexes: () => Promise<void>;
28
+ };
29
+
22
30
  /**
23
31
  * Data sync between client and services.
24
32
  */
25
33
  // TODO(burdon): Move to client-services.
26
34
  export class DataServiceImpl implements DataService {
27
- constructor(private readonly _automergeHost: AutomergeHost) {}
35
+ private readonly _automergeHost: AutomergeHost;
36
+ private readonly _updateIndexes: () => Promise<void>;
37
+
38
+ constructor(params: DataServiceParams) {
39
+ this._automergeHost = params.automergeHost;
40
+ this._updateIndexes = params.updateIndexes;
41
+ }
28
42
 
29
43
  subscribe(request: SubscribeRequest): Stream<EchoEvent> {
30
44
  throw new Error('Deprecated.');
@@ -53,8 +67,8 @@ export class DataServiceImpl implements DataService {
53
67
  }
54
68
 
55
69
  async getDocumentHeads(request: GetDocumentHeadsRequest): Promise<GetDocumentHeadsResponse> {
56
- const states = await Promise.all(
57
- request.documentIds?.map(async (documentId): Promise<GetDocumentHeadsResponse.DocState> => {
70
+ const entries = await Promise.all(
71
+ request.documentIds?.map(async (documentId): Promise<DocHeadsList.Entry> => {
58
72
  const heads = await this._automergeHost.getHeads(documentId as DocumentId);
59
73
  return {
60
74
  documentId,
@@ -62,6 +76,25 @@ export class DataServiceImpl implements DataService {
62
76
  };
63
77
  }) ?? [],
64
78
  );
65
- return { states };
79
+ return {
80
+ heads: {
81
+ entries,
82
+ },
83
+ };
84
+ }
85
+
86
+ async waitUntilHeadsReplicated(
87
+ request: WaitUntilHeadsReplicatedRequest,
88
+ options?: RequestOptions | undefined,
89
+ ): Promise<void> {
90
+ await this._automergeHost.waitUntilHeadsReplicated(request.heads);
91
+ }
92
+
93
+ async reIndexHeads(request: ReIndexHeadsRequest, options?: RequestOptions): Promise<void> {
94
+ await this._automergeHost.reIndexHeads((request.documentIds ?? []) as DocumentId[]);
95
+ }
96
+
97
+ async updateIndexes() {
98
+ await this._updateIndexes();
66
99
  }
67
100
  }