@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1068cf700f

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 (199) hide show
  1. package/dist/lib/neutral/chunk-FJPXA75J.mjs +235 -0
  2. package/dist/lib/neutral/chunk-FJPXA75J.mjs.map +7 -0
  3. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-WIQUYO7H.mjs} +300 -213
  4. package/dist/lib/neutral/chunk-WIQUYO7H.mjs.map +7 -0
  5. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  6. package/dist/lib/{browser → neutral}/index.mjs +2832 -946
  7. package/dist/lib/neutral/index.mjs.map +7 -0
  8. package/dist/lib/neutral/meta.json +1 -0
  9. package/dist/lib/{browser → neutral}/testing/index.mjs +73 -224
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/automerge/automerge-host.d.ts +53 -42
  12. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  13. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  14. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  15. package/dist/types/src/automerge/collection-synchronizer.d.ts +6 -4
  16. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  17. package/dist/types/src/automerge/echo-network-adapter.d.ts +22 -10
  18. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  19. package/dist/types/src/automerge/echo-replicator.d.ts +37 -15
  20. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  21. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  22. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  23. package/dist/types/src/automerge/index.d.ts +1 -1
  24. package/dist/types/src/automerge/index.d.ts.map +1 -1
  25. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +5 -5
  26. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  27. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  28. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  29. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +5 -5
  30. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  31. package/dist/types/src/common/codec.d.ts +1 -1
  32. package/dist/types/src/common/codec.d.ts.map +1 -1
  33. package/dist/types/src/db-host/automerge-data-source.d.ts +34 -0
  34. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  35. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  36. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  37. package/dist/types/src/db-host/data-service.d.ts +5 -4
  38. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  39. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  40. package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
  41. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  42. package/dist/types/src/db-host/echo-host.d.ts +44 -23
  43. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  44. package/dist/types/src/db-host/index.d.ts +1 -0
  45. package/dist/types/src/db-host/index.d.ts.map +1 -1
  46. package/dist/types/src/db-host/local-queue-service.d.ts +16 -0
  47. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  48. package/dist/types/src/db-host/query-service.d.ts +14 -9
  49. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  50. package/dist/types/src/db-host/queue-data-source.d.ts +28 -0
  51. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  52. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  53. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  54. package/dist/types/src/db-host/space-state-manager.d.ts +2 -1
  55. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  56. package/dist/types/src/db-host/stub.d.ts +10 -0
  57. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  58. package/dist/types/src/edge/echo-edge-replicator.d.ts +8 -6
  59. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  60. package/dist/types/src/filter/filter-match.d.ts +4 -1
  61. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  62. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  63. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  64. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  65. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  66. package/dist/types/src/query/errors.d.ts +21 -9
  67. package/dist/types/src/query/errors.d.ts.map +1 -1
  68. package/dist/types/src/query/plan.d.ts +57 -5
  69. package/dist/types/src/query/plan.d.ts.map +1 -1
  70. package/dist/types/src/query/query-executor.d.ts +49 -10
  71. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  72. package/dist/types/src/query/query-planner.d.ts +9 -0
  73. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  74. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  75. package/dist/types/src/space/auth.d.ts +3 -3
  76. package/dist/types/src/space/auth.d.ts.map +1 -1
  77. package/dist/types/src/space/control-pipeline.d.ts +3 -3
  78. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  79. package/dist/types/src/space/space-manager.d.ts +7 -7
  80. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  81. package/dist/types/src/space/space-protocol.d.ts +6 -6
  82. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  83. package/dist/types/src/space/space.d.ts +4 -4
  84. package/dist/types/src/space/space.d.ts.map +1 -1
  85. package/dist/types/src/testing/index.d.ts +0 -2
  86. package/dist/types/src/testing/index.d.ts.map +1 -1
  87. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  88. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  89. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  90. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  91. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  92. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  93. package/dist/types/src/util.d.ts +1 -1
  94. package/dist/types/src/util.d.ts.map +1 -1
  95. package/dist/types/tsconfig.tsbuildinfo +1 -1
  96. package/package.json +53 -45
  97. package/src/automerge/automerge-host.test.ts +55 -24
  98. package/src/automerge/automerge-host.ts +399 -126
  99. package/src/automerge/automerge-repo.test.ts +214 -46
  100. package/src/automerge/collection-synchronizer.test.ts +22 -15
  101. package/src/automerge/collection-synchronizer.ts +57 -22
  102. package/src/automerge/echo-data-monitor.ts +1 -1
  103. package/src/automerge/echo-network-adapter.test.ts +3 -3
  104. package/src/automerge/echo-network-adapter.ts +73 -28
  105. package/src/automerge/echo-replicator.ts +40 -15
  106. package/src/automerge/heads-store.ts +42 -3
  107. package/src/automerge/index.ts +1 -1
  108. package/src/automerge/leveldb-storage-adapter.ts +11 -11
  109. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  110. package/src/automerge/mesh-echo-replicator.ts +15 -11
  111. package/src/automerge/storage-adapter.test.ts +1 -1
  112. package/src/common/space-id.ts +1 -1
  113. package/src/db-host/automerge-data-source.test.ts +292 -0
  114. package/src/db-host/automerge-data-source.ts +144 -0
  115. package/src/db-host/data-service.ts +19 -20
  116. package/src/db-host/database-root.ts +2 -2
  117. package/src/db-host/documents-synchronizer.test.ts +73 -13
  118. package/src/db-host/documents-synchronizer.ts +58 -44
  119. package/src/db-host/echo-host.ts +194 -84
  120. package/src/db-host/index.ts +1 -0
  121. package/src/db-host/local-queue-service.ts +147 -0
  122. package/src/db-host/query-service.ts +28 -95
  123. package/src/db-host/queue-data-source.ts +132 -0
  124. package/src/db-host/queue-service.test.ts +145 -0
  125. package/src/db-host/space-state-manager.ts +6 -2
  126. package/src/db-host/stub.ts +28 -0
  127. package/src/edge/echo-edge-replicator.test.ts +8 -6
  128. package/src/edge/echo-edge-replicator.ts +124 -42
  129. package/src/filter/filter-match.test.ts +58 -32
  130. package/src/filter/filter-match.ts +163 -15
  131. package/src/metadata/metadata-store.ts +8 -5
  132. package/src/pipeline/pipeline-stress.test.ts +4 -2
  133. package/src/pipeline/pipeline.test.ts +3 -2
  134. package/src/pipeline/pipeline.ts +10 -7
  135. package/src/query/errors.ts +3 -1
  136. package/src/query/plan.ts +75 -5
  137. package/src/query/query-executor.ts +812 -117
  138. package/src/query/query-planner.test.ts +652 -34
  139. package/src/query/query-planner.ts +276 -13
  140. package/src/space/admission-discovery-extension.ts +3 -3
  141. package/src/space/auth.ts +6 -6
  142. package/src/space/control-pipeline.test.ts +4 -3
  143. package/src/space/control-pipeline.ts +11 -8
  144. package/src/space/space-manager.browser.test.ts +1 -1
  145. package/src/space/space-manager.ts +11 -10
  146. package/src/space/space-protocol.browser.test.ts +2 -2
  147. package/src/space/space-protocol.test.ts +3 -2
  148. package/src/space/space-protocol.ts +17 -14
  149. package/src/space/space.test.ts +1 -1
  150. package/src/space/space.ts +6 -5
  151. package/src/testing/index.ts +0 -2
  152. package/src/testing/test-agent-builder.ts +4 -3
  153. package/src/testing/test-network-adapter.ts +16 -3
  154. package/src/testing/test-replicator.ts +23 -18
  155. package/src/util.ts +7 -3
  156. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  157. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  158. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  159. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  160. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  161. package/dist/lib/browser/index.mjs.map +0 -7
  162. package/dist/lib/browser/meta.json +0 -1
  163. package/dist/lib/browser/testing/index.mjs.map +0 -7
  164. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  165. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  166. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  167. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  168. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  169. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  170. package/dist/lib/node/filter/index.cjs +0 -32
  171. package/dist/lib/node/filter/index.cjs.map +0 -7
  172. package/dist/lib/node/index.cjs +0 -4699
  173. package/dist/lib/node/index.cjs.map +0 -7
  174. package/dist/lib/node/meta.json +0 -1
  175. package/dist/lib/node/testing/index.cjs +0 -753
  176. package/dist/lib/node/testing/index.cjs.map +0 -7
  177. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  178. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  179. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  180. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  181. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  182. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  183. package/dist/lib/node-esm/filter/index.mjs +0 -11
  184. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  185. package/dist/lib/node-esm/index.mjs +0 -4743
  186. package/dist/lib/node-esm/index.mjs.map +0 -7
  187. package/dist/lib/node-esm/meta.json +0 -1
  188. package/dist/lib/node-esm/testing/index.mjs +0 -742
  189. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  190. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  191. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  192. package/dist/types/src/testing/test-data.d.ts +0 -18
  193. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  194. package/dist/types/src/testing/test-schema.d.ts +0 -37
  195. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  196. package/src/db-host/documents-iterator.ts +0 -74
  197. package/src/testing/test-data.ts +0 -127
  198. package/src/testing/test-schema.ts +0 -53
  199. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -7,7 +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 { createEphemeralEdgeIdentity, EdgeClient, MessageSchema } from '@dxos/edge-client';
10
+ import { EdgeClient, type EdgeHttpClient, MessageSchema, createEphemeralEdgeIdentity } from '@dxos/edge-client';
11
11
  import { createTestEdgeWsServer } from '@dxos/edge-client/testing';
12
12
  import { PublicKey, SpaceId } from '@dxos/keys';
13
13
  import { EdgeService } from '@dxos/protocols';
@@ -16,8 +16,9 @@ import { createBuf } from '@dxos/protocols/buf';
16
16
  import type { Peer } from '@dxos/protocols/proto/dxos/edge/messenger';
17
17
  import { openAndClose } from '@dxos/test-utils';
18
18
 
19
+ import type { AutomergeReplicatorConnection, AutomergeReplicatorContext } from '../automerge';
20
+
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 () => {
@@ -78,8 +79,9 @@ describe('EchoEdgeReplicator', () => {
78
79
  });
79
80
  });
80
81
 
81
- const connectReplicator = async (client: EdgeClient, context: EchoReplicatorContext) => {
82
- const replicator = new EchoEdgeReplicator({ edgeConnection: client });
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 });
83
85
  await replicator.connect(context);
84
86
  onTestFinished(() => replicator.disconnect());
85
87
  return replicator;
@@ -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,34 +2,44 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { cbor } from '@automerge/automerge-repo';
5
+ import * as Automerge 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 { bufferToArray, setDeep } from '@dxos/util';
22
31
 
23
- import { InflightRequestLimiter } from './inflight-request-limiter';
24
32
  import {
33
+ type AutomergeReplicator,
34
+ type AutomergeReplicatorConnection,
35
+ type AutomergeReplicatorContext,
36
+ type ShouldAdvertiseProps,
37
+ type ShouldSyncCollectionProps,
25
38
  getSpaceIdFromCollectionId,
26
- type EchoReplicator,
27
- type EchoReplicatorContext,
28
- type ReplicatorConnection,
29
- type ShouldAdvertiseParams,
30
- type ShouldSyncCollectionParams,
31
39
  } from '../automerge';
32
40
 
41
+ import { InflightRequestLimiter } from './inflight-request-limiter';
42
+
33
43
  /**
34
44
  * Delay before restarting the connection after receiving a forbidden error.
35
45
  */
@@ -37,27 +47,30 @@ 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
+ export class EchoEdgeReplicator implements AutomergeReplicator {
46
57
  private readonly _edgeConnection: EdgeConnection;
58
+ private readonly _edgeHttpClient: EdgeHttpClient;
47
59
  private readonly _mutex = new Mutex();
48
60
 
49
61
  private _ctx?: Context = undefined;
50
- private _context: EchoReplicatorContext | null = null;
62
+ private _context: AutomergeReplicatorContext | null = null;
51
63
  private _connectedSpaces = new Set<SpaceId>();
52
64
  private _connections = new Map<SpaceId, EdgeReplicatorConnection>();
53
65
  private _sharePolicyEnabled = true;
54
66
 
55
- constructor({ edgeConnection, disableSharePolicy }: EchoEdgeReplicatorParams) {
67
+ constructor({ edgeConnection, edgeHttpClient, disableSharePolicy }: EchoEdgeReplicatorProps) {
56
68
  this._edgeConnection = edgeConnection;
69
+ this._edgeHttpClient = edgeHttpClient;
57
70
  this._sharePolicyEnabled = !disableSharePolicy;
58
71
  }
59
72
 
60
- async connect(context: EchoReplicatorContext): Promise<void> {
73
+ async connect(context: AutomergeReplicatorContext): Promise<void> {
61
74
  log('connecting...', { peerId: context.peerId, connectedSpaces: this._connectedSpaces.size });
62
75
  this._context = context;
63
76
  this._ctx = Context.default();
@@ -95,6 +108,7 @@ export class EchoEdgeReplicator implements EchoReplicator {
95
108
  }
96
109
 
97
110
  async connectToSpace(spaceId: SpaceId): Promise<void> {
111
+ log('connectToSpace', { spaceId });
98
112
  using _guard = await this._mutex.acquire();
99
113
 
100
114
  if (this._connectedSpaces.has(spaceId)) {
@@ -127,14 +141,17 @@ export class EchoEdgeReplicator implements EchoReplicator {
127
141
  let restartScheduled = false;
128
142
 
129
143
  const connection = new EdgeReplicatorConnection({
144
+ edgeHttpClient: this._edgeHttpClient,
130
145
  edgeConnection: this._edgeConnection,
131
146
  spaceId,
132
147
  context: this._context,
133
148
  sharedPolicyEnabled: this._sharePolicyEnabled,
134
149
  onRemoteConnected: async () => {
150
+ log.trace('dxos.echo.edge.replicator.onRemoteConnected', { spaceId });
135
151
  this._context?.onConnectionOpen(connection);
136
152
  },
137
153
  onRemoteDisconnected: async () => {
154
+ log.trace('dxos.echo.edge.replicator.onRemoteDisconnected', { spaceId });
138
155
  this._context?.onConnectionClosed(connection);
139
156
  },
140
157
  onRestartRequested: async () => {
@@ -162,6 +179,7 @@ export class EchoEdgeReplicator implements EchoReplicator {
162
179
  if (ctx?.disposed) {
163
180
  return;
164
181
  }
182
+ log.trace('dxos.echo.edge.replicator.restart', { spaceId, reconnects, restartDelay });
165
183
  await this._openConnection(spaceId, reconnects + 1);
166
184
  },
167
185
  restartDelay,
@@ -174,10 +192,11 @@ export class EchoEdgeReplicator implements EchoReplicator {
174
192
  }
175
193
  }
176
194
 
177
- type EdgeReplicatorConnectionsParams = {
195
+ type EdgeReplicatorConnectionsProps = {
178
196
  edgeConnection: EdgeConnection;
197
+ edgeHttpClient: EdgeHttpClient;
179
198
  spaceId: SpaceId;
180
- context: EchoReplicatorContext;
199
+ context: AutomergeReplicatorContext;
181
200
  sharedPolicyEnabled: boolean;
182
201
  onRemoteConnected: () => Promise<void>;
183
202
  onRemoteDisconnected: () => Promise<void>;
@@ -187,16 +206,19 @@ type EdgeReplicatorConnectionsParams = {
187
206
  const MAX_INFLIGHT_REQUESTS = 5;
188
207
  const MAX_RATE_LIMIT_WAIT_TIME_MS = 3000;
189
208
 
190
- class EdgeReplicatorConnection extends Resource implements ReplicatorConnection {
209
+ class EdgeReplicatorConnection extends Resource implements AutomergeReplicatorConnection {
210
+ private readonly _connectionId = randomUUID();
191
211
  private readonly _edgeConnection: EdgeConnection;
212
+ private readonly _edgeHttpClient: EdgeHttpClient;
192
213
  private readonly _remotePeerId: string | null = null;
193
214
  private readonly _targetServiceId: string;
194
215
  private readonly _spaceId: SpaceId;
195
- private readonly _context: EchoReplicatorContext;
216
+ private readonly _context: AutomergeReplicatorContext;
196
217
  private readonly _sharedPolicyEnabled: boolean;
197
218
  private readonly _onRemoteConnected: () => Promise<void>;
198
219
  private readonly _onRemoteDisconnected: () => Promise<void>;
199
220
  private readonly _onRestartRequested: () => void;
221
+ private _sequence = 0;
200
222
 
201
223
  private _requestLimiter = new InflightRequestLimiter({
202
224
  maxInflightRequests: MAX_INFLIGHT_REQUESTS,
@@ -210,22 +232,24 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
210
232
 
211
233
  constructor({
212
234
  edgeConnection,
235
+ edgeHttpClient,
213
236
  spaceId,
214
237
  context,
215
238
  sharedPolicyEnabled,
216
239
  onRemoteConnected,
217
240
  onRemoteDisconnected,
218
241
  onRestartRequested,
219
- }: EdgeReplicatorConnectionsParams) {
242
+ }: EdgeReplicatorConnectionsProps) {
220
243
  super();
221
244
  this._edgeConnection = edgeConnection;
245
+ this._edgeHttpClient = edgeHttpClient;
222
246
  this._spaceId = spaceId;
223
247
  this._context = context;
224
248
  // Generate a unique peer id for every connection.
225
249
  // This way automerge-repo will have separate sync states for every connection.
226
250
  // This is important because the previous connection might have had some messages that failed to deliver
227
251
  // 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()}`;
252
+ this._remotePeerId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}-${this._connectionId}`;
229
253
  this._targetServiceId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}`;
230
254
  this._sharedPolicyEnabled = sharedPolicyEnabled;
231
255
  this._onRemoteConnected = onRemoteConnected;
@@ -252,13 +276,26 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
252
276
 
253
277
  await this._requestLimiter.open();
254
278
 
255
- // TODO: handle reconnects
256
279
  this._ctx.onDispose(
257
280
  this._edgeConnection.onMessage((msg: RouterMessage) => {
258
281
  this._onMessage(msg);
259
282
  }),
260
283
  );
261
284
 
285
+ let firstReconnect = true;
286
+ this._ctx.onDispose(
287
+ // NOTE: This will fire immediately if the connection is already open.
288
+ this._edgeConnection.onReconnected(async () => {
289
+ if (firstReconnect) {
290
+ log.verbose('first reconnect skipped');
291
+ firstReconnect = false;
292
+ return;
293
+ }
294
+
295
+ this._onRestartRequested();
296
+ }),
297
+ );
298
+
262
299
  await this._onRemoteConnected();
263
300
  }
264
301
 
@@ -276,7 +313,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
276
313
  return this._remotePeerId;
277
314
  }
278
315
 
279
- async shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean> {
316
+ async shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean> {
280
317
  if (!this._sharedPolicyEnabled) {
281
318
  return true;
282
319
  }
@@ -301,7 +338,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
301
338
  return spaceId === this._spaceId;
302
339
  }
303
340
 
304
- shouldSyncCollection(params: ShouldSyncCollectionParams): boolean {
341
+ shouldSyncCollection(params: ShouldSyncCollectionProps): boolean {
305
342
  if (!this._sharedPolicyEnabled) {
306
343
  return true;
307
344
  }
@@ -317,8 +354,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
317
354
 
318
355
  const payload = cbor.decode(message.payload!.value) as AutomergeProtocolMessage;
319
356
  log.verbose('received', {
320
- type: payload.type,
321
- documentId: payload.type === 'sync' && payload.documentId,
357
+ ...getMessageInfo(payload),
322
358
  remoteId: this._remotePeerId,
323
359
  });
324
360
 
@@ -327,11 +363,33 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
327
363
  this._processMessage(payload);
328
364
  }
329
365
 
366
+ get bundleSyncEnabled(): boolean {
367
+ return true;
368
+ }
369
+
370
+ async pushBundle(bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]) {
371
+ const request: ImportBundleRequest = {
372
+ bundle: bundle.map(({ documentId, data, heads }) => ({
373
+ documentId,
374
+ mutation: DocumentCodec.encode(data),
375
+ heads,
376
+ })),
377
+ };
378
+ await this._edgeHttpClient.importBundle(this._spaceId, request);
379
+ }
380
+
381
+ async pullBundle(docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>> {
382
+ const request: ExportBundleRequest = { docHeads };
383
+ const response = await this._edgeHttpClient.exportBundle(this._spaceId, request);
384
+ return Object.fromEntries(response.bundle.map((doc) => [doc.documentId, DocumentCodec.decode(doc.mutation)]));
385
+ }
386
+
330
387
  private _processMessage(message: AutomergeProtocolMessage): void {
331
388
  // There's a race between the credentials being replicated that are needed for access control and the data replication.
332
389
  // AutomergeReplicator might return a Forbidden error if the credentials are not yet replicated.
333
390
  // We restart the connection with some delay to account for that.
334
- if (isForbiddenErrorMessage(message)) {
391
+ if (isErrorMessage(message)) {
392
+ log.verbose('stream error', { error: (message as ErrorProtocolMessage).message });
335
393
  this._onRestartRequested();
336
394
  return;
337
395
  }
@@ -345,29 +403,53 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
345
403
  // Fix the peer id.
346
404
  (message as any).targetId = this._targetServiceId as PeerId;
347
405
 
406
+ // Note: This is used on EDGE to detect out-of-order messages per connection.
407
+ setDeep(message, ['metadata', 'dxos_sequence'], this._getSequence());
408
+ setDeep(message, ['metadata', 'dxos_connectionId'], this._connectionId);
409
+
348
410
  log.verbose('sending...', {
349
- type: message.type,
350
- documentId: message.type === 'sync' && message.documentId,
411
+ ...getMessageInfo(message),
351
412
  remoteId: this._remotePeerId,
352
413
  });
353
414
 
354
415
  const encoded = cbor.encode(message);
355
416
 
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
- );
417
+ try {
418
+ await this._edgeConnection.send(
419
+ buf.create(RouterMessageSchema, {
420
+ serviceId: this._targetServiceId,
421
+ source: {
422
+ identityKey: this._edgeConnection.identityKey,
423
+ peerKey: this._edgeConnection.peerKey,
424
+ },
425
+ payload: { value: bufferToArray(encoded) },
426
+ }),
427
+ );
428
+ } catch (err) {
429
+ log.error('failed to send message', { err });
430
+ }
431
+ }
432
+
433
+ private _getSequence(): number {
434
+ return this._sequence++;
366
435
  }
367
436
  }
368
437
 
369
438
  /**
370
439
  * This message is sent by EDGE AutomergeReplicator when the authorization is denied.
371
440
  */
372
- const isForbiddenErrorMessage = (message: AutomergeProtocolMessage) =>
373
- message.type === 'error' && message.message === 'Forbidden';
441
+ const isErrorMessage = (message: AutomergeProtocolMessage) => message.type === 'error';
442
+
443
+ const getMessageInfo = (msg: AutomergeProtocolMessage) => {
444
+ const { have, heads, need, changes } = msg.type === 'sync' ? Automerge.decodeSyncMessage(msg.data) : {};
445
+ return {
446
+ type: msg.type,
447
+ documentId: 'documentId' in msg ? msg.documentId : undefined,
448
+ collectionId: 'collectionId' in msg ? msg.collectionId : undefined,
449
+ have,
450
+ heads,
451
+ need,
452
+ changes: changes?.length,
453
+ sequence: msg.metadata?.dxos_sequence,
454
+ };
455
+ };
@@ -4,69 +4,88 @@
4
4
 
5
5
  import { describe, expect, test } from 'vitest';
6
6
 
7
- import { Filter } from '@dxos/echo';
7
+ import { Filter, Ref } from '@dxos/echo';
8
+ import { TestSchema } from '@dxos/echo/testing';
8
9
  import { ObjectStructure } from '@dxos/echo-protocol';
9
- import { Expando, EXPANDO_TYPENAME, Ref } from '@dxos/echo-schema';
10
10
  import { DXN, ObjectId, SpaceId } from '@dxos/keys';
11
11
 
12
- import { filterMatchObject, type MatchedObject } from './filter-match';
12
+ import { type MatchedObject, filterMatchObject } from './filter-match';
13
13
 
14
14
  describe('filterMatch', () => {
15
+ test('everything', () => {
16
+ expect(filterMatchObject(Filter.everything().ast, OBJECT_1)).to.be.true;
17
+ expect(filterMatchObject(Filter.everything().ast, OBJECT_2)).to.be.true;
18
+ });
19
+
20
+ test('nothing', () => {
21
+ expect(filterMatchObject(Filter.nothing().ast, OBJECT_1)).to.be.false;
22
+ expect(filterMatchObject(Filter.nothing().ast, OBJECT_2)).to.be.false;
23
+ });
24
+
15
25
  test('properties', () => {
16
- expect(filterMatchObject(Filter.type(Expando, { title: 'test' }).ast, OBJECT_1)).to.be.true;
17
- expect(filterMatchObject(Filter.type(Expando, { value: 100 }).ast, OBJECT_1)).to.be.true;
18
- expect(filterMatchObject(Filter.type(Expando, { complete: false }).ast, OBJECT_1)).to.be.false;
19
- expect(filterMatchObject(Filter.type(Expando, { missing: undefined }).ast, OBJECT_1)).to.be.true;
26
+ expect(filterMatchObject(Filter.type(TestSchema.Expando, { title: 'test' }).ast, OBJECT_1)).to.be.true;
27
+ expect(filterMatchObject(Filter.type(TestSchema.Expando, { value: 100 }).ast, OBJECT_1)).to.be.true;
28
+ expect(filterMatchObject(Filter.type(TestSchema.Expando, { complete: false }).ast, OBJECT_1)).to.be.false;
29
+ expect(filterMatchObject(Filter.type(TestSchema.Expando, { missing: undefined }).ast, OBJECT_1)).to.be.true;
30
+ expect(filterMatchObject(Filter.type(TestSchema.Expando, { properties: { subject: 'test' } }).ast, OBJECT_1)).to.be
31
+ .true;
32
+ expect(filterMatchObject(Filter.type(TestSchema.Expando, { array: Filter.contains('two') }).ast, OBJECT_1)).to.be
33
+ .true;
20
34
  });
21
35
 
22
36
  test('and', () => {
23
- const filter1 = Filter.type(Expando, { title: 'test' });
24
- const filter2 = Filter.type(Expando, { value: 100 });
25
- const filter3 = Filter.type(Expando, { complete: true });
37
+ const filter1 = Filter.type(TestSchema.Expando, { title: 'test' });
38
+ const filter2 = Filter.type(TestSchema.Expando, { value: 100 });
39
+ const filter3 = Filter.type(TestSchema.Expando, { complete: true });
26
40
 
27
41
  expect(filterMatchObject(Filter.and(filter1, filter2, filter3).ast, OBJECT_1)).to.be.true;
28
42
  });
29
43
 
30
44
  test('or', () => {
31
- const filter1 = Filter.type(Expando, { value: 200 });
32
- const filter2 = Filter.type(Expando, { title: 'test' });
33
- const filter3 = Filter.type(Expando, { complete: false });
45
+ const filter1 = Filter.type(TestSchema.Expando, { value: 200 });
46
+ const filter2 = Filter.type(TestSchema.Expando, { title: 'test' });
47
+ const filter3 = Filter.type(TestSchema.Expando, { complete: false });
34
48
 
35
49
  expect(filterMatchObject(Filter.or(filter1, filter2, filter3).ast, OBJECT_1)).to.be.true;
36
50
  });
37
51
 
38
52
  test('not', () => {
39
- const filter1 = Filter.type(Expando, { title: 'test' });
53
+ const filter1 = Filter.type(TestSchema.Expando, { title: 'test' });
40
54
  const filter2 = Filter.not(filter1);
41
55
 
42
56
  expect(filterMatchObject(filter1.ast, OBJECT_1)).to.be.true;
43
57
  expect(filterMatchObject(filter2.ast, OBJECT_1)).to.be.false;
44
58
  });
45
59
 
60
+ test('contains', () => {
61
+ expect(
62
+ filterMatchObject(
63
+ Filter.type(TestSchema.Expando, { properties: { label: Filter.contains('test') } }).ast,
64
+ OBJECT_1,
65
+ ),
66
+ ).to.be.true;
67
+ expect(
68
+ filterMatchObject(
69
+ Filter.type(TestSchema.Expando, { fields: Filter.contains({ label: 'label', value: 'test' }) }).ast,
70
+ OBJECT_1,
71
+ ),
72
+ ).to.be.true;
73
+ });
74
+
46
75
  test('complex', () => {
47
- const filter1 = Filter.type(Expando, { title: 'bad' });
48
- const filter2 = Filter.type(Expando, { value: 0 });
76
+ const filter1 = Filter.type(TestSchema.Expando, { title: 'bad' });
77
+ const filter2 = Filter.type(TestSchema.Expando, { value: 0 });
49
78
  expect(filterMatchObject(Filter.not(Filter.or(filter1, filter2)).ast, OBJECT_1)).to.be.true;
50
79
  });
51
80
 
52
81
  test('ids', () => {
53
- const filter = Filter.ids(OBJECT_1.id);
82
+ const filter = Filter.id(OBJECT_1.id);
54
83
  expect(filterMatchObject(filter.ast, OBJECT_1)).to.be.true;
55
84
  expect(filterMatchObject(filter.ast, OBJECT_2)).to.be.false;
56
85
  });
57
86
 
58
- test('everything', () => {
59
- expect(filterMatchObject(Filter.everything().ast, OBJECT_1)).to.be.true;
60
- expect(filterMatchObject(Filter.everything().ast, OBJECT_2)).to.be.true;
61
- });
62
-
63
- test('nothing', () => {
64
- expect(filterMatchObject(Filter.nothing().ast, OBJECT_1)).to.be.false;
65
- expect(filterMatchObject(Filter.nothing().ast, OBJECT_2)).to.be.false;
66
- });
67
-
68
87
  test('refs', () => {
69
- const filter = Filter.type(Expando, { parent: Ref.fromDXN(DXN.fromLocalObjectId(OBJECT_1.id)) });
88
+ const filter = Filter.type(TestSchema.Expando, { parent: Ref.fromDXN(DXN.fromLocalObjectId(OBJECT_1.id)) });
70
89
  expect(filterMatchObject(filter.ast, OBJECT_1)).to.be.false;
71
90
  expect(filterMatchObject(filter.ast, OBJECT_2)).to.be.false;
72
91
  expect(filterMatchObject(filter.ast, OBJECT_3)).to.be.true;
@@ -77,8 +96,15 @@ const OBJECT_1: MatchedObject = {
77
96
  id: ObjectId.make('01JVS9YYT5VMVJW0GGTM1YHCCH'),
78
97
  spaceId: SpaceId.make('B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO'),
79
98
  doc: ObjectStructure.makeObject({
80
- type: DXN.fromTypenameAndVersion(EXPANDO_TYPENAME, '0.1.0').toString(),
81
- data: { title: 'test', value: 100, complete: true },
99
+ type: DXN.fromTypenameAndVersion(TestSchema.Expando.typename, '0.1.0').toString(),
100
+ data: {
101
+ title: 'test',
102
+ value: 100,
103
+ complete: true,
104
+ array: ['one', 'two', 'three'],
105
+ properties: { label: ['test'], subject: 'test' },
106
+ fields: [{ label: 'label', value: 'test' }],
107
+ },
82
108
  }),
83
109
  };
84
110
 
@@ -86,7 +112,7 @@ const OBJECT_2: MatchedObject = {
86
112
  id: ObjectId.make('01JT5TD6K9FFJ3VNM5FGMS5C0Q'),
87
113
  spaceId: SpaceId.make('B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO'),
88
114
  doc: ObjectStructure.makeObject({
89
- type: DXN.fromTypenameAndVersion(EXPANDO_TYPENAME, '0.1.0').toString(),
115
+ type: DXN.fromTypenameAndVersion(TestSchema.Expando.typename, '0.1.0').toString(),
90
116
  data: { title: 'test', value: 100, complete: true },
91
117
  deleted: true,
92
118
  }),
@@ -96,7 +122,7 @@ const OBJECT_3: MatchedObject = {
96
122
  id: ObjectId.make('01JT5TD6K9FFJ3VNM5FGMS5C0Q'),
97
123
  spaceId: SpaceId.make('B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO'),
98
124
  doc: ObjectStructure.makeObject({
99
- type: DXN.fromTypenameAndVersion(EXPANDO_TYPENAME, '0.1.0').toString(),
125
+ type: DXN.fromTypenameAndVersion(TestSchema.Expando.typename, '0.1.0').toString(),
100
126
  data: { title: 'test', value: 100, complete: true, parent: { '/': DXN.fromLocalObjectId(OBJECT_1.id).toString() } },
101
127
  }),
102
128
  };