@dxos/echo-pipeline 0.8.3 → 0.8.4-main.05e74ebcff

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 (244) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/neutral/chunk-46DCRDA7.mjs +271 -0
  4. package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
  5. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-LR7UFVQX.mjs} +313 -709
  6. package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
  7. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -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 +90 -311
  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 +119 -54
  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 +25 -4
  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 +23 -10
  27. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  28. package/dist/types/src/automerge/echo-replicator.d.ts +46 -15
  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 +3 -1
  37. package/dist/types/src/automerge/index.d.ts.map +1 -1
  38. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +11 -5
  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 +9 -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/codec.d.ts +1 -1
  47. package/dist/types/src/common/codec.d.ts.map +1 -1
  48. package/dist/types/src/common/feeds.d.ts.map +1 -1
  49. package/dist/types/src/common/space-id.d.ts.map +1 -1
  50. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  51. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  52. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  53. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  54. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  55. package/dist/types/src/db-host/data-service.d.ts +13 -7
  56. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  57. package/dist/types/src/db-host/database-root.d.ts +9 -3
  58. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  59. package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
  60. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  61. package/dist/types/src/db-host/echo-host.d.ts +74 -28
  62. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  63. package/dist/types/src/db-host/index.d.ts +2 -0
  64. package/dist/types/src/db-host/index.d.ts.map +1 -1
  65. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  66. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  67. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  68. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  69. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  70. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  71. package/dist/types/src/db-host/query-service.d.ts +19 -11
  72. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  73. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  74. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  75. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  76. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  77. package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
  78. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  79. package/dist/types/src/db-host/stub.d.ts +11 -0
  80. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  81. package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
  82. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  83. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  84. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  85. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  86. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  87. package/dist/types/src/edge/index.d.ts +1 -0
  88. package/dist/types/src/edge/index.d.ts.map +1 -1
  89. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  90. package/dist/types/src/filter/filter-match.d.ts +4 -1
  91. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  92. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  93. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  94. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  95. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  96. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  97. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  98. package/dist/types/src/query/errors.d.ts +22 -10
  99. package/dist/types/src/query/errors.d.ts.map +1 -1
  100. package/dist/types/src/query/plan.d.ts +66 -8
  101. package/dist/types/src/query/plan.d.ts.map +1 -1
  102. package/dist/types/src/query/query-executor.d.ts +66 -11
  103. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  104. package/dist/types/src/query/query-planner.d.ts +12 -1
  105. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  106. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  107. package/dist/types/src/space/auth.d.ts +3 -3
  108. package/dist/types/src/space/auth.d.ts.map +1 -1
  109. package/dist/types/src/space/control-pipeline.d.ts +5 -4
  110. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  111. package/dist/types/src/space/space-manager.d.ts +8 -8
  112. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  113. package/dist/types/src/space/space-protocol.d.ts +10 -9
  114. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  115. package/dist/types/src/space/space.d.ts +6 -6
  116. package/dist/types/src/space/space.d.ts.map +1 -1
  117. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  118. package/dist/types/src/testing/index.d.ts +0 -2
  119. package/dist/types/src/testing/index.d.ts.map +1 -1
  120. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  121. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  122. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
  123. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  124. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  125. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  126. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  127. package/dist/types/src/util.d.ts +1 -1
  128. package/dist/types/src/util.d.ts.map +1 -1
  129. package/dist/types/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +59 -55
  131. package/src/automerge/automerge-host-subduction.test.ts +263 -0
  132. package/src/automerge/automerge-host.test.ts +162 -39
  133. package/src/automerge/automerge-host.ts +679 -201
  134. package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
  135. package/src/automerge/automerge-repo.test.ts +349 -91
  136. package/src/automerge/automerge-subduction.test.ts +330 -0
  137. package/src/automerge/collection-synchronizer.test.ts +98 -17
  138. package/src/automerge/collection-synchronizer.ts +141 -37
  139. package/src/automerge/echo-data-monitor.ts +1 -1
  140. package/src/automerge/echo-network-adapter.test.ts +5 -4
  141. package/src/automerge/echo-network-adapter.ts +76 -31
  142. package/src/automerge/echo-replicator.ts +50 -15
  143. package/src/automerge/errors.ts +10 -0
  144. package/src/automerge/handle-state.ts +62 -0
  145. package/src/automerge/heads-store.ts +42 -3
  146. package/src/automerge/index.ts +3 -1
  147. package/src/automerge/leveldb-storage-adapter.ts +77 -11
  148. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  149. package/src/automerge/mesh-echo-replicator.ts +15 -11
  150. package/src/automerge/storage-adapter.test.ts +1 -1
  151. package/src/common/space-id.ts +1 -1
  152. package/src/db-host/automerge-data-source.test.ts +299 -0
  153. package/src/db-host/automerge-data-source.ts +187 -0
  154. package/src/db-host/data-service.ts +35 -26
  155. package/src/db-host/database-root.ts +20 -8
  156. package/src/db-host/documents-synchronizer.test.ts +145 -14
  157. package/src/db-host/documents-synchronizer.ts +163 -51
  158. package/src/db-host/echo-host.ts +384 -99
  159. package/src/db-host/index.ts +2 -0
  160. package/src/db-host/invalidation-hint.ts +50 -0
  161. package/src/db-host/local-queue-service.ts +127 -0
  162. package/src/db-host/query-invalidation.test.ts +270 -0
  163. package/src/db-host/query-service.ts +111 -106
  164. package/src/db-host/queue-data-source.ts +160 -0
  165. package/src/db-host/queue-service.test.ts +145 -0
  166. package/src/db-host/space-state-manager.ts +12 -10
  167. package/src/db-host/stub.ts +33 -0
  168. package/src/edge/echo-edge-replicator.test.ts +12 -10
  169. package/src/edge/echo-edge-replicator.ts +144 -58
  170. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  171. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  172. package/src/edge/index.ts +1 -0
  173. package/src/filter/filter-match.test.ts +68 -32
  174. package/src/filter/filter-match.ts +230 -21
  175. package/src/metadata/metadata-store.ts +11 -8
  176. package/src/pipeline/pipeline-stress.test.ts +1 -1
  177. package/src/pipeline/pipeline.test.ts +2 -2
  178. package/src/pipeline/pipeline.ts +9 -7
  179. package/src/query/errors.ts +3 -1
  180. package/src/query/plan.ts +84 -9
  181. package/src/query/query-executor.ts +1197 -165
  182. package/src/query/query-planner.test.ts +993 -81
  183. package/src/query/query-planner.ts +503 -27
  184. package/src/space/admission-discovery-extension.ts +3 -3
  185. package/src/space/auth.ts +6 -6
  186. package/src/space/control-pipeline.test.ts +5 -4
  187. package/src/space/control-pipeline.ts +16 -13
  188. package/src/space/space-manager.browser.test.ts +1 -1
  189. package/src/space/space-manager.ts +16 -20
  190. package/src/space/space-protocol.browser.test.ts +15 -14
  191. package/src/space/space-protocol.test.ts +15 -14
  192. package/src/space/space-protocol.ts +22 -18
  193. package/src/space/space.test.ts +3 -3
  194. package/src/space/space.ts +11 -11
  195. package/src/testing/index.ts +0 -2
  196. package/src/testing/test-agent-builder.ts +3 -3
  197. package/src/testing/test-network-adapter.ts +24 -3
  198. package/src/testing/test-replicator.ts +19 -20
  199. package/src/util.ts +7 -3
  200. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  201. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  202. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  203. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  204. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  205. package/dist/lib/browser/index.mjs +0 -4743
  206. package/dist/lib/browser/index.mjs.map +0 -7
  207. package/dist/lib/browser/meta.json +0 -1
  208. package/dist/lib/browser/testing/index.mjs.map +0 -7
  209. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  210. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  211. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  212. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  213. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  214. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  215. package/dist/lib/node/filter/index.cjs +0 -32
  216. package/dist/lib/node/filter/index.cjs.map +0 -7
  217. package/dist/lib/node/index.cjs +0 -4699
  218. package/dist/lib/node/index.cjs.map +0 -7
  219. package/dist/lib/node/meta.json +0 -1
  220. package/dist/lib/node/testing/index.cjs +0 -753
  221. package/dist/lib/node/testing/index.cjs.map +0 -7
  222. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  223. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  224. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  225. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  226. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  227. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  228. package/dist/lib/node-esm/filter/index.mjs +0 -11
  229. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  230. package/dist/lib/node-esm/index.mjs +0 -4743
  231. package/dist/lib/node-esm/index.mjs.map +0 -7
  232. package/dist/lib/node-esm/meta.json +0 -1
  233. package/dist/lib/node-esm/testing/index.mjs +0 -742
  234. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  235. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  236. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  237. package/dist/types/src/testing/test-data.d.ts +0 -18
  238. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  239. package/dist/types/src/testing/test-schema.d.ts +0 -37
  240. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  241. package/src/db-host/documents-iterator.ts +0 -74
  242. package/src/testing/test-data.ts +0 -127
  243. package/src/testing/test-schema.ts +0 -53
  244. /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 { createEphemeralEdgeIdentity, EdgeClient, MessageSchema } from '@dxos/edge-client';
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: EchoReplicatorContext) => {
82
- const replicator = new EchoEdgeReplicator({ edgeConnection: client });
83
- await replicator.connect(context);
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: ReplicatorConnection[] = [];
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 EchoReplicatorContext,
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 { cbor } from '@automerge/automerge-repo';
5
+ import * as A from '@automerge/automerge';
6
+ import { type DocumentId, type Heads, cbor } from '@automerge/automerge-repo';
6
7
 
7
- import { Mutex, scheduleTask, scheduleMicroTask } from '@dxos/async';
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 { EdgeService, type AutomergeProtocolMessage, type PeerId } from '@dxos/protocols';
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 { bufferToArray } from '@dxos/util';
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 AutomergeReplicatorConnection,
35
+ type AutomergeReplicatorContext,
36
+ type EdgeAutomergeReplicator,
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 EchoEdgeReplicatorParams = {
50
+ export type EchoEdgeReplicatorProps = {
41
51
  edgeConnection: EdgeConnection;
52
+ edgeHttpClient: EdgeHttpClient;
42
53
  disableSharePolicy?: boolean;
43
54
  };
44
55
 
45
- export class EchoEdgeReplicator implements EchoReplicator {
56
+ @trace.resource()
57
+ export class EchoEdgeReplicator implements EdgeAutomergeReplicator {
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: EchoReplicatorContext | null = null;
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 }: EchoEdgeReplicatorParams) {
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: EchoReplicatorContext): Promise<void> {
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 = Context.default();
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
- 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 });
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
- 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> {
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 ctx = this._ctx;
160
- await connection.close(); // Will call onRemoteDisconnected
178
+ const replicatorCtx = this._ctx;
179
+ await connection.close();
161
180
  this._connections.delete(spaceId);
162
- if (ctx?.disposed) {
181
+ if (replicatorCtx?.disposed) {
163
182
  return;
164
183
  }
165
- await this._openConnection(spaceId, reconnects + 1);
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 EdgeReplicatorConnectionsParams = {
197
+ type EdgeReplicatorConnectionsProps = {
178
198
  edgeConnection: EdgeConnection;
199
+ edgeHttpClient: EdgeHttpClient;
179
200
  spaceId: SpaceId;
180
- context: EchoReplicatorContext;
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 ReplicatorConnection {
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: EchoReplicatorContext;
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
- }: EdgeReplicatorConnectionsParams) {
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._remotePeerId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}-${randomUUID()}`;
229
- 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}`;
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: ShouldAdvertiseParams): Promise<boolean> {
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: ShouldSyncCollectionParams): boolean {
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
- type: payload.type,
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 (isForbiddenErrorMessage(message)) {
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
- type: message.type,
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
- await this._edgeConnection.send(
357
- buf.create(RouterMessageSchema, {
358
- serviceId: this._targetServiceId,
359
- source: {
360
- identityKey: this._edgeConnection.identityKey,
361
- peerKey: this._edgeConnection.peerKey,
362
- },
363
- payload: { value: bufferToArray(encoded) },
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 isForbiddenErrorMessage = (message: AutomergeProtocolMessage) =>
373
- message.type === 'error' && message.message === 'Forbidden';
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
+ };