@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.
- package/dist/lib/browser/{chunk-UJQ5VS5V.mjs → chunk-SJUDZ3CQ.mjs} +17 -7
- package/dist/lib/browser/chunk-SJUDZ3CQ.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +562 -57
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/node/{chunk-RH6TDRML.cjs → chunk-NLHNTXVQ.cjs} +20 -10
- package/dist/lib/node/chunk-NLHNTXVQ.cjs.map +7 -0
- package/dist/lib/node/index.cjs +582 -79
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +11 -11
- package/dist/types/src/automerge/automerge-doc-loader.d.ts +71 -0
- package/dist/types/src/automerge/automerge-doc-loader.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-doc-loader.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-doc-loader.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-host.d.ts +17 -2
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +1 -1
- package/dist/types/src/automerge/index.d.ts +2 -0
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/local-host-network-adapter.d.ts +30 -0
- package/dist/types/src/automerge/local-host-network-adapter.d.ts.map +1 -0
- package/dist/types/src/db-host/data-service.d.ts +5 -2
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-doc-loader.test.ts +103 -0
- package/src/automerge/automerge-doc-loader.ts +267 -0
- package/src/automerge/automerge-host.ts +56 -6
- package/src/automerge/automerge-repo.test.ts +1 -124
- package/src/automerge/echo-network-adapter.ts +1 -1
- package/src/automerge/index.ts +2 -0
- package/src/automerge/local-host-network-adapter.ts +115 -0
- package/src/db-host/data-service.ts +20 -3
- package/src/db-host/documents-synchronizer.test.ts +1 -1
- package/src/db-host/documents-synchronizer.ts +1 -1
- package/dist/lib/browser/chunk-UJQ5VS5V.mjs.map +0 -7
- 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
|
});
|
package/src/automerge/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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> => {
|
|
@@ -73,7 +73,7 @@ export class DocumentsSynchronizer extends Resource {
|
|
|
73
73
|
this._syncStates.clear();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
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);
|