@dxos/echo-pipeline 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc

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 (221) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/{browser/chunk-WJJXJTNS.mjs → neutral/chunk-46DCRDA7.mjs} +44 -13
  4. package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
  5. package/dist/lib/{browser/chunk-FR6GJ4S4.mjs → neutral/chunk-LR7UFVQX.mjs} +145 -618
  6. package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
  7. package/dist/lib/{browser → neutral}/filter/index.mjs +1 -2
  8. package/dist/lib/neutral/index.mjs +7332 -0
  9. package/dist/lib/neutral/index.mjs.map +7 -0
  10. package/dist/lib/neutral/meta.json +1 -0
  11. package/dist/lib/{browser → neutral}/testing/index.mjs +49 -292
  12. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  13. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
  14. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
  15. package/dist/types/src/automerge/automerge-host.d.ts +105 -27
  16. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  17. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
  18. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
  19. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  20. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  21. package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
  22. package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
  23. package/dist/types/src/automerge/collection-synchronizer.d.ts +22 -2
  24. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  25. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-network-adapter.d.ts +17 -11
  27. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  28. package/dist/types/src/automerge/echo-replicator.d.ts +28 -16
  29. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  30. package/dist/types/src/automerge/errors.d.ts +35 -0
  31. package/dist/types/src/automerge/errors.d.ts.map +1 -0
  32. package/dist/types/src/automerge/handle-state.d.ts +52 -0
  33. package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
  34. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  35. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  36. package/dist/types/src/automerge/index.d.ts +2 -0
  37. package/dist/types/src/automerge/index.d.ts.map +1 -1
  38. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +10 -4
  39. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  40. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +8 -8
  41. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  42. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
  43. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  44. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  45. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  46. package/dist/types/src/common/feeds.d.ts.map +1 -1
  47. package/dist/types/src/common/space-id.d.ts.map +1 -1
  48. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  49. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  50. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  51. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  52. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  53. package/dist/types/src/db-host/data-service.d.ts +12 -6
  54. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  55. package/dist/types/src/db-host/database-root.d.ts +9 -3
  56. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  57. package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
  58. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  59. package/dist/types/src/db-host/echo-host.d.ts +74 -28
  60. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  61. package/dist/types/src/db-host/index.d.ts +2 -0
  62. package/dist/types/src/db-host/index.d.ts.map +1 -1
  63. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  64. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  65. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  66. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  67. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  68. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  69. package/dist/types/src/db-host/query-service.d.ts +18 -10
  70. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  71. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  72. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  73. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  74. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  75. package/dist/types/src/db-host/space-state-manager.d.ts +3 -2
  76. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  77. package/dist/types/src/db-host/stub.d.ts +11 -0
  78. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  79. package/dist/types/src/edge/echo-edge-replicator.d.ts +7 -6
  80. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  81. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  82. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  83. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  84. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  85. package/dist/types/src/edge/index.d.ts +1 -0
  86. package/dist/types/src/edge/index.d.ts.map +1 -1
  87. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  88. package/dist/types/src/filter/filter-match.d.ts +1 -1
  89. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  90. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  91. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  92. package/dist/types/src/pipeline/pipeline.d.ts +2 -2
  93. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  94. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  95. package/dist/types/src/query/errors.d.ts +16 -20
  96. package/dist/types/src/query/errors.d.ts.map +1 -1
  97. package/dist/types/src/query/plan.d.ts +59 -8
  98. package/dist/types/src/query/plan.d.ts.map +1 -1
  99. package/dist/types/src/query/query-executor.d.ts +55 -12
  100. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  101. package/dist/types/src/query/query-planner.d.ts +10 -1
  102. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  103. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  104. package/dist/types/src/space/auth.d.ts +3 -3
  105. package/dist/types/src/space/auth.d.ts.map +1 -1
  106. package/dist/types/src/space/control-pipeline.d.ts +4 -3
  107. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  108. package/dist/types/src/space/space-manager.d.ts +7 -7
  109. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  110. package/dist/types/src/space/space-protocol.d.ts +9 -8
  111. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  112. package/dist/types/src/space/space.d.ts +5 -5
  113. package/dist/types/src/space/space.d.ts.map +1 -1
  114. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  115. package/dist/types/src/testing/index.d.ts +0 -2
  116. package/dist/types/src/testing/index.d.ts.map +1 -1
  117. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  118. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
  119. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  120. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  121. package/dist/types/src/testing/test-replicator.d.ts +13 -13
  122. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  123. package/dist/types/src/util.d.ts.map +1 -1
  124. package/dist/types/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +56 -54
  126. package/src/automerge/automerge-host-subduction.test.ts +263 -0
  127. package/src/automerge/automerge-host.test.ts +145 -32
  128. package/src/automerge/automerge-host.ts +442 -163
  129. package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
  130. package/src/automerge/automerge-repo.test.ts +284 -77
  131. package/src/automerge/automerge-subduction.test.ts +330 -0
  132. package/src/automerge/collection-synchronizer.test.ts +96 -16
  133. package/src/automerge/collection-synchronizer.ts +125 -26
  134. package/src/automerge/echo-network-adapter.test.ts +2 -1
  135. package/src/automerge/echo-network-adapter.ts +44 -32
  136. package/src/automerge/echo-replicator.ts +30 -16
  137. package/src/automerge/errors.ts +10 -0
  138. package/src/automerge/handle-state.ts +62 -0
  139. package/src/automerge/heads-store.ts +42 -3
  140. package/src/automerge/index.ts +2 -0
  141. package/src/automerge/leveldb-storage-adapter.ts +70 -4
  142. package/src/automerge/mesh-echo-replicator-connection.ts +8 -8
  143. package/src/automerge/mesh-echo-replicator.ts +14 -11
  144. package/src/db-host/automerge-data-source.test.ts +299 -0
  145. package/src/db-host/automerge-data-source.ts +187 -0
  146. package/src/db-host/data-service.ts +27 -10
  147. package/src/db-host/database-root.ts +19 -7
  148. package/src/db-host/documents-synchronizer.test.ts +145 -14
  149. package/src/db-host/documents-synchronizer.ts +158 -59
  150. package/src/db-host/echo-host.ts +376 -90
  151. package/src/db-host/index.ts +2 -0
  152. package/src/db-host/invalidation-hint.ts +50 -0
  153. package/src/db-host/local-queue-service.ts +127 -0
  154. package/src/db-host/query-invalidation.test.ts +270 -0
  155. package/src/db-host/query-service.ts +108 -111
  156. package/src/db-host/queue-data-source.ts +160 -0
  157. package/src/db-host/queue-service.test.ts +145 -0
  158. package/src/db-host/space-state-manager.ts +11 -9
  159. package/src/db-host/stub.ts +33 -0
  160. package/src/edge/echo-edge-replicator.test.ts +9 -9
  161. package/src/edge/echo-edge-replicator.ts +50 -42
  162. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  163. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  164. package/src/edge/index.ts +1 -0
  165. package/src/filter/filter-match.test.ts +41 -25
  166. package/src/filter/filter-match.ts +75 -20
  167. package/src/metadata/metadata-store.ts +8 -5
  168. package/src/pipeline/pipeline-stress.test.ts +1 -3
  169. package/src/pipeline/pipeline.test.ts +0 -1
  170. package/src/pipeline/pipeline.ts +2 -3
  171. package/src/query/errors.ts +2 -2
  172. package/src/query/plan.ts +73 -9
  173. package/src/query/query-executor.ts +1064 -172
  174. package/src/query/query-planner.test.ts +854 -86
  175. package/src/query/query-planner.ts +444 -38
  176. package/src/space/admission-discovery-extension.ts +1 -1
  177. package/src/space/auth.ts +6 -6
  178. package/src/space/control-pipeline.test.ts +2 -2
  179. package/src/space/control-pipeline.ts +7 -7
  180. package/src/space/space-manager.ts +12 -17
  181. package/src/space/space-protocol.browser.test.ts +13 -12
  182. package/src/space/space-protocol.test.ts +13 -13
  183. package/src/space/space-protocol.ts +16 -15
  184. package/src/space/space.test.ts +2 -2
  185. package/src/space/space.ts +9 -10
  186. package/src/testing/index.ts +0 -2
  187. package/src/testing/test-agent-builder.ts +0 -1
  188. package/src/testing/test-network-adapter.ts +24 -3
  189. package/src/testing/test-replicator.ts +15 -20
  190. package/src/util.ts +7 -3
  191. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  192. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  193. package/dist/lib/browser/chunk-FR6GJ4S4.mjs.map +0 -7
  194. package/dist/lib/browser/chunk-WJJXJTNS.mjs.map +0 -7
  195. package/dist/lib/browser/index.mjs +0 -5492
  196. package/dist/lib/browser/index.mjs.map +0 -7
  197. package/dist/lib/browser/meta.json +0 -1
  198. package/dist/lib/browser/testing/index.mjs.map +0 -7
  199. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  200. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  201. package/dist/lib/node-esm/chunk-JQCCVDLT.mjs +0 -2144
  202. package/dist/lib/node-esm/chunk-JQCCVDLT.mjs.map +0 -7
  203. package/dist/lib/node-esm/chunk-LEQ77KAT.mjs +0 -240
  204. package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
  205. package/dist/lib/node-esm/filter/index.mjs +0 -13
  206. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  207. package/dist/lib/node-esm/index.mjs +0 -5492
  208. package/dist/lib/node-esm/index.mjs.map +0 -7
  209. package/dist/lib/node-esm/meta.json +0 -1
  210. package/dist/lib/node-esm/testing/index.mjs +0 -764
  211. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  212. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  213. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  214. package/dist/types/src/testing/test-data.d.ts +0 -18
  215. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  216. package/dist/types/src/testing/test-schema.d.ts +0 -37
  217. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  218. package/src/db-host/documents-iterator.ts +0 -74
  219. package/src/testing/test-data.ts +0 -127
  220. package/src/testing/test-schema.ts +0 -53
  221. /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,6 +7,7 @@ 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 { Context } from '@dxos/context';
10
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';
@@ -16,8 +17,7 @@ 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
 
19
- import type { EchoReplicatorContext, ReplicatorConnection } from '../automerge';
20
-
20
+ import type { AutomergeReplicatorConnection, AutomergeReplicatorContext } from '../automerge';
21
21
  import { EchoEdgeReplicator } from './echo-edge-replicator';
22
22
 
23
23
  describe('EchoEdgeReplicator', () => {
@@ -28,7 +28,7 @@ describe('EchoEdgeReplicator', () => {
28
28
 
29
29
  const { context, connectionOpen } = createMockContext();
30
30
  const replicator = await connectReplicator(client, context);
31
- await replicator.connectToSpace(spaceId);
31
+ await replicator.connectToSpace(Context.default(), spaceId);
32
32
 
33
33
  client.setIdentity(await createEphemeralEdgeIdentity());
34
34
  await connectionOpen.waitForCount(1);
@@ -55,7 +55,7 @@ describe('EchoEdgeReplicator', () => {
55
55
  documentSpaceId: { [documentId]: spaceId },
56
56
  });
57
57
  const replicator = await connectReplicator(client, context);
58
- await replicator.connectToSpace(spaceId);
58
+ await replicator.connectToSpace(Context.default(), spaceId);
59
59
 
60
60
  await expect.poll(() => openConnections.length === 1).toBeTruthy();
61
61
  expect(openConnections[0].shouldAdvertise({ documentId })).toBeTruthy();
@@ -69,7 +69,7 @@ describe('EchoEdgeReplicator', () => {
69
69
  const remoteCollections: { [peerId: string]: { [documentId: string]: boolean } } = {};
70
70
  const { context, openConnections } = createMockContext({ remoteCollections });
71
71
  const replicator = await connectReplicator(client, context);
72
- await replicator.connectToSpace(spaceId);
72
+ await replicator.connectToSpace(Context.default(), spaceId);
73
73
 
74
74
  await expect.poll(() => openConnections.length === 1).toBeTruthy();
75
75
  const connection = openConnections[0];
@@ -79,10 +79,10 @@ describe('EchoEdgeReplicator', () => {
79
79
  });
80
80
  });
81
81
 
82
- const connectReplicator = async (client: EdgeClient, context: EchoReplicatorContext) => {
82
+ const connectReplicator = async (client: EdgeClient, context: AutomergeReplicatorContext) => {
83
83
  // EdgeHttpClient functionality is not tested here.
84
84
  const replicator = new EchoEdgeReplicator({ edgeConnection: client, edgeHttpClient: {} as EdgeHttpClient });
85
- await replicator.connect(context);
85
+ await replicator.connect(Context.default(), context);
86
86
  onTestFinished(() => replicator.disconnect());
87
87
  return replicator;
88
88
  };
@@ -101,7 +101,7 @@ const createMockContext = (args?: {
101
101
  documentSpaceId?: { [documentId: string]: SpaceId };
102
102
  }) => {
103
103
  const connectionOpen = new Event();
104
- const openConnections: ReplicatorConnection[] = [];
104
+ const openConnections: AutomergeReplicatorConnection[] = [];
105
105
  return {
106
106
  context: {
107
107
  getContainingSpaceIdForDocument: async (documentId) => args?.documentSpaceId?.[documentId] ?? null,
@@ -121,7 +121,7 @@ const createMockContext = (args?: {
121
121
  },
122
122
 
123
123
  peerId: PublicKey.random().toHex(),
124
- } satisfies EchoReplicatorContext,
124
+ } satisfies AutomergeReplicatorContext,
125
125
 
126
126
  openConnections,
127
127
  connectionOpen,
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import * as Automerge from '@automerge/automerge';
5
+ import * as A from '@automerge/automerge';
6
6
  import { type DocumentId, type Heads, cbor } from '@automerge/automerge-repo';
7
7
 
8
8
  import { Mutex, scheduleMicroTask, scheduleTask } from '@dxos/async';
@@ -17,6 +17,7 @@ import {
17
17
  type AutomergeProtocolMessage,
18
18
  DocumentCodec,
19
19
  EdgeService,
20
+ type ErrorProtocolMessage,
20
21
  type ExportBundleRequest,
21
22
  type ImportBundleRequest,
22
23
  type PeerId,
@@ -26,17 +27,17 @@ import {
26
27
  type Message as RouterMessage,
27
28
  MessageSchema as RouterMessageSchema,
28
29
  } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
29
- import { bufferToArray, setDeep } from '@dxos/util';
30
+ import { trace } from '@dxos/tracing';
31
+ import { bufferToArray, compositeKey, setDeep } from '@dxos/util';
30
32
 
31
33
  import {
32
- type EchoReplicator,
33
- type EchoReplicatorContext,
34
- type ReplicatorConnection,
35
- type ShouldAdvertiseParams,
36
- type ShouldSyncCollectionParams,
34
+ type AutomergeReplicatorConnection,
35
+ type AutomergeReplicatorContext,
36
+ type EdgeAutomergeReplicator,
37
+ type ShouldAdvertiseProps,
38
+ type ShouldSyncCollectionProps,
37
39
  getSpaceIdFromCollectionId,
38
40
  } from '../automerge';
39
-
40
41
  import { InflightRequestLimiter } from './inflight-request-limiter';
41
42
 
42
43
  /**
@@ -46,33 +47,34 @@ const INITIAL_RESTART_DELAY = 500;
46
47
  const RESTART_DELAY_JITTER = 250;
47
48
  const MAX_RESTART_DELAY = 5000;
48
49
 
49
- export type EchoEdgeReplicatorParams = {
50
+ export type EchoEdgeReplicatorProps = {
50
51
  edgeConnection: EdgeConnection;
51
52
  edgeHttpClient: EdgeHttpClient;
52
53
  disableSharePolicy?: boolean;
53
54
  };
54
55
 
55
- export class EchoEdgeReplicator implements EchoReplicator {
56
+ @trace.resource()
57
+ export class EchoEdgeReplicator implements EdgeAutomergeReplicator {
56
58
  private readonly _edgeConnection: EdgeConnection;
57
59
  private readonly _edgeHttpClient: EdgeHttpClient;
58
60
  private readonly _mutex = new Mutex();
59
61
 
60
62
  private _ctx?: Context = undefined;
61
- private _context: EchoReplicatorContext | null = null;
63
+ private _context: AutomergeReplicatorContext | null = null;
62
64
  private _connectedSpaces = new Set<SpaceId>();
63
65
  private _connections = new Map<SpaceId, EdgeReplicatorConnection>();
64
66
  private _sharePolicyEnabled = true;
65
67
 
66
- constructor({ edgeConnection, edgeHttpClient, disableSharePolicy }: EchoEdgeReplicatorParams) {
68
+ constructor({ edgeConnection, edgeHttpClient, disableSharePolicy }: EchoEdgeReplicatorProps) {
67
69
  this._edgeConnection = edgeConnection;
68
70
  this._edgeHttpClient = edgeHttpClient;
69
71
  this._sharePolicyEnabled = !disableSharePolicy;
70
72
  }
71
73
 
72
- async connect(context: EchoReplicatorContext): Promise<void> {
74
+ async connect(ctx: Context, context: AutomergeReplicatorContext): Promise<void> {
73
75
  log('connecting...', { peerId: context.peerId, connectedSpaces: this._connectedSpaces.size });
74
76
  this._context = context;
75
- this._ctx = Context.default();
77
+ this._ctx = ctx.derive();
76
78
  this._ctx.onDispose(
77
79
  this._edgeConnection.onReconnected(() => {
78
80
  this._ctx && scheduleMicroTask(this._ctx, () => this._handleReconnect());
@@ -91,7 +93,7 @@ export class EchoEdgeReplicator implements EchoReplicator {
91
93
 
92
94
  if (this._context !== null) {
93
95
  for (const spaceId of spaces) {
94
- await this._openConnection(spaceId);
96
+ await this._openConnection(this._ctx!, spaceId);
95
97
  }
96
98
  }
97
99
  }
@@ -106,7 +108,9 @@ export class EchoEdgeReplicator implements EchoReplicator {
106
108
  this._connections.clear();
107
109
  }
108
110
 
109
- async connectToSpace(spaceId: SpaceId): Promise<void> {
111
+ @trace.span({ showInBrowserTimeline: true, op: 'replication' })
112
+ async connectToSpace(ctx: Context, spaceId: SpaceId): Promise<void> {
113
+ log('connectToSpace', { spaceId });
110
114
  using _guard = await this._mutex.acquire();
111
115
 
112
116
  if (this._connectedSpaces.has(spaceId)) {
@@ -114,9 +118,8 @@ export class EchoEdgeReplicator implements EchoReplicator {
114
118
  }
115
119
  this._connectedSpaces.add(spaceId);
116
120
 
117
- // Check if AM-repo requested that we connect to remote peers.
118
121
  if (this._context !== null) {
119
- await this._openConnection(spaceId);
122
+ await this._openConnection(ctx, spaceId);
120
123
  }
121
124
  }
122
125
 
@@ -132,7 +135,8 @@ export class EchoEdgeReplicator implements EchoReplicator {
132
135
  }
133
136
  }
134
137
 
135
- private async _openConnection(spaceId: SpaceId, reconnects: number = 0): Promise<void> {
138
+ @trace.span({ showInBrowserTimeline: true, op: 'replication' })
139
+ private async _openConnection(ctx: Context, spaceId: SpaceId, reconnects: number = 0): Promise<void> {
136
140
  invariant(this._context);
137
141
  invariant(!this._connections.has(spaceId));
138
142
 
@@ -164,21 +168,21 @@ export class EchoEdgeReplicator implements EchoReplicator {
164
168
 
165
169
  restartScheduled = true;
166
170
  scheduleTask(
167
- this._ctx,
171
+ this._ctx!,
168
172
  async () => {
169
173
  using _guard = await this._mutex.acquire();
170
174
  if (this._connections.get(spaceId) !== connection) {
171
175
  return;
172
176
  }
173
177
 
174
- const ctx = this._ctx;
175
- await connection.close(); // Will call onRemoteDisconnected
178
+ const replicatorCtx = this._ctx;
179
+ await connection.close();
176
180
  this._connections.delete(spaceId);
177
- if (ctx?.disposed) {
181
+ if (replicatorCtx?.disposed) {
178
182
  return;
179
183
  }
180
184
  log.trace('dxos.echo.edge.replicator.restart', { spaceId, reconnects, restartDelay });
181
- await this._openConnection(spaceId, reconnects + 1);
185
+ await this._openConnection(replicatorCtx!, spaceId, reconnects + 1);
182
186
  },
183
187
  restartDelay,
184
188
  );
@@ -186,15 +190,15 @@ export class EchoEdgeReplicator implements EchoReplicator {
186
190
  });
187
191
  this._connections.set(spaceId, connection);
188
192
 
189
- await connection.open();
193
+ await connection.open(ctx);
190
194
  }
191
195
  }
192
196
 
193
- type EdgeReplicatorConnectionsParams = {
197
+ type EdgeReplicatorConnectionsProps = {
194
198
  edgeConnection: EdgeConnection;
195
199
  edgeHttpClient: EdgeHttpClient;
196
200
  spaceId: SpaceId;
197
- context: EchoReplicatorContext;
201
+ context: AutomergeReplicatorContext;
198
202
  sharedPolicyEnabled: boolean;
199
203
  onRemoteConnected: () => Promise<void>;
200
204
  onRemoteDisconnected: () => Promise<void>;
@@ -204,14 +208,14 @@ type EdgeReplicatorConnectionsParams = {
204
208
  const MAX_INFLIGHT_REQUESTS = 5;
205
209
  const MAX_RATE_LIMIT_WAIT_TIME_MS = 3000;
206
210
 
207
- class EdgeReplicatorConnection extends Resource implements ReplicatorConnection {
211
+ class EdgeReplicatorConnection extends Resource implements AutomergeReplicatorConnection {
208
212
  private readonly _connectionId = randomUUID();
209
213
  private readonly _edgeConnection: EdgeConnection;
210
214
  private readonly _edgeHttpClient: EdgeHttpClient;
211
215
  private readonly _remotePeerId: string | null = null;
212
216
  private readonly _targetServiceId: string;
213
217
  private readonly _spaceId: SpaceId;
214
- private readonly _context: EchoReplicatorContext;
218
+ private readonly _context: AutomergeReplicatorContext;
215
219
  private readonly _sharedPolicyEnabled: boolean;
216
220
  private readonly _onRemoteConnected: () => Promise<void>;
217
221
  private readonly _onRemoteDisconnected: () => Promise<void>;
@@ -237,7 +241,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
237
241
  onRemoteConnected,
238
242
  onRemoteDisconnected,
239
243
  onRestartRequested,
240
- }: EdgeReplicatorConnectionsParams) {
244
+ }: EdgeReplicatorConnectionsProps) {
241
245
  super();
242
246
  this._edgeConnection = edgeConnection;
243
247
  this._edgeHttpClient = edgeHttpClient;
@@ -247,8 +251,9 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
247
251
  // This way automerge-repo will have separate sync states for every connection.
248
252
  // This is important because the previous connection might have had some messages that failed to deliver
249
253
  // abd if we don't clear the sync-state, automerge will not attempt to deliver them again.
250
- this._remotePeerId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}-${this._connectionId}`;
251
- this._targetServiceId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}`;
254
+ this._targetServiceId = compositeKey(EdgeService.AUTOMERGE_REPLICATOR, spaceId);
255
+ // TODO(wittjosiah): Use compositeKey.
256
+ this._remotePeerId = `${this._targetServiceId}-${this._connectionId}`;
252
257
  this._sharedPolicyEnabled = sharedPolicyEnabled;
253
258
  this._onRemoteConnected = onRemoteConnected;
254
259
  this._onRemoteDisconnected = onRemoteDisconnected;
@@ -264,7 +269,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
264
269
  write: async (message: AutomergeProtocolMessage, controller) => {
265
270
  await this._requestLimiter.rateLimit(message);
266
271
 
267
- await this._sendMessage(message);
272
+ await this._sendMessage(this._ctx, message);
268
273
  },
269
274
  });
270
275
  }
@@ -297,7 +302,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
297
302
  await this._onRemoteConnected();
298
303
  }
299
304
 
300
- protected override async _close(): Promise<void> {
305
+ protected override async _close(ctx: Context): Promise<void> {
301
306
  log('closing...');
302
307
  this._readableStreamController.close();
303
308
 
@@ -311,7 +316,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
311
316
  return this._remotePeerId;
312
317
  }
313
318
 
314
- async shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean> {
319
+ async shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean> {
315
320
  if (!this._sharedPolicyEnabled) {
316
321
  return true;
317
322
  }
@@ -336,7 +341,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
336
341
  return spaceId === this._spaceId;
337
342
  }
338
343
 
339
- shouldSyncCollection(params: ShouldSyncCollectionParams): boolean {
344
+ shouldSyncCollection(params: ShouldSyncCollectionProps): boolean {
340
345
  if (!this._sharedPolicyEnabled) {
341
346
  return true;
342
347
  }
@@ -365,7 +370,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
365
370
  return true;
366
371
  }
367
372
 
368
- async pushBundle(bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]) {
373
+ async pushBundle(ctx: Context, bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]) {
369
374
  const request: ImportBundleRequest = {
370
375
  bundle: bundle.map(({ documentId, data, heads }) => ({
371
376
  documentId,
@@ -373,12 +378,12 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
373
378
  heads,
374
379
  })),
375
380
  };
376
- await this._edgeHttpClient.importBundle(this._spaceId, request);
381
+ await this._edgeHttpClient.importBundle(ctx, this._spaceId, request);
377
382
  }
378
383
 
379
- async pullBundle(docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>> {
384
+ async pullBundle(ctx: Context, docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>> {
380
385
  const request: ExportBundleRequest = { docHeads };
381
- const response = await this._edgeHttpClient.exportBundle(this._spaceId, request);
386
+ const response = await this._edgeHttpClient.exportBundle(ctx, this._spaceId, request);
382
387
  return Object.fromEntries(response.bundle.map((doc) => [doc.documentId, DocumentCodec.decode(doc.mutation)]));
383
388
  }
384
389
 
@@ -387,6 +392,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
387
392
  // AutomergeReplicator might return a Forbidden error if the credentials are not yet replicated.
388
393
  // We restart the connection with some delay to account for that.
389
394
  if (isErrorMessage(message)) {
395
+ log.verbose('stream error', { error: (message as ErrorProtocolMessage).message });
390
396
  this._onRestartRequested();
391
397
  return;
392
398
  }
@@ -396,7 +402,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
396
402
  this._readableStreamController.enqueue(message);
397
403
  }
398
404
 
399
- private async _sendMessage(message: AutomergeProtocolMessage): Promise<void> {
405
+ private async _sendMessage(ctx: Context, message: AutomergeProtocolMessage): Promise<void> {
400
406
  // Fix the peer id.
401
407
  (message as any).targetId = this._targetServiceId as PeerId;
402
408
 
@@ -413,6 +419,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
413
419
 
414
420
  try {
415
421
  await this._edgeConnection.send(
422
+ ctx,
416
423
  buf.create(RouterMessageSchema, {
417
424
  serviceId: this._targetServiceId,
418
425
  source: {
@@ -438,10 +445,11 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
438
445
  const isErrorMessage = (message: AutomergeProtocolMessage) => message.type === 'error';
439
446
 
440
447
  const getMessageInfo = (msg: AutomergeProtocolMessage) => {
441
- const { have, heads, need, changes } = msg.type === 'sync' ? Automerge.decodeSyncMessage(msg.data) : {};
448
+ const { have, heads, need, changes } = msg.type === 'sync' ? A.decodeSyncMessage(msg.data) : {};
442
449
  return {
443
450
  type: msg.type,
444
451
  documentId: 'documentId' in msg ? msg.documentId : undefined,
452
+ collectionId: 'collectionId' in msg ? msg.collectionId : undefined,
445
453
  have,
446
454
  heads,
447
455
  need,
@@ -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
+ });