@dxos/echo-pipeline 0.6.2-main.8a232a5 → 0.6.2-main.d41f0d2

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 (40) hide show
  1. package/dist/lib/browser/{chunk-UJQ5VS5V.mjs → chunk-SJUDZ3CQ.mjs} +17 -7
  2. package/dist/lib/browser/chunk-SJUDZ3CQ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +562 -57
  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-RH6TDRML.cjs → chunk-NLHNTXVQ.cjs} +20 -10
  8. package/dist/lib/node/chunk-NLHNTXVQ.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +582 -79
  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-doc-loader.d.ts +71 -0
  14. package/dist/types/src/automerge/automerge-doc-loader.d.ts.map +1 -0
  15. package/dist/types/src/automerge/automerge-doc-loader.test.d.ts +2 -0
  16. package/dist/types/src/automerge/automerge-doc-loader.test.d.ts.map +1 -0
  17. package/dist/types/src/automerge/automerge-host.d.ts +17 -2
  18. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  19. package/dist/types/src/automerge/echo-network-adapter.d.ts +1 -1
  20. package/dist/types/src/automerge/index.d.ts +2 -0
  21. package/dist/types/src/automerge/index.d.ts.map +1 -1
  22. package/dist/types/src/automerge/local-host-network-adapter.d.ts +30 -0
  23. package/dist/types/src/automerge/local-host-network-adapter.d.ts.map +1 -0
  24. package/dist/types/src/db-host/data-service.d.ts +5 -2
  25. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  26. package/dist/types/src/db-host/documents-synchronizer.d.ts +1 -1
  27. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  28. package/package.json +33 -33
  29. package/src/automerge/automerge-doc-loader.test.ts +103 -0
  30. package/src/automerge/automerge-doc-loader.ts +267 -0
  31. package/src/automerge/automerge-host.ts +56 -6
  32. package/src/automerge/automerge-repo.test.ts +1 -124
  33. package/src/automerge/echo-network-adapter.ts +1 -1
  34. package/src/automerge/index.ts +2 -0
  35. package/src/automerge/local-host-network-adapter.ts +115 -0
  36. package/src/db-host/data-service.ts +20 -3
  37. package/src/db-host/documents-synchronizer.test.ts +1 -1
  38. package/src/db-host/documents-synchronizer.ts +1 -1
  39. package/dist/lib/browser/chunk-UJQ5VS5V.mjs.map +0 -7
  40. package/dist/lib/node/chunk-RH6TDRML.cjs.map +0 -7
@@ -6,18 +6,7 @@ import { expect } from 'chai';
6
6
  import waitForExpect from 'wait-for-expect';
7
7
 
8
8
  import { asyncTimeout, sleep } from '@dxos/async';
9
- import {
10
- next as A,
11
- type Heads,
12
- change,
13
- clone,
14
- equals,
15
- from,
16
- getBackend,
17
- getHeads,
18
- save,
19
- saveSince,
20
- } from '@dxos/automerge/automerge';
9
+ import { type Heads, change, clone, equals, from, getBackend, getHeads } from '@dxos/automerge/automerge';
21
10
  import {
22
11
  type Message,
23
12
  Repo,
@@ -25,8 +14,6 @@ import {
25
14
  type DocumentId,
26
15
  type HandleState,
27
16
  type AutomergeUrl,
28
- parseAutomergeUrl,
29
- generateAutomergeUrl,
30
17
  } from '@dxos/automerge/automerge-repo';
31
18
  import { randomBytes } from '@dxos/crypto';
32
19
  import { PublicKey } from '@dxos/keys';
@@ -643,115 +630,5 @@ describe('AutomergeRepo', () => {
643
630
  expect(peer2.handles[hostHandle.documentId]).to.not.be.undefined;
644
631
  });
645
632
  });
646
-
647
- test('client cold-starts and syncs doc from a Repo', async () => {
648
- const repo = new Repo({ network: [] });
649
- const serverHandle = repo.create<{ field?: string }>();
650
-
651
- let clientDoc = A.from<{ field?: string }>({});
652
- const receiveByClient = (blob: Uint8Array) => {
653
- clientDoc = A.loadIncremental(clientDoc, blob);
654
- };
655
-
656
- // Sync handshake.
657
- let syncedHeads = getHeads(serverHandle.docSync());
658
- receiveByClient(save(serverHandle.docSync()));
659
-
660
- serverHandle.on('change', ({ doc }) => {
661
- // Note: This is mock of a sync protocol between client and server.
662
- const blob = saveSince(doc, syncedHeads);
663
- syncedHeads = getHeads(doc);
664
- receiveByClient(blob);
665
- });
666
-
667
- {
668
- const value = 'text to test if sync works';
669
- serverHandle.change((doc: any) => {
670
- doc.field = value;
671
- });
672
- expect(clientDoc.field).to.deep.equal(value);
673
- }
674
-
675
- {
676
- const value = 'test if updates propagate';
677
- serverHandle.change((doc: any) => {
678
- doc.field = value;
679
- });
680
- expect(clientDoc.field).to.deep.equal(value);
681
- }
682
- });
683
-
684
- test('client creates doc and syncs with a Repo', async () => {
685
- const repo = new Repo({ network: [] });
686
- const receiveByServer = async (blob: Uint8Array, docId: DocumentId) => {
687
- const serverHandle = repo.find(docId);
688
- serverHandle.update((doc) => {
689
- return A.loadIncremental(doc, blob);
690
- });
691
- };
692
-
693
- let clientDoc = A.from<{ field?: string }>({});
694
- const { documentId } = parseAutomergeUrl(generateAutomergeUrl());
695
- // Sync handshake.
696
- let sentHeads = getHeads(clientDoc);
697
-
698
- // Sync protocol.
699
- const sendDoc = async (doc: A.Doc<any>) => {
700
- await receiveByServer(saveSince(doc, sentHeads), documentId);
701
- sentHeads = getHeads(doc);
702
- };
703
-
704
- {
705
- // Change doc and send changes to server.
706
- const value = 'text to test if sync works';
707
- clientDoc = A.change(clientDoc, (doc: any) => {
708
- doc.field = value;
709
- });
710
- await sendDoc(clientDoc);
711
-
712
- await repo.find(documentId).whenReady();
713
- expect(repo.find(documentId).docSync().field).to.deep.equal(value);
714
- }
715
- });
716
-
717
- test('two repo sync docs on `update` call', async () => {
718
- const [adapter1, adapter2] = TestAdapter.createPair();
719
- const repoA = new Repo({
720
- peerId: 'A' as any,
721
- network: [adapter1],
722
- sharePolicy: async () => true,
723
- });
724
- const repoB = new Repo({
725
- peerId: 'B' as any,
726
- network: [adapter2],
727
- sharePolicy: async () => true,
728
- });
729
-
730
- {
731
- // Connect repos.
732
- adapter1.ready();
733
- adapter2.ready();
734
- await adapter1.onConnect.wait();
735
- await adapter2.onConnect.wait();
736
- adapter1.peerCandidate(adapter2.peerId!);
737
- adapter2.peerCandidate(adapter1.peerId!);
738
- }
739
-
740
- const handleA = repoA.create();
741
- const handleB = repoB.find(handleA.url);
742
-
743
- const text = 'Hello world';
744
- handleA.update((doc: any) => {
745
- const newDoc = A.change(doc, (doc: any) => {
746
- doc.text = text;
747
- });
748
- return newDoc;
749
- });
750
-
751
- expect(handleA.docSync().text).to.equal(text);
752
-
753
- await asyncTimeout(handleB.whenReady(), 1000);
754
- expect(handleB.docSync().text).to.equal(text);
755
- });
756
633
  });
757
634
  });
@@ -71,7 +71,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
71
71
  @synchronized
72
72
  async close() {
73
73
  if (this._lifecycleState === LifecycleState.CLOSED) {
74
- return this;
74
+ return;
75
75
  }
76
76
 
77
77
  for (const replicator of this._replicators) {
@@ -3,6 +3,8 @@
3
3
  //
4
4
 
5
5
  export * from './automerge-host';
6
+ export * from './automerge-doc-loader';
6
7
  export * from './leveldb-storage-adapter';
8
+ export * from './local-host-network-adapter';
7
9
  export * from './mesh-echo-replicator';
8
10
  export * from './echo-replicator';
@@ -0,0 +1,115 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Trigger } from '@dxos/async';
6
+ import { NetworkAdapter, type Message, type PeerId, cbor } from '@dxos/automerge/automerge-repo';
7
+ import { Stream } from '@dxos/codec-protobuf';
8
+ import { invariant } from '@dxos/invariant';
9
+ import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
10
+
11
+ type ClientSyncState = {
12
+ connected: boolean;
13
+ send: (message: Message) => void;
14
+ disconnect: () => void;
15
+ };
16
+
17
+ /**
18
+ * Used to replicate with apps running on the same device.
19
+ */
20
+ export class LocalHostNetworkAdapter extends NetworkAdapter {
21
+ private readonly _peers: Map<PeerId, ClientSyncState> = new Map();
22
+
23
+ /**
24
+ * Emits `ready` event. That signals to `Repo` that it can start using the adapter.
25
+ */
26
+ ready() {
27
+ // NOTE: Emitting `ready` event in NetworkAdapter`s constructor causes a race condition
28
+ // because `Repo` waits for `ready` event (which it never receives) before it starts using the adapter.
29
+ this.emit('ready', {
30
+ network: this,
31
+ });
32
+ }
33
+
34
+ private readonly _connected = new Trigger();
35
+ private _isConnected: boolean = false;
36
+
37
+ /**
38
+ * Called by `Repo` to connect to the network.
39
+ *
40
+ * @param peerId Our peer Id.
41
+ */
42
+ override connect(peerId: PeerId): void {
43
+ this.peerId = peerId;
44
+ this._isConnected = true;
45
+ this._connected.wake();
46
+ // No-op. Client always connects first
47
+ }
48
+
49
+ override send(message: Message): void {
50
+ const peer = this._peers.get(message.targetId);
51
+ invariant(peer, 'Peer not found.');
52
+ peer.send(message);
53
+ }
54
+
55
+ async close() {
56
+ this._peers.forEach((peer) => peer.disconnect());
57
+ this.emit('close');
58
+ }
59
+
60
+ override disconnect(): void {
61
+ // TODO(mykola): `disconnect` is not used anywhere in `Repo` from `@automerge/automerge-repo`. Should we remove it?
62
+ // No-op
63
+ }
64
+
65
+ async whenConnected(): Promise<void> {
66
+ await this._connected.wait({ timeout: 10_000 });
67
+ }
68
+
69
+ syncRepo({ id, syncMessage }: SyncRepoRequest): Stream<SyncRepoResponse> {
70
+ const peerId = this._getPeerId(id);
71
+
72
+ return new Stream(({ next, close }) => {
73
+ invariant(!this._peers.has(peerId), 'Peer already connected.');
74
+ this._peers.set(peerId, {
75
+ connected: true,
76
+ send: (message) => {
77
+ next({
78
+ syncMessage: cbor.encode(message),
79
+ });
80
+ },
81
+ disconnect: () => {
82
+ this._peers.delete(peerId);
83
+ close();
84
+ this.emit('peer-disconnected', {
85
+ peerId,
86
+ });
87
+ },
88
+ });
89
+
90
+ invariant(this._isConnected);
91
+ this.emit('peer-candidate', {
92
+ peerMetadata: {},
93
+ peerId,
94
+ });
95
+ });
96
+ }
97
+
98
+ async sendSyncMessage({ id, syncMessage }: SyncRepoRequest): Promise<void> {
99
+ invariant(this._isConnected);
100
+ const message = cbor.decode(syncMessage!) as Message;
101
+ this.emit('message', message);
102
+ }
103
+
104
+ async getHostInfo(): Promise<HostInfo> {
105
+ invariant(this._isConnected);
106
+ invariant(this.peerId, 'Peer id not set.');
107
+ return {
108
+ peerId: this.peerId,
109
+ };
110
+ }
111
+
112
+ private _getPeerId(id: string): PeerId {
113
+ return id as PeerId;
114
+ }
115
+ }
@@ -10,14 +10,17 @@ import {
10
10
  type DataService,
11
11
  type DocHeadsList,
12
12
  type FlushRequest,
13
+ type HostInfo,
13
14
  type SubscribeRequest,
14
15
  type BatchedDocumentUpdates,
15
16
  type UpdateSubscriptionRequest,
16
17
  type GetDocumentHeadsRequest,
17
18
  type GetDocumentHeadsResponse,
18
19
  type ReIndexHeadsRequest,
20
+ type SyncRepoRequest,
21
+ type SyncRepoResponse,
19
22
  type WaitUntilHeadsReplicatedRequest,
20
- type UpdateRequest,
23
+ type WriteRequest,
21
24
  } from '@dxos/protocols/proto/dxos/echo/service';
22
25
 
23
26
  import { DocumentsSynchronizer } from './documents-synchronizer';
@@ -76,20 +79,34 @@ export class DataServiceImpl implements DataService {
76
79
  }
77
80
  }
78
81
 
79
- async update(request: UpdateRequest): Promise<void> {
82
+ async write(request: WriteRequest): Promise<void> {
80
83
  if (!request.updates) {
81
84
  return;
82
85
  }
83
86
  const synchronizer = this._subscriptions.get(request.subscriptionId);
84
87
  invariant(synchronizer, 'Subscription not found');
85
88
 
86
- synchronizer.update(request.updates);
89
+ synchronizer.write(request.updates);
87
90
  }
88
91
 
89
92
  async flush(request: FlushRequest): Promise<void> {
90
93
  await this._automergeHost.flush(request);
91
94
  }
92
95
 
96
+ // Automerge specific.
97
+
98
+ async getHostInfo(request: void): Promise<HostInfo> {
99
+ return this._automergeHost.getHostInfo();
100
+ }
101
+
102
+ syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse> {
103
+ return this._automergeHost.syncRepo(request);
104
+ }
105
+
106
+ sendSyncMessage(request: SyncRepoRequest): Promise<void> {
107
+ return this._automergeHost.sendSyncMessage(request);
108
+ }
109
+
93
110
  async getDocumentHeads(request: GetDocumentHeadsRequest): Promise<GetDocumentHeadsResponse> {
94
111
  const entries = await Promise.all(
95
112
  request.documentIds?.map(async (documentId): Promise<DocHeadsList.Entry> => {
@@ -23,7 +23,7 @@ describe('DocumentsSynchronizer', () => {
23
23
  });
24
24
  await openAndClose(synchronizer);
25
25
 
26
- synchronizer.update([
26
+ synchronizer.write([
27
27
  {
28
28
  documentId: parseAutomergeUrl(generateAutomergeUrl()).documentId,
29
29
  mutation: A.save(A.from({ text: 'hello' })),
@@ -73,7 +73,7 @@ export class DocumentsSynchronizer extends Resource {
73
73
  this._syncStates.clear();
74
74
  }
75
75
 
76
- update(updates: DocumentUpdate[]) {
76
+ write(updates: DocumentUpdate[]) {
77
77
  for (const { documentId, mutation, isNew } of updates) {
78
78
  if (isNew) {
79
79
  const doc = this._params.repo.find(documentId as DocumentId);