@dxos/echo-pipeline 0.8.3 → 0.8.4-main.03d5cd7b56
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-35I6ERLG.mjs → neutral/chunk-VIHYDCCC.mjs} +313 -709
- package/dist/lib/neutral/chunk-VIHYDCCC.mjs.map +7 -0
- package/dist/lib/neutral/chunk-YBC6ONBZ.mjs +247 -0
- package/dist/lib/neutral/chunk-YBC6ONBZ.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
- package/dist/lib/neutral/index.mjs +7065 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-host.d.ts +99 -54
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
- package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +38 -15
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/handle-state.d.ts +52 -0
- package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
- package/dist/types/src/automerge/heads-store.d.ts +9 -2
- package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +2 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +9 -5
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
- package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
- package/dist/types/src/common/codec.d.ts +1 -1
- package/dist/types/src/common/codec.d.ts.map +1 -1
- package/dist/types/src/common/feeds.d.ts.map +1 -1
- package/dist/types/src/common/space-id.d.ts.map +1 -1
- package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
- package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts +8 -7
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +9 -3
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +9 -8
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +62 -27
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +2 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
- package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
- package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +19 -11
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
- package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
- package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
- package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/stub.d.ts +11 -0
- package/dist/types/src/db-host/stub.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
- package/dist/types/src/edge/index.d.ts +1 -0
- package/dist/types/src/edge/index.d.ts.map +1 -1
- package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
- package/dist/types/src/filter/filter-match.d.ts +4 -1
- package/dist/types/src/filter/filter-match.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline.d.ts +3 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +22 -10
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +66 -8
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +66 -11
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +12 -1
- package/dist/types/src/query/query-planner.d.ts.map +1 -1
- package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
- package/dist/types/src/space/auth.d.ts +3 -3
- package/dist/types/src/space/auth.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts +5 -4
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +8 -8
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +10 -9
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +6 -6
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +0 -2
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
- package/dist/types/src/testing/test-replicator.d.ts +14 -13
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +56 -54
- package/src/automerge/automerge-host-subduction.test.ts +247 -0
- package/src/automerge/automerge-host.test.ts +65 -39
- package/src/automerge/automerge-host.ts +589 -206
- package/src/automerge/automerge-repo-subduction.test.ts +1049 -0
- package/src/automerge/automerge-repo.test.ts +349 -91
- package/src/automerge/automerge-subduction.test.ts +330 -0
- package/src/automerge/collection-synchronizer.test.ts +53 -17
- package/src/automerge/collection-synchronizer.ts +119 -36
- package/src/automerge/echo-data-monitor.ts +1 -1
- package/src/automerge/echo-network-adapter.test.ts +5 -4
- package/src/automerge/echo-network-adapter.ts +74 -30
- package/src/automerge/echo-replicator.ts +41 -15
- package/src/automerge/handle-state.ts +62 -0
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +2 -1
- package/src/automerge/leveldb-storage-adapter.ts +40 -11
- package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
- package/src/automerge/mesh-echo-replicator.ts +15 -11
- package/src/automerge/storage-adapter.test.ts +1 -1
- package/src/common/space-id.ts +1 -1
- package/src/db-host/automerge-data-source.test.ts +299 -0
- package/src/db-host/automerge-data-source.ts +187 -0
- package/src/db-host/data-service.ts +25 -26
- package/src/db-host/database-root.ts +20 -8
- package/src/db-host/documents-synchronizer.test.ts +143 -14
- package/src/db-host/documents-synchronizer.ts +72 -55
- package/src/db-host/echo-host.ts +348 -96
- package/src/db-host/index.ts +2 -0
- package/src/db-host/invalidation-hint.ts +50 -0
- package/src/db-host/local-queue-service.ts +127 -0
- package/src/db-host/query-invalidation.test.ts +270 -0
- package/src/db-host/query-service.ts +111 -106
- package/src/db-host/queue-data-source.ts +160 -0
- package/src/db-host/queue-service.test.ts +145 -0
- package/src/db-host/space-state-manager.ts +12 -10
- package/src/db-host/stub.ts +33 -0
- package/src/edge/echo-edge-replicator.test.ts +12 -10
- package/src/edge/echo-edge-replicator.ts +144 -58
- package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
- package/src/edge/echo-edge-subduction-replicator.ts +377 -0
- package/src/edge/index.ts +1 -0
- package/src/filter/filter-match.test.ts +68 -32
- package/src/filter/filter-match.ts +184 -20
- package/src/metadata/metadata-store.ts +11 -8
- package/src/pipeline/pipeline-stress.test.ts +1 -1
- package/src/pipeline/pipeline.test.ts +2 -2
- package/src/pipeline/pipeline.ts +9 -7
- package/src/query/errors.ts +3 -1
- package/src/query/plan.ts +82 -8
- package/src/query/query-executor.ts +1199 -164
- package/src/query/query-planner.test.ts +993 -81
- package/src/query/query-planner.ts +467 -27
- package/src/space/admission-discovery-extension.ts +3 -3
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +5 -4
- package/src/space/control-pipeline.ts +16 -13
- package/src/space/space-manager.browser.test.ts +1 -1
- package/src/space/space-manager.ts +16 -20
- package/src/space/space-protocol.browser.test.ts +15 -14
- package/src/space/space-protocol.test.ts +15 -14
- package/src/space/space-protocol.ts +22 -18
- package/src/space/space.test.ts +3 -3
- package/src/space/space.ts +11 -11
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +3 -3
- package/src/testing/test-network-adapter.ts +24 -3
- package/src/testing/test-replicator.ts +19 -20
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
- package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
- package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -4743
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
- package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
- package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
- package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
- package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
- package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
- package/dist/lib/node/filter/index.cjs +0 -32
- package/dist/lib/node/filter/index.cjs.map +0 -7
- package/dist/lib/node/index.cjs +0 -4699
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node/testing/index.cjs +0 -753
- package/dist/lib/node/testing/index.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
- package/dist/lib/node-esm/filter/index.mjs +0 -11
- package/dist/lib/node-esm/filter/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -4743
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -742
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
- package/dist/types/src/testing/test-data.d.ts +0 -18
- package/dist/types/src/testing/test-data.d.ts.map +0 -1
- package/dist/types/src/testing/test-schema.d.ts +0 -37
- package/dist/types/src/testing/test-schema.d.ts.map +0 -1
- package/src/db-host/documents-iterator.ts +0 -74
- package/src/testing/test-data.ts +0 -127
- package/src/testing/test-schema.ts +0 -53
- /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
DeleteFromQueueRequest,
|
|
7
|
+
InsertIntoQueueRequest,
|
|
8
|
+
QueryQueueRequest,
|
|
9
|
+
QueueQueryResult,
|
|
10
|
+
QueueService,
|
|
11
|
+
SyncQueueRequest,
|
|
12
|
+
} from '@dxos/protocols/proto/dxos/client/services';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Stub implementation for when Edge is not available.
|
|
16
|
+
*/
|
|
17
|
+
export class QueueServiceStub implements QueueService {
|
|
18
|
+
queryQueue(request: QueryQueueRequest): Promise<QueueQueryResult> {
|
|
19
|
+
throw new Error('Not available.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
insertIntoQueue(request: InsertIntoQueueRequest): Promise<void> {
|
|
23
|
+
throw new Error('Not available.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
deleteFromQueue(request: DeleteFromQueueRequest): Promise<void> {
|
|
27
|
+
throw new Error('Not available.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
syncQueue(request: SyncQueueRequest): Promise<void> {
|
|
31
|
+
throw new Error('Not available.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -7,7 +7,8 @@ import { getRandomPort } from 'get-port-please';
|
|
|
7
7
|
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
8
8
|
|
|
9
9
|
import { Event } from '@dxos/async';
|
|
10
|
-
import {
|
|
10
|
+
import { Context } from '@dxos/context';
|
|
11
|
+
import { EdgeClient, type EdgeHttpClient, MessageSchema, createEphemeralEdgeIdentity } from '@dxos/edge-client';
|
|
11
12
|
import { createTestEdgeWsServer } from '@dxos/edge-client/testing';
|
|
12
13
|
import { PublicKey, SpaceId } from '@dxos/keys';
|
|
13
14
|
import { EdgeService } from '@dxos/protocols';
|
|
@@ -16,8 +17,8 @@ import { createBuf } from '@dxos/protocols/buf';
|
|
|
16
17
|
import type { Peer } from '@dxos/protocols/proto/dxos/edge/messenger';
|
|
17
18
|
import { openAndClose } from '@dxos/test-utils';
|
|
18
19
|
|
|
20
|
+
import type { AutomergeReplicatorConnection, AutomergeReplicatorContext } from '../automerge';
|
|
19
21
|
import { EchoEdgeReplicator } from './echo-edge-replicator';
|
|
20
|
-
import type { EchoReplicatorContext, ReplicatorConnection } from '../automerge';
|
|
21
22
|
|
|
22
23
|
describe('EchoEdgeReplicator', () => {
|
|
23
24
|
test('reconnects', async () => {
|
|
@@ -27,7 +28,7 @@ describe('EchoEdgeReplicator', () => {
|
|
|
27
28
|
|
|
28
29
|
const { context, connectionOpen } = createMockContext();
|
|
29
30
|
const replicator = await connectReplicator(client, context);
|
|
30
|
-
await replicator.connectToSpace(spaceId);
|
|
31
|
+
await replicator.connectToSpace(Context.default(), spaceId);
|
|
31
32
|
|
|
32
33
|
client.setIdentity(await createEphemeralEdgeIdentity());
|
|
33
34
|
await connectionOpen.waitForCount(1);
|
|
@@ -54,7 +55,7 @@ describe('EchoEdgeReplicator', () => {
|
|
|
54
55
|
documentSpaceId: { [documentId]: spaceId },
|
|
55
56
|
});
|
|
56
57
|
const replicator = await connectReplicator(client, context);
|
|
57
|
-
await replicator.connectToSpace(spaceId);
|
|
58
|
+
await replicator.connectToSpace(Context.default(), spaceId);
|
|
58
59
|
|
|
59
60
|
await expect.poll(() => openConnections.length === 1).toBeTruthy();
|
|
60
61
|
expect(openConnections[0].shouldAdvertise({ documentId })).toBeTruthy();
|
|
@@ -68,7 +69,7 @@ describe('EchoEdgeReplicator', () => {
|
|
|
68
69
|
const remoteCollections: { [peerId: string]: { [documentId: string]: boolean } } = {};
|
|
69
70
|
const { context, openConnections } = createMockContext({ remoteCollections });
|
|
70
71
|
const replicator = await connectReplicator(client, context);
|
|
71
|
-
await replicator.connectToSpace(spaceId);
|
|
72
|
+
await replicator.connectToSpace(Context.default(), spaceId);
|
|
72
73
|
|
|
73
74
|
await expect.poll(() => openConnections.length === 1).toBeTruthy();
|
|
74
75
|
const connection = openConnections[0];
|
|
@@ -78,9 +79,10 @@ describe('EchoEdgeReplicator', () => {
|
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
const connectReplicator = async (client: EdgeClient, context:
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
const connectReplicator = async (client: EdgeClient, context: AutomergeReplicatorContext) => {
|
|
83
|
+
// EdgeHttpClient functionality is not tested here.
|
|
84
|
+
const replicator = new EchoEdgeReplicator({ edgeConnection: client, edgeHttpClient: {} as EdgeHttpClient });
|
|
85
|
+
await replicator.connect(Context.default(), context);
|
|
84
86
|
onTestFinished(() => replicator.disconnect());
|
|
85
87
|
return replicator;
|
|
86
88
|
};
|
|
@@ -99,7 +101,7 @@ const createMockContext = (args?: {
|
|
|
99
101
|
documentSpaceId?: { [documentId: string]: SpaceId };
|
|
100
102
|
}) => {
|
|
101
103
|
const connectionOpen = new Event();
|
|
102
|
-
const openConnections:
|
|
104
|
+
const openConnections: AutomergeReplicatorConnection[] = [];
|
|
103
105
|
return {
|
|
104
106
|
context: {
|
|
105
107
|
getContainingSpaceIdForDocument: async (documentId) => args?.documentSpaceId?.[documentId] ?? null,
|
|
@@ -119,7 +121,7 @@ const createMockContext = (args?: {
|
|
|
119
121
|
},
|
|
120
122
|
|
|
121
123
|
peerId: PublicKey.random().toHex(),
|
|
122
|
-
} satisfies
|
|
124
|
+
} satisfies AutomergeReplicatorContext,
|
|
123
125
|
|
|
124
126
|
openConnections,
|
|
125
127
|
connectionOpen,
|
|
@@ -2,33 +2,43 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as A from '@automerge/automerge';
|
|
6
|
+
import { type DocumentId, type Heads, cbor } from '@automerge/automerge-repo';
|
|
6
7
|
|
|
7
|
-
import { Mutex,
|
|
8
|
+
import { Mutex, scheduleMicroTask, scheduleTask } from '@dxos/async';
|
|
8
9
|
import { Context, Resource } from '@dxos/context';
|
|
9
10
|
import { randomUUID } from '@dxos/crypto';
|
|
10
11
|
import type { CollectionId } from '@dxos/echo-protocol';
|
|
11
|
-
import { type EdgeConnection } from '@dxos/edge-client';
|
|
12
|
+
import { type EdgeConnection, type EdgeHttpClient } from '@dxos/edge-client';
|
|
12
13
|
import { invariant } from '@dxos/invariant';
|
|
13
14
|
import type { SpaceId } from '@dxos/keys';
|
|
14
15
|
import { log } from '@dxos/log';
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type AutomergeProtocolMessage,
|
|
18
|
+
DocumentCodec,
|
|
19
|
+
EdgeService,
|
|
20
|
+
type ErrorProtocolMessage,
|
|
21
|
+
type ExportBundleRequest,
|
|
22
|
+
type ImportBundleRequest,
|
|
23
|
+
type PeerId,
|
|
24
|
+
} from '@dxos/protocols';
|
|
16
25
|
import { buf } from '@dxos/protocols/buf';
|
|
17
26
|
import {
|
|
18
27
|
type Message as RouterMessage,
|
|
19
28
|
MessageSchema as RouterMessageSchema,
|
|
20
29
|
} from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
21
|
-
import {
|
|
30
|
+
import { trace } from '@dxos/tracing';
|
|
31
|
+
import { bufferToArray, compositeKey, setDeep } from '@dxos/util';
|
|
22
32
|
|
|
23
|
-
import { InflightRequestLimiter } from './inflight-request-limiter';
|
|
24
33
|
import {
|
|
34
|
+
type AutomergeReplicator,
|
|
35
|
+
type AutomergeReplicatorConnection,
|
|
36
|
+
type AutomergeReplicatorContext,
|
|
37
|
+
type ShouldAdvertiseProps,
|
|
38
|
+
type ShouldSyncCollectionProps,
|
|
25
39
|
getSpaceIdFromCollectionId,
|
|
26
|
-
type EchoReplicator,
|
|
27
|
-
type EchoReplicatorContext,
|
|
28
|
-
type ReplicatorConnection,
|
|
29
|
-
type ShouldAdvertiseParams,
|
|
30
|
-
type ShouldSyncCollectionParams,
|
|
31
40
|
} from '../automerge';
|
|
41
|
+
import { InflightRequestLimiter } from './inflight-request-limiter';
|
|
32
42
|
|
|
33
43
|
/**
|
|
34
44
|
* Delay before restarting the connection after receiving a forbidden error.
|
|
@@ -37,30 +47,34 @@ const INITIAL_RESTART_DELAY = 500;
|
|
|
37
47
|
const RESTART_DELAY_JITTER = 250;
|
|
38
48
|
const MAX_RESTART_DELAY = 5000;
|
|
39
49
|
|
|
40
|
-
export type
|
|
50
|
+
export type EchoEdgeReplicatorProps = {
|
|
41
51
|
edgeConnection: EdgeConnection;
|
|
52
|
+
edgeHttpClient: EdgeHttpClient;
|
|
42
53
|
disableSharePolicy?: boolean;
|
|
43
54
|
};
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
@trace.resource()
|
|
57
|
+
export class EchoEdgeReplicator implements AutomergeReplicator {
|
|
46
58
|
private readonly _edgeConnection: EdgeConnection;
|
|
59
|
+
private readonly _edgeHttpClient: EdgeHttpClient;
|
|
47
60
|
private readonly _mutex = new Mutex();
|
|
48
61
|
|
|
49
62
|
private _ctx?: Context = undefined;
|
|
50
|
-
private _context:
|
|
63
|
+
private _context: AutomergeReplicatorContext | null = null;
|
|
51
64
|
private _connectedSpaces = new Set<SpaceId>();
|
|
52
65
|
private _connections = new Map<SpaceId, EdgeReplicatorConnection>();
|
|
53
66
|
private _sharePolicyEnabled = true;
|
|
54
67
|
|
|
55
|
-
constructor({ edgeConnection, disableSharePolicy }:
|
|
68
|
+
constructor({ edgeConnection, edgeHttpClient, disableSharePolicy }: EchoEdgeReplicatorProps) {
|
|
56
69
|
this._edgeConnection = edgeConnection;
|
|
70
|
+
this._edgeHttpClient = edgeHttpClient;
|
|
57
71
|
this._sharePolicyEnabled = !disableSharePolicy;
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
async connect(context:
|
|
74
|
+
async connect(ctx: Context, context: AutomergeReplicatorContext): Promise<void> {
|
|
61
75
|
log('connecting...', { peerId: context.peerId, connectedSpaces: this._connectedSpaces.size });
|
|
62
76
|
this._context = context;
|
|
63
|
-
this._ctx =
|
|
77
|
+
this._ctx = ctx.derive();
|
|
64
78
|
this._ctx.onDispose(
|
|
65
79
|
this._edgeConnection.onReconnected(() => {
|
|
66
80
|
this._ctx && scheduleMicroTask(this._ctx, () => this._handleReconnect());
|
|
@@ -79,7 +93,7 @@ export class EchoEdgeReplicator implements EchoReplicator {
|
|
|
79
93
|
|
|
80
94
|
if (this._context !== null) {
|
|
81
95
|
for (const spaceId of spaces) {
|
|
82
|
-
await this._openConnection(spaceId);
|
|
96
|
+
await this._openConnection(this._ctx!, spaceId);
|
|
83
97
|
}
|
|
84
98
|
}
|
|
85
99
|
}
|
|
@@ -94,7 +108,9 @@ export class EchoEdgeReplicator implements EchoReplicator {
|
|
|
94
108
|
this._connections.clear();
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
@trace.span({ showInBrowserTimeline: true, op: 'replication' })
|
|
112
|
+
async connectToSpace(ctx: Context, spaceId: SpaceId): Promise<void> {
|
|
113
|
+
log('connectToSpace', { spaceId });
|
|
98
114
|
using _guard = await this._mutex.acquire();
|
|
99
115
|
|
|
100
116
|
if (this._connectedSpaces.has(spaceId)) {
|
|
@@ -102,9 +118,8 @@ export class EchoEdgeReplicator implements EchoReplicator {
|
|
|
102
118
|
}
|
|
103
119
|
this._connectedSpaces.add(spaceId);
|
|
104
120
|
|
|
105
|
-
// Check if AM-repo requested that we connect to remote peers.
|
|
106
121
|
if (this._context !== null) {
|
|
107
|
-
await this._openConnection(spaceId);
|
|
122
|
+
await this._openConnection(ctx, spaceId);
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
|
|
@@ -120,21 +135,25 @@ export class EchoEdgeReplicator implements EchoReplicator {
|
|
|
120
135
|
}
|
|
121
136
|
}
|
|
122
137
|
|
|
123
|
-
|
|
138
|
+
@trace.span({ showInBrowserTimeline: true, op: 'replication' })
|
|
139
|
+
private async _openConnection(ctx: Context, spaceId: SpaceId, reconnects: number = 0): Promise<void> {
|
|
124
140
|
invariant(this._context);
|
|
125
141
|
invariant(!this._connections.has(spaceId));
|
|
126
142
|
|
|
127
143
|
let restartScheduled = false;
|
|
128
144
|
|
|
129
145
|
const connection = new EdgeReplicatorConnection({
|
|
146
|
+
edgeHttpClient: this._edgeHttpClient,
|
|
130
147
|
edgeConnection: this._edgeConnection,
|
|
131
148
|
spaceId,
|
|
132
149
|
context: this._context,
|
|
133
150
|
sharedPolicyEnabled: this._sharePolicyEnabled,
|
|
134
151
|
onRemoteConnected: async () => {
|
|
152
|
+
log.trace('dxos.echo.edge.replicator.onRemoteConnected', { spaceId });
|
|
135
153
|
this._context?.onConnectionOpen(connection);
|
|
136
154
|
},
|
|
137
155
|
onRemoteDisconnected: async () => {
|
|
156
|
+
log.trace('dxos.echo.edge.replicator.onRemoteDisconnected', { spaceId });
|
|
138
157
|
this._context?.onConnectionClosed(connection);
|
|
139
158
|
},
|
|
140
159
|
onRestartRequested: async () => {
|
|
@@ -149,20 +168,21 @@ export class EchoEdgeReplicator implements EchoReplicator {
|
|
|
149
168
|
|
|
150
169
|
restartScheduled = true;
|
|
151
170
|
scheduleTask(
|
|
152
|
-
this._ctx
|
|
171
|
+
this._ctx!,
|
|
153
172
|
async () => {
|
|
154
173
|
using _guard = await this._mutex.acquire();
|
|
155
174
|
if (this._connections.get(spaceId) !== connection) {
|
|
156
175
|
return;
|
|
157
176
|
}
|
|
158
177
|
|
|
159
|
-
const
|
|
160
|
-
await connection.close();
|
|
178
|
+
const replicatorCtx = this._ctx;
|
|
179
|
+
await connection.close();
|
|
161
180
|
this._connections.delete(spaceId);
|
|
162
|
-
if (
|
|
181
|
+
if (replicatorCtx?.disposed) {
|
|
163
182
|
return;
|
|
164
183
|
}
|
|
165
|
-
|
|
184
|
+
log.trace('dxos.echo.edge.replicator.restart', { spaceId, reconnects, restartDelay });
|
|
185
|
+
await this._openConnection(replicatorCtx!, spaceId, reconnects + 1);
|
|
166
186
|
},
|
|
167
187
|
restartDelay,
|
|
168
188
|
);
|
|
@@ -170,14 +190,15 @@ export class EchoEdgeReplicator implements EchoReplicator {
|
|
|
170
190
|
});
|
|
171
191
|
this._connections.set(spaceId, connection);
|
|
172
192
|
|
|
173
|
-
await connection.open();
|
|
193
|
+
await connection.open(ctx);
|
|
174
194
|
}
|
|
175
195
|
}
|
|
176
196
|
|
|
177
|
-
type
|
|
197
|
+
type EdgeReplicatorConnectionsProps = {
|
|
178
198
|
edgeConnection: EdgeConnection;
|
|
199
|
+
edgeHttpClient: EdgeHttpClient;
|
|
179
200
|
spaceId: SpaceId;
|
|
180
|
-
context:
|
|
201
|
+
context: AutomergeReplicatorContext;
|
|
181
202
|
sharedPolicyEnabled: boolean;
|
|
182
203
|
onRemoteConnected: () => Promise<void>;
|
|
183
204
|
onRemoteDisconnected: () => Promise<void>;
|
|
@@ -187,16 +208,19 @@ type EdgeReplicatorConnectionsParams = {
|
|
|
187
208
|
const MAX_INFLIGHT_REQUESTS = 5;
|
|
188
209
|
const MAX_RATE_LIMIT_WAIT_TIME_MS = 3000;
|
|
189
210
|
|
|
190
|
-
class EdgeReplicatorConnection extends Resource implements
|
|
211
|
+
class EdgeReplicatorConnection extends Resource implements AutomergeReplicatorConnection {
|
|
212
|
+
private readonly _connectionId = randomUUID();
|
|
191
213
|
private readonly _edgeConnection: EdgeConnection;
|
|
214
|
+
private readonly _edgeHttpClient: EdgeHttpClient;
|
|
192
215
|
private readonly _remotePeerId: string | null = null;
|
|
193
216
|
private readonly _targetServiceId: string;
|
|
194
217
|
private readonly _spaceId: SpaceId;
|
|
195
|
-
private readonly _context:
|
|
218
|
+
private readonly _context: AutomergeReplicatorContext;
|
|
196
219
|
private readonly _sharedPolicyEnabled: boolean;
|
|
197
220
|
private readonly _onRemoteConnected: () => Promise<void>;
|
|
198
221
|
private readonly _onRemoteDisconnected: () => Promise<void>;
|
|
199
222
|
private readonly _onRestartRequested: () => void;
|
|
223
|
+
private _sequence = 0;
|
|
200
224
|
|
|
201
225
|
private _requestLimiter = new InflightRequestLimiter({
|
|
202
226
|
maxInflightRequests: MAX_INFLIGHT_REQUESTS,
|
|
@@ -210,23 +234,26 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
210
234
|
|
|
211
235
|
constructor({
|
|
212
236
|
edgeConnection,
|
|
237
|
+
edgeHttpClient,
|
|
213
238
|
spaceId,
|
|
214
239
|
context,
|
|
215
240
|
sharedPolicyEnabled,
|
|
216
241
|
onRemoteConnected,
|
|
217
242
|
onRemoteDisconnected,
|
|
218
243
|
onRestartRequested,
|
|
219
|
-
}:
|
|
244
|
+
}: EdgeReplicatorConnectionsProps) {
|
|
220
245
|
super();
|
|
221
246
|
this._edgeConnection = edgeConnection;
|
|
247
|
+
this._edgeHttpClient = edgeHttpClient;
|
|
222
248
|
this._spaceId = spaceId;
|
|
223
249
|
this._context = context;
|
|
224
250
|
// Generate a unique peer id for every connection.
|
|
225
251
|
// This way automerge-repo will have separate sync states for every connection.
|
|
226
252
|
// This is important because the previous connection might have had some messages that failed to deliver
|
|
227
253
|
// abd if we don't clear the sync-state, automerge will not attempt to deliver them again.
|
|
228
|
-
this.
|
|
229
|
-
|
|
254
|
+
this._targetServiceId = compositeKey(EdgeService.AUTOMERGE_REPLICATOR, spaceId);
|
|
255
|
+
// TODO(wittjosiah): Use compositeKey.
|
|
256
|
+
this._remotePeerId = `${this._targetServiceId}-${this._connectionId}`;
|
|
230
257
|
this._sharedPolicyEnabled = sharedPolicyEnabled;
|
|
231
258
|
this._onRemoteConnected = onRemoteConnected;
|
|
232
259
|
this._onRemoteDisconnected = onRemoteDisconnected;
|
|
@@ -242,7 +269,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
242
269
|
write: async (message: AutomergeProtocolMessage, controller) => {
|
|
243
270
|
await this._requestLimiter.rateLimit(message);
|
|
244
271
|
|
|
245
|
-
await this._sendMessage(message);
|
|
272
|
+
await this._sendMessage(this._ctx, message);
|
|
246
273
|
},
|
|
247
274
|
});
|
|
248
275
|
}
|
|
@@ -252,17 +279,30 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
252
279
|
|
|
253
280
|
await this._requestLimiter.open();
|
|
254
281
|
|
|
255
|
-
// TODO: handle reconnects
|
|
256
282
|
this._ctx.onDispose(
|
|
257
283
|
this._edgeConnection.onMessage((msg: RouterMessage) => {
|
|
258
284
|
this._onMessage(msg);
|
|
259
285
|
}),
|
|
260
286
|
);
|
|
261
287
|
|
|
288
|
+
let firstReconnect = true;
|
|
289
|
+
this._ctx.onDispose(
|
|
290
|
+
// NOTE: This will fire immediately if the connection is already open.
|
|
291
|
+
this._edgeConnection.onReconnected(async () => {
|
|
292
|
+
if (firstReconnect) {
|
|
293
|
+
log.verbose('first reconnect skipped');
|
|
294
|
+
firstReconnect = false;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this._onRestartRequested();
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
|
|
262
302
|
await this._onRemoteConnected();
|
|
263
303
|
}
|
|
264
304
|
|
|
265
|
-
protected override async _close(): Promise<void> {
|
|
305
|
+
protected override async _close(ctx: Context): Promise<void> {
|
|
266
306
|
log('closing...');
|
|
267
307
|
this._readableStreamController.close();
|
|
268
308
|
|
|
@@ -276,7 +316,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
276
316
|
return this._remotePeerId;
|
|
277
317
|
}
|
|
278
318
|
|
|
279
|
-
async shouldAdvertise(params:
|
|
319
|
+
async shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean> {
|
|
280
320
|
if (!this._sharedPolicyEnabled) {
|
|
281
321
|
return true;
|
|
282
322
|
}
|
|
@@ -301,7 +341,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
301
341
|
return spaceId === this._spaceId;
|
|
302
342
|
}
|
|
303
343
|
|
|
304
|
-
shouldSyncCollection(params:
|
|
344
|
+
shouldSyncCollection(params: ShouldSyncCollectionProps): boolean {
|
|
305
345
|
if (!this._sharedPolicyEnabled) {
|
|
306
346
|
return true;
|
|
307
347
|
}
|
|
@@ -317,8 +357,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
317
357
|
|
|
318
358
|
const payload = cbor.decode(message.payload!.value) as AutomergeProtocolMessage;
|
|
319
359
|
log.verbose('received', {
|
|
320
|
-
|
|
321
|
-
documentId: payload.type === 'sync' && payload.documentId,
|
|
360
|
+
...getMessageInfo(payload),
|
|
322
361
|
remoteId: this._remotePeerId,
|
|
323
362
|
});
|
|
324
363
|
|
|
@@ -327,11 +366,33 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
327
366
|
this._processMessage(payload);
|
|
328
367
|
}
|
|
329
368
|
|
|
369
|
+
get bundleSyncEnabled(): boolean {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async pushBundle(ctx: Context, bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]) {
|
|
374
|
+
const request: ImportBundleRequest = {
|
|
375
|
+
bundle: bundle.map(({ documentId, data, heads }) => ({
|
|
376
|
+
documentId,
|
|
377
|
+
mutation: DocumentCodec.encode(data),
|
|
378
|
+
heads,
|
|
379
|
+
})),
|
|
380
|
+
};
|
|
381
|
+
await this._edgeHttpClient.importBundle(ctx, this._spaceId, request);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async pullBundle(ctx: Context, docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>> {
|
|
385
|
+
const request: ExportBundleRequest = { docHeads };
|
|
386
|
+
const response = await this._edgeHttpClient.exportBundle(ctx, this._spaceId, request);
|
|
387
|
+
return Object.fromEntries(response.bundle.map((doc) => [doc.documentId, DocumentCodec.decode(doc.mutation)]));
|
|
388
|
+
}
|
|
389
|
+
|
|
330
390
|
private _processMessage(message: AutomergeProtocolMessage): void {
|
|
331
391
|
// There's a race between the credentials being replicated that are needed for access control and the data replication.
|
|
332
392
|
// AutomergeReplicator might return a Forbidden error if the credentials are not yet replicated.
|
|
333
393
|
// We restart the connection with some delay to account for that.
|
|
334
|
-
if (
|
|
394
|
+
if (isErrorMessage(message)) {
|
|
395
|
+
log.verbose('stream error', { error: (message as ErrorProtocolMessage).message });
|
|
335
396
|
this._onRestartRequested();
|
|
336
397
|
return;
|
|
337
398
|
}
|
|
@@ -341,33 +402,58 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
341
402
|
this._readableStreamController.enqueue(message);
|
|
342
403
|
}
|
|
343
404
|
|
|
344
|
-
private async _sendMessage(message: AutomergeProtocolMessage): Promise<void> {
|
|
405
|
+
private async _sendMessage(ctx: Context, message: AutomergeProtocolMessage): Promise<void> {
|
|
345
406
|
// Fix the peer id.
|
|
346
407
|
(message as any).targetId = this._targetServiceId as PeerId;
|
|
347
408
|
|
|
409
|
+
// Note: This is used on EDGE to detect out-of-order messages per connection.
|
|
410
|
+
setDeep(message, ['metadata', 'dxos_sequence'], this._getSequence());
|
|
411
|
+
setDeep(message, ['metadata', 'dxos_connectionId'], this._connectionId);
|
|
412
|
+
|
|
348
413
|
log.verbose('sending...', {
|
|
349
|
-
|
|
350
|
-
documentId: message.type === 'sync' && message.documentId,
|
|
414
|
+
...getMessageInfo(message),
|
|
351
415
|
remoteId: this._remotePeerId,
|
|
352
416
|
});
|
|
353
417
|
|
|
354
418
|
const encoded = cbor.encode(message);
|
|
355
419
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
420
|
+
try {
|
|
421
|
+
await this._edgeConnection.send(
|
|
422
|
+
ctx,
|
|
423
|
+
buf.create(RouterMessageSchema, {
|
|
424
|
+
serviceId: this._targetServiceId,
|
|
425
|
+
source: {
|
|
426
|
+
identityKey: this._edgeConnection.identityKey,
|
|
427
|
+
peerKey: this._edgeConnection.peerKey,
|
|
428
|
+
},
|
|
429
|
+
payload: { value: bufferToArray(encoded) },
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
log.error('failed to send message', { err });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private _getSequence(): number {
|
|
438
|
+
return this._sequence++;
|
|
366
439
|
}
|
|
367
440
|
}
|
|
368
441
|
|
|
369
442
|
/**
|
|
370
443
|
* This message is sent by EDGE AutomergeReplicator when the authorization is denied.
|
|
371
444
|
*/
|
|
372
|
-
const
|
|
373
|
-
|
|
445
|
+
const isErrorMessage = (message: AutomergeProtocolMessage) => message.type === 'error';
|
|
446
|
+
|
|
447
|
+
const getMessageInfo = (msg: AutomergeProtocolMessage) => {
|
|
448
|
+
const { have, heads, need, changes } = msg.type === 'sync' ? A.decodeSyncMessage(msg.data) : {};
|
|
449
|
+
return {
|
|
450
|
+
type: msg.type,
|
|
451
|
+
documentId: 'documentId' in msg ? msg.documentId : undefined,
|
|
452
|
+
collectionId: 'collectionId' in msg ? msg.collectionId : undefined,
|
|
453
|
+
have,
|
|
454
|
+
heads,
|
|
455
|
+
need,
|
|
456
|
+
changes: changes?.length,
|
|
457
|
+
sequence: msg.metadata?.dxos_sequence,
|
|
458
|
+
};
|
|
459
|
+
};
|