@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1c7ec43d41
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,158 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { cbor } from '@automerge/automerge-repo';
|
|
6
|
+
import { getRandomPort } from 'get-port-please';
|
|
7
|
+
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { Event } from '@dxos/async';
|
|
10
|
+
import { Context } from '@dxos/context';
|
|
11
|
+
import { EdgeClient, type EdgeHttpClient, MessageSchema, createEphemeralEdgeIdentity } from '@dxos/edge-client';
|
|
12
|
+
import { createTestEdgeWsServer } from '@dxos/edge-client/testing';
|
|
13
|
+
import { PublicKey, SpaceId } from '@dxos/keys';
|
|
14
|
+
import { EdgeService } from '@dxos/protocols';
|
|
15
|
+
import { createBuf } from '@dxos/protocols/buf';
|
|
16
|
+
import type { Peer } from '@dxos/protocols/proto/dxos/edge/messenger';
|
|
17
|
+
import { openAndClose } from '@dxos/test-utils';
|
|
18
|
+
import { compositeKey } from '@dxos/util';
|
|
19
|
+
|
|
20
|
+
import type { AutomergeReplicatorConnection, AutomergeReplicatorContext } from '../automerge';
|
|
21
|
+
import { EchoEdgeSubductionReplicator } from './echo-edge-subduction-replicator';
|
|
22
|
+
|
|
23
|
+
describe('EchoEdgeSubductionReplicator', () => {
|
|
24
|
+
test('opens a subduction connection when connectToSpace is called', async () => {
|
|
25
|
+
const { client } = await createClientServer();
|
|
26
|
+
|
|
27
|
+
const spaceId = SpaceId.random();
|
|
28
|
+
const { context, connectionOpen, openConnections } = createMockContext();
|
|
29
|
+
const replicator = await connectReplicator(client, context);
|
|
30
|
+
|
|
31
|
+
await replicator.connectToSpace(Context.default(), spaceId);
|
|
32
|
+
await connectionOpen.waitForCount(1);
|
|
33
|
+
|
|
34
|
+
expect(openConnections.length).toBe(1);
|
|
35
|
+
|
|
36
|
+
await replicator.disconnect();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('reconnects', async () => {
|
|
40
|
+
const { client, server } = await createClientServer();
|
|
41
|
+
|
|
42
|
+
const spaceId = SpaceId.random();
|
|
43
|
+
|
|
44
|
+
const { context, connectionOpen } = createMockContext();
|
|
45
|
+
const replicator = await connectReplicator(client, context);
|
|
46
|
+
await replicator.connectToSpace(Context.default(), spaceId);
|
|
47
|
+
|
|
48
|
+
client.setIdentity(await createEphemeralEdgeIdentity());
|
|
49
|
+
await connectionOpen.waitForCount(1);
|
|
50
|
+
|
|
51
|
+
// Subduction-era restart: edge emits a `subduction-reconnect` frame on the same
|
|
52
|
+
// SUBDUCTION_REPLICATOR service id; the connection tears down and the replicator
|
|
53
|
+
// opens a fresh connection on the next mutex pass.
|
|
54
|
+
const reconnectFrame = createSubductionReconnectMessage(
|
|
55
|
+
{ identityKey: client.identityKey, peerKey: client.peerKey },
|
|
56
|
+
spaceId,
|
|
57
|
+
);
|
|
58
|
+
await server.sendMessage(reconnectFrame);
|
|
59
|
+
await connectionOpen.waitForCount(1);
|
|
60
|
+
|
|
61
|
+
// Double restart to check for race conditions.
|
|
62
|
+
client.setIdentity(await createEphemeralEdgeIdentity());
|
|
63
|
+
await server.sendMessage(reconnectFrame);
|
|
64
|
+
await connectionOpen.waitForCount(1);
|
|
65
|
+
|
|
66
|
+
await replicator.disconnect();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('shouldAdvertise', () => {
|
|
70
|
+
test('true if space document belongs to connection space', async () => {
|
|
71
|
+
const { client } = await createClientServer();
|
|
72
|
+
|
|
73
|
+
const spaceId = SpaceId.random();
|
|
74
|
+
const documentId = PublicKey.random().toHex();
|
|
75
|
+
const { context, openConnections, connectionOpen } = createMockContext({
|
|
76
|
+
documentSpaceId: { [documentId]: spaceId },
|
|
77
|
+
});
|
|
78
|
+
const replicator = await connectReplicator(client, context);
|
|
79
|
+
await replicator.connectToSpace(Context.default(), spaceId);
|
|
80
|
+
|
|
81
|
+
await connectionOpen.waitForCount(1);
|
|
82
|
+
expect(openConnections.length).toBe(1);
|
|
83
|
+
expect(await openConnections[0].shouldAdvertise({ documentId })).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('checks remote collection if space id can not be resolved', async () => {
|
|
87
|
+
const { client } = await createClientServer();
|
|
88
|
+
|
|
89
|
+
const spaceId = SpaceId.random();
|
|
90
|
+
const documentId = PublicKey.random().toHex();
|
|
91
|
+
const remoteCollections: { [peerId: string]: { [documentId: string]: boolean } } = {};
|
|
92
|
+
const { context, openConnections, connectionOpen } = createMockContext({ remoteCollections });
|
|
93
|
+
const replicator = await connectReplicator(client, context);
|
|
94
|
+
await replicator.connectToSpace(Context.default(), spaceId);
|
|
95
|
+
|
|
96
|
+
await connectionOpen.waitForCount(1);
|
|
97
|
+
const connection = openConnections[0];
|
|
98
|
+
expect(await connection.shouldAdvertise({ documentId })).toBeFalsy();
|
|
99
|
+
remoteCollections[connection.peerId] = { [documentId]: true };
|
|
100
|
+
expect(await connection.shouldAdvertise({ documentId })).toBeTruthy();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const connectReplicator = async (client: EdgeClient, context: AutomergeReplicatorContext) => {
|
|
105
|
+
// EdgeHttpClient functionality is not used by the subduction replicator.
|
|
106
|
+
const replicator = new EchoEdgeSubductionReplicator({
|
|
107
|
+
edgeConnection: client,
|
|
108
|
+
edgeHttpClient: {} as EdgeHttpClient,
|
|
109
|
+
});
|
|
110
|
+
await replicator.connect(Context.default(), context);
|
|
111
|
+
onTestFinished(() => replicator.disconnect());
|
|
112
|
+
return replicator;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const createClientServer = async () => {
|
|
116
|
+
const server = await createTestEdgeWsServer(await getRandomPort());
|
|
117
|
+
onTestFinished(server.cleanup);
|
|
118
|
+
const client = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: server.endpoint });
|
|
119
|
+
await openAndClose(client);
|
|
120
|
+
return { client, server };
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const createMockContext = (args?: {
|
|
125
|
+
remoteCollections?: { [peerId: string]: { [documentId: string]: boolean } };
|
|
126
|
+
documentSpaceId?: { [documentId: string]: SpaceId };
|
|
127
|
+
}) => {
|
|
128
|
+
const connectionOpen = new Event();
|
|
129
|
+
const openConnections: AutomergeReplicatorConnection[] = [];
|
|
130
|
+
const context: AutomergeReplicatorContext = {
|
|
131
|
+
getContainingSpaceIdForDocument: async (documentId) => args?.documentSpaceId?.[documentId] ?? null,
|
|
132
|
+
getContainingSpaceForDocument: async () => null,
|
|
133
|
+
isDocumentInRemoteCollection: async (params) =>
|
|
134
|
+
args?.remoteCollections?.[params.peerId]?.[params.documentId] ?? false,
|
|
135
|
+
onConnectionAuthScopeChanged: () => {},
|
|
136
|
+
onConnectionClosed: (connection) => {
|
|
137
|
+
const idx = openConnections.indexOf(connection);
|
|
138
|
+
if (idx >= 0) {
|
|
139
|
+
openConnections.splice(idx, 1);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
onConnectionOpen: (connection) => {
|
|
143
|
+
openConnections.push(connection);
|
|
144
|
+
connectionOpen.emit();
|
|
145
|
+
},
|
|
146
|
+
peerId: PublicKey.random().toHex(),
|
|
147
|
+
};
|
|
148
|
+
return { context, openConnections, connectionOpen };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const createSubductionReconnectMessage = (target: Peer, spaceId: SpaceId) =>
|
|
152
|
+
createBuf(MessageSchema, {
|
|
153
|
+
target: [target],
|
|
154
|
+
serviceId: compositeKey(EdgeService.SUBDUCTION_REPLICATOR, spaceId),
|
|
155
|
+
payload: {
|
|
156
|
+
value: cbor.encode({ type: 'subduction-reconnect' }),
|
|
157
|
+
},
|
|
158
|
+
});
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { cbor } from '@automerge/automerge-repo';
|
|
6
|
+
|
|
7
|
+
import { Mutex, scheduleMicroTask, scheduleTask } from '@dxos/async';
|
|
8
|
+
import { Context, Resource } from '@dxos/context';
|
|
9
|
+
import { randomUUID } from '@dxos/crypto';
|
|
10
|
+
import type { CollectionId } from '@dxos/echo-protocol';
|
|
11
|
+
import { type EdgeConnection, type EdgeHttpClient } from '@dxos/edge-client';
|
|
12
|
+
import { invariant } from '@dxos/invariant';
|
|
13
|
+
import type { SpaceId } from '@dxos/keys';
|
|
14
|
+
import { log } from '@dxos/log';
|
|
15
|
+
import { type AutomergeProtocolMessage, EdgeService, type PeerId } from '@dxos/protocols';
|
|
16
|
+
import { buf } from '@dxos/protocols/buf';
|
|
17
|
+
import {
|
|
18
|
+
type Message as RouterMessage,
|
|
19
|
+
MessageSchema as RouterMessageSchema,
|
|
20
|
+
} from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
21
|
+
import { trace } from '@dxos/tracing';
|
|
22
|
+
import { bufferToArray, compositeKey } from '@dxos/util';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
type AutomergeReplicator,
|
|
26
|
+
type AutomergeReplicatorConnection,
|
|
27
|
+
type AutomergeReplicatorContext,
|
|
28
|
+
type ShouldAdvertiseProps,
|
|
29
|
+
type ShouldSyncCollectionProps,
|
|
30
|
+
getSpaceIdFromCollectionId,
|
|
31
|
+
} from '../automerge';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Delay before restarting the connection after the edge requests it.
|
|
35
|
+
*/
|
|
36
|
+
const INITIAL_RESTART_DELAY = 500;
|
|
37
|
+
const RESTART_DELAY_JITTER = 250;
|
|
38
|
+
const MAX_RESTART_DELAY = 5000;
|
|
39
|
+
|
|
40
|
+
export type EchoEdgeSubductionReplicatorProps = {
|
|
41
|
+
edgeConnection: EdgeConnection;
|
|
42
|
+
edgeHttpClient: EdgeHttpClient;
|
|
43
|
+
disableSharePolicy?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Edge replicator for the Subduction transport.
|
|
48
|
+
*
|
|
49
|
+
* Subduction byte tunnel over the Edge router WebSocket. Every inbound router message
|
|
50
|
+
* targeting the subduction service is forwarded verbatim to the local Repo via an
|
|
51
|
+
* {@link AutomergeReplicatorConnection}. Outbound repo messages are wrapped in a router
|
|
52
|
+
* frame and sent to the edge.
|
|
53
|
+
*
|
|
54
|
+
* No classical automerge-repo sync, collection-query/state, bundle sync, or rate-limiting
|
|
55
|
+
* runs through this class — Subduction's sedimentree protocol replaces those
|
|
56
|
+
* responsibilities. For the classical sync path see {@link EchoEdgeReplicator}.
|
|
57
|
+
*/
|
|
58
|
+
@trace.resource()
|
|
59
|
+
export class EchoEdgeSubductionReplicator implements AutomergeReplicator {
|
|
60
|
+
private readonly _edgeConnection: EdgeConnection;
|
|
61
|
+
private readonly _edgeHttpClient: EdgeHttpClient;
|
|
62
|
+
private readonly _mutex = new Mutex();
|
|
63
|
+
private readonly _disableSharePolicy: boolean;
|
|
64
|
+
|
|
65
|
+
private _ctx?: Context = undefined;
|
|
66
|
+
private _context: AutomergeReplicatorContext | null = null;
|
|
67
|
+
private _connectedSpaces = new Set<SpaceId>();
|
|
68
|
+
private _connections = new Map<SpaceId, EdgeSubductionReplicatorConnection>();
|
|
69
|
+
|
|
70
|
+
constructor({ edgeConnection, edgeHttpClient, disableSharePolicy }: EchoEdgeSubductionReplicatorProps) {
|
|
71
|
+
this._edgeConnection = edgeConnection;
|
|
72
|
+
this._edgeHttpClient = edgeHttpClient;
|
|
73
|
+
this._disableSharePolicy = disableSharePolicy ?? false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async connect(ctx: Context, context: AutomergeReplicatorContext): Promise<void> {
|
|
77
|
+
log('connecting...', { peerId: context.peerId, connectedSpaces: this._connectedSpaces.size });
|
|
78
|
+
this._context = context;
|
|
79
|
+
this._ctx = ctx.derive();
|
|
80
|
+
this._ctx.onDispose(
|
|
81
|
+
this._edgeConnection.onReconnected(() => {
|
|
82
|
+
this._ctx && scheduleMicroTask(this._ctx, () => this._handleReconnect());
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async _handleReconnect(): Promise<void> {
|
|
88
|
+
using _guard = await this._mutex.acquire();
|
|
89
|
+
|
|
90
|
+
const spaces = [...this._connectedSpaces];
|
|
91
|
+
for (const connection of this._connections.values()) {
|
|
92
|
+
await connection.close();
|
|
93
|
+
}
|
|
94
|
+
this._connections.clear();
|
|
95
|
+
|
|
96
|
+
if (this._context !== null) {
|
|
97
|
+
for (const spaceId of spaces) {
|
|
98
|
+
await this._openConnection(spaceId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async disconnect(): Promise<void> {
|
|
104
|
+
using _guard = await this._mutex.acquire();
|
|
105
|
+
await this._ctx?.dispose();
|
|
106
|
+
|
|
107
|
+
for (const connection of this._connections.values()) {
|
|
108
|
+
await connection.close();
|
|
109
|
+
}
|
|
110
|
+
this._connections.clear();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@trace.span({ showInBrowserTimeline: true })
|
|
114
|
+
async connectToSpace(ctx: Context, spaceId: SpaceId): Promise<void> {
|
|
115
|
+
log('connectToSpace', { spaceId });
|
|
116
|
+
using _guard = await this._mutex.acquire();
|
|
117
|
+
|
|
118
|
+
if (this._connectedSpaces.has(spaceId)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this._connectedSpaces.add(spaceId);
|
|
122
|
+
|
|
123
|
+
if (this._context !== null) {
|
|
124
|
+
await this._openConnection(spaceId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async disconnectFromSpace(spaceId: SpaceId): Promise<void> {
|
|
129
|
+
using _guard = await this._mutex.acquire();
|
|
130
|
+
|
|
131
|
+
this._connectedSpaces.delete(spaceId);
|
|
132
|
+
|
|
133
|
+
const connection = this._connections.get(spaceId);
|
|
134
|
+
if (connection) {
|
|
135
|
+
await connection.close();
|
|
136
|
+
this._connections.delete(spaceId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async _openConnection(spaceId: SpaceId, reconnects: number = 0): Promise<void> {
|
|
141
|
+
invariant(this._context);
|
|
142
|
+
invariant(!this._connections.has(spaceId));
|
|
143
|
+
|
|
144
|
+
let restartScheduled = false;
|
|
145
|
+
|
|
146
|
+
const connection = new EdgeSubductionReplicatorConnection({
|
|
147
|
+
edgeConnection: this._edgeConnection,
|
|
148
|
+
spaceId,
|
|
149
|
+
context: this._context,
|
|
150
|
+
sharedPolicyEnabled: !this._disableSharePolicy,
|
|
151
|
+
onRemoteConnected: async () => {
|
|
152
|
+
log.trace('dxos.echo.edge.subduction-replicator.onRemoteConnected', { spaceId });
|
|
153
|
+
this._context?.onConnectionOpen(connection);
|
|
154
|
+
},
|
|
155
|
+
onRemoteDisconnected: async () => {
|
|
156
|
+
log.trace('dxos.echo.edge.subduction-replicator.onRemoteDisconnected', { spaceId });
|
|
157
|
+
this._context?.onConnectionClosed(connection);
|
|
158
|
+
},
|
|
159
|
+
onRestartRequested: async () => {
|
|
160
|
+
if (!this._ctx || restartScheduled) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const restartDelay =
|
|
165
|
+
Math.min(MAX_RESTART_DELAY, INITIAL_RESTART_DELAY * reconnects) + Math.random() * RESTART_DELAY_JITTER;
|
|
166
|
+
|
|
167
|
+
log('connection restart scheduled', { spaceId, reconnects, restartDelay });
|
|
168
|
+
|
|
169
|
+
restartScheduled = true;
|
|
170
|
+
scheduleTask(
|
|
171
|
+
this._ctx!,
|
|
172
|
+
async () => {
|
|
173
|
+
using _guard = await this._mutex.acquire();
|
|
174
|
+
if (this._connections.get(spaceId) !== connection) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ctx = this._ctx;
|
|
179
|
+
await connection.close();
|
|
180
|
+
this._connections.delete(spaceId);
|
|
181
|
+
if (ctx?.disposed) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
log.trace('dxos.echo.edge.subduction-replicator.restart', { spaceId, reconnects, restartDelay });
|
|
185
|
+
await this._openConnection(spaceId, reconnects + 1);
|
|
186
|
+
},
|
|
187
|
+
restartDelay,
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
this._connections.set(spaceId, connection);
|
|
192
|
+
|
|
193
|
+
await connection.open();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
type EdgeSubductionReplicatorConnectionProps = {
|
|
198
|
+
edgeConnection: EdgeConnection;
|
|
199
|
+
spaceId: SpaceId;
|
|
200
|
+
context: AutomergeReplicatorContext;
|
|
201
|
+
sharedPolicyEnabled: boolean;
|
|
202
|
+
onRemoteConnected: () => Promise<void>;
|
|
203
|
+
onRemoteDisconnected: () => Promise<void>;
|
|
204
|
+
onRestartRequested: () => Promise<void>;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
class EdgeSubductionReplicatorConnection extends Resource implements AutomergeReplicatorConnection {
|
|
208
|
+
private readonly _connectionId = randomUUID();
|
|
209
|
+
private readonly _edgeConnection: EdgeConnection;
|
|
210
|
+
private readonly _remotePeerId: string;
|
|
211
|
+
private readonly _subductionServiceId: string;
|
|
212
|
+
private readonly _spaceId: SpaceId;
|
|
213
|
+
private readonly _context: AutomergeReplicatorContext;
|
|
214
|
+
private readonly _sharedPolicyEnabled: boolean;
|
|
215
|
+
private readonly _onRemoteConnected: () => Promise<void>;
|
|
216
|
+
private readonly _onRemoteDisconnected: () => Promise<void>;
|
|
217
|
+
private readonly _onRestartRequested: () => void;
|
|
218
|
+
|
|
219
|
+
private _readableStreamController!: ReadableStreamDefaultController<AutomergeProtocolMessage>;
|
|
220
|
+
|
|
221
|
+
public readable: ReadableStream<AutomergeProtocolMessage>;
|
|
222
|
+
public writable: WritableStream<AutomergeProtocolMessage>;
|
|
223
|
+
|
|
224
|
+
constructor({
|
|
225
|
+
edgeConnection,
|
|
226
|
+
spaceId,
|
|
227
|
+
context,
|
|
228
|
+
sharedPolicyEnabled,
|
|
229
|
+
onRemoteConnected,
|
|
230
|
+
onRemoteDisconnected,
|
|
231
|
+
onRestartRequested,
|
|
232
|
+
}: EdgeSubductionReplicatorConnectionProps) {
|
|
233
|
+
super();
|
|
234
|
+
this._edgeConnection = edgeConnection;
|
|
235
|
+
this._spaceId = spaceId;
|
|
236
|
+
this._context = context;
|
|
237
|
+
this._sharedPolicyEnabled = sharedPolicyEnabled;
|
|
238
|
+
// Generate a unique peer id for every connection so sync-state is fresh on reconnect.
|
|
239
|
+
this._subductionServiceId = compositeKey(EdgeService.SUBDUCTION_REPLICATOR, spaceId);
|
|
240
|
+
this._remotePeerId = `${this._subductionServiceId}-${this._connectionId}`;
|
|
241
|
+
this._onRemoteConnected = onRemoteConnected;
|
|
242
|
+
this._onRemoteDisconnected = onRemoteDisconnected;
|
|
243
|
+
this._onRestartRequested = onRestartRequested;
|
|
244
|
+
|
|
245
|
+
this.readable = new ReadableStream<AutomergeProtocolMessage>({
|
|
246
|
+
start: (controller) => {
|
|
247
|
+
this._readableStreamController = controller;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
this.writable = new WritableStream<AutomergeProtocolMessage>({
|
|
252
|
+
write: async (message: AutomergeProtocolMessage) => {
|
|
253
|
+
await this._sendMessage(this._ctx, message);
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
protected override async _open(ctx: Context): Promise<void> {
|
|
259
|
+
log('opening...');
|
|
260
|
+
|
|
261
|
+
this._ctx.onDispose(
|
|
262
|
+
this._edgeConnection.onMessage((msg: RouterMessage) => {
|
|
263
|
+
this._onMessage(msg);
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
let firstReconnect = true;
|
|
268
|
+
this._ctx.onDispose(
|
|
269
|
+
this._edgeConnection.onReconnected(async () => {
|
|
270
|
+
if (firstReconnect) {
|
|
271
|
+
log.verbose('first reconnect skipped');
|
|
272
|
+
firstReconnect = false;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this._onRestartRequested();
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await this._onRemoteConnected();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
protected override async _close(ctx: Context): Promise<void> {
|
|
284
|
+
log('closing...');
|
|
285
|
+
this._readableStreamController.close();
|
|
286
|
+
await this._onRemoteDisconnected();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
get peerId(): string {
|
|
290
|
+
return this._remotePeerId;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
get bundleSyncEnabled(): boolean {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean> {
|
|
298
|
+
if (!this._sharedPolicyEnabled) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
const spaceId = await this._context.getContainingSpaceIdForDocument(params.documentId);
|
|
302
|
+
if (!spaceId) {
|
|
303
|
+
const remoteDocumentExists = await this._context.isDocumentInRemoteCollection({
|
|
304
|
+
documentId: params.documentId,
|
|
305
|
+
peerId: this._remotePeerId as PeerId,
|
|
306
|
+
});
|
|
307
|
+
log.verbose('document not found locally for share policy check', {
|
|
308
|
+
documentId: params.documentId,
|
|
309
|
+
acceptDocument: remoteDocumentExists,
|
|
310
|
+
remoteId: this._remotePeerId,
|
|
311
|
+
});
|
|
312
|
+
// If a document is not present locally return true only if it already exists on edge.
|
|
313
|
+
return remoteDocumentExists;
|
|
314
|
+
}
|
|
315
|
+
return spaceId === this._spaceId;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
shouldSyncCollection(params: ShouldSyncCollectionProps): boolean {
|
|
319
|
+
if (!this._sharedPolicyEnabled) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
const spaceId = getSpaceIdFromCollectionId(params.collectionId as CollectionId);
|
|
323
|
+
// Only sync collections of form `space:id:rootDoc`; edge ignores legacy `space:id` collections.
|
|
324
|
+
return spaceId === this._spaceId && params.collectionId.split(':').length === 3;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private _onMessage(message: RouterMessage): void {
|
|
328
|
+
if (message.serviceId !== this._subductionServiceId) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const payload = cbor.decode(message.payload!.value) as any;
|
|
333
|
+
|
|
334
|
+
// Out-of-band reconnect signal from the edge (sent after DO hibernation).
|
|
335
|
+
if (payload?.type === 'subduction-reconnect') {
|
|
336
|
+
log.info('received subduction-reconnect signal');
|
|
337
|
+
this._onRestartRequested();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
log.verbose('received subduction frame', { remoteId: this._remotePeerId });
|
|
342
|
+
|
|
343
|
+
// Fix the peer id so subduction routing inside the Repo accepts the frame.
|
|
344
|
+
const msg = payload as AutomergeProtocolMessage;
|
|
345
|
+
msg.senderId = this._remotePeerId as PeerId;
|
|
346
|
+
this._readableStreamController.enqueue(msg);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private async _sendMessage(ctx: Context, message: AutomergeProtocolMessage): Promise<void> {
|
|
350
|
+
// Fix the peer id for the outbound frame.
|
|
351
|
+
(message as any).targetId = this._subductionServiceId as PeerId;
|
|
352
|
+
|
|
353
|
+
log.verbose('sending...', {
|
|
354
|
+
type: message.type,
|
|
355
|
+
serviceId: this._subductionServiceId,
|
|
356
|
+
remoteId: this._remotePeerId,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const encoded = cbor.encode(message);
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
await this._edgeConnection.send(
|
|
363
|
+
ctx,
|
|
364
|
+
buf.create(RouterMessageSchema, {
|
|
365
|
+
serviceId: this._subductionServiceId,
|
|
366
|
+
source: {
|
|
367
|
+
identityKey: this._edgeConnection.identityKey,
|
|
368
|
+
peerKey: this._edgeConnection.peerKey,
|
|
369
|
+
},
|
|
370
|
+
payload: { value: bufferToArray(encoded) },
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
log.error('failed to send message', { err });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
package/src/edge/index.ts
CHANGED