@dxos/echo-pipeline 0.6.12 → 0.6.13-main.548ca8d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/chunk-PESZVYAN.mjs +2050 -0
- package/dist/lib/browser/chunk-PESZVYAN.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +3463 -17
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +3 -4
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-7HHYCGUR.cjs → chunk-6EZVIJNE.cjs} +89 -47
- package/dist/lib/node/chunk-6EZVIJNE.cjs.map +7 -0
- package/dist/lib/node/index.cjs +3440 -35
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +11 -12
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/lib/{browser/chunk-UKXIJW43.mjs → node-esm/chunk-4LW7MDPZ.mjs} +76 -36
- package/dist/lib/node-esm/chunk-4LW7MDPZ.mjs.map +7 -0
- package/dist/lib/{browser/chunk-MPWFDDQK.mjs → node-esm/index.mjs} +1702 -335
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/testing/index.mjs +551 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host.d.ts +24 -1
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/collection-synchronizer.d.ts +2 -0
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +3 -3
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +3 -3
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/space-collection.d.ts +3 -2
- package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
- package/dist/types/src/db-host/automerge-metrics.d.ts +11 -0
- package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -0
- package/dist/types/src/db-host/data-service.d.ts +3 -2
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +20 -0
- package/dist/types/src/db-host/database-root.d.ts.map +1 -0
- package/dist/types/src/db-host/documents-iterator.d.ts +7 -0
- package/dist/types/src/db-host/documents-iterator.d.ts.map +1 -0
- package/dist/types/src/db-host/echo-host.d.ts +73 -0
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -0
- package/dist/types/src/db-host/index.d.ts +5 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/migration.d.ts +8 -0
- package/dist/types/src/db-host/migration.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +25 -0
- package/dist/types/src/db-host/query-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-state.d.ts +41 -0
- package/dist/types/src/db-host/query-state.d.ts.map +1 -0
- package/dist/types/src/db-host/space-state-manager.d.ts +23 -0
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +23 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.test.d.ts +2 -0
- package/dist/types/src/edge/echo-edge-replicator.test.d.ts.map +1 -0
- package/dist/types/src/edge/index.d.ts +2 -0
- package/dist/types/src/edge/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +4 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-replicator.d.ts +4 -4
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/package.json +40 -50
- package/src/automerge/automerge-host.test.ts +8 -9
- package/src/automerge/automerge-host.ts +46 -7
- package/src/automerge/automerge-repo.test.ts +18 -16
- package/src/automerge/collection-synchronizer.test.ts +10 -5
- package/src/automerge/collection-synchronizer.ts +17 -6
- package/src/automerge/echo-data-monitor.test.ts +1 -3
- package/src/automerge/echo-network-adapter.test.ts +4 -3
- package/src/automerge/echo-network-adapter.ts +5 -4
- package/src/automerge/echo-replicator.ts +3 -3
- package/src/automerge/mesh-echo-replicator-connection.ts +10 -9
- package/src/automerge/mesh-echo-replicator.ts +2 -1
- package/src/automerge/space-collection.ts +3 -2
- package/src/automerge/storage-adapter.test.ts +2 -3
- package/src/db-host/automerge-metrics.ts +38 -0
- package/src/db-host/data-service.ts +29 -14
- package/src/db-host/database-root.ts +86 -0
- package/src/db-host/documents-iterator.ts +73 -0
- package/src/db-host/documents-synchronizer.test.ts +2 -2
- package/src/db-host/echo-host.ts +257 -0
- package/src/db-host/index.ts +6 -1
- package/src/db-host/migration.ts +57 -0
- package/src/db-host/query-service.ts +208 -0
- package/src/db-host/query-state.ts +200 -0
- package/src/db-host/space-state-manager.ts +90 -0
- package/src/edge/echo-edge-replicator.test.ts +96 -0
- package/src/edge/echo-edge-replicator.ts +337 -0
- package/src/edge/index.ts +5 -0
- package/src/index.ts +1 -0
- package/src/metadata/metadata-store.ts +20 -0
- package/src/pipeline/pipeline-stress.test.ts +44 -47
- package/src/pipeline/pipeline.test.ts +3 -4
- package/src/space/control-pipeline.test.ts +2 -3
- package/src/space/control-pipeline.ts +10 -1
- package/src/space/replication.browser.test.ts +2 -8
- package/src/space/space-manager.browser.test.ts +6 -5
- package/src/space/space-protocol.browser.test.ts +29 -34
- package/src/space/space-protocol.test.ts +29 -27
- package/src/space/space.test.ts +28 -11
- package/src/testing/test-agent-builder.ts +2 -2
- package/src/testing/test-replicator.ts +3 -3
- package/dist/lib/browser/chunk-MPWFDDQK.mjs.map +0 -7
- package/dist/lib/browser/chunk-UKXIJW43.mjs.map +0 -7
- package/dist/lib/browser/chunk-XPCF2V5U.mjs +0 -31
- package/dist/lib/browser/chunk-XPCF2V5U.mjs.map +0 -7
- package/dist/lib/browser/light.mjs +0 -32
- package/dist/lib/browser/light.mjs.map +0 -7
- package/dist/lib/node/chunk-5DH4KR2S.cjs +0 -2148
- package/dist/lib/node/chunk-5DH4KR2S.cjs.map +0 -7
- package/dist/lib/node/chunk-7HHYCGUR.cjs.map +0 -7
- package/dist/lib/node/chunk-DZVH7HDD.cjs +0 -43
- package/dist/lib/node/chunk-DZVH7HDD.cjs.map +0 -7
- package/dist/lib/node/light.cjs +0 -52
- package/dist/lib/node/light.cjs.map +0 -7
- package/dist/types/src/light.d.ts +0 -4
- package/dist/types/src/light.d.ts.map +0 -1
- package/src/light.ts +0 -7
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Mutex, scheduleTask } from '@dxos/async';
|
|
6
|
+
import * as A from '@dxos/automerge/automerge';
|
|
7
|
+
import { cbor } from '@dxos/automerge/automerge-repo';
|
|
8
|
+
import { Context, Resource } from '@dxos/context';
|
|
9
|
+
import { randomUUID } from '@dxos/crypto';
|
|
10
|
+
import type { CollectionId } from '@dxos/echo-protocol';
|
|
11
|
+
import { type EdgeConnection } from '@dxos/edge-client';
|
|
12
|
+
import { invariant } from '@dxos/invariant';
|
|
13
|
+
import type { SpaceId } from '@dxos/keys';
|
|
14
|
+
import { log } from '@dxos/log';
|
|
15
|
+
import { EdgeService, type AutomergeProtocolMessage, type PeerId } from '@dxos/protocols';
|
|
16
|
+
import { buf } from '@dxos/protocols/buf';
|
|
17
|
+
import {
|
|
18
|
+
type Message as RouterMessage,
|
|
19
|
+
MessageSchema as RouterMessageSchema,
|
|
20
|
+
} from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
21
|
+
import { bufferToArray } from '@dxos/util';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
getSpaceIdFromCollectionId,
|
|
25
|
+
type EchoReplicator,
|
|
26
|
+
type EchoReplicatorContext,
|
|
27
|
+
type ReplicatorConnection,
|
|
28
|
+
type ShouldAdvertiseParams,
|
|
29
|
+
type ShouldSyncCollectionParams,
|
|
30
|
+
} from '../automerge';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Delay before restarting the connection after receiving a forbidden error.
|
|
34
|
+
*/
|
|
35
|
+
const RESTART_DELAY = 500;
|
|
36
|
+
|
|
37
|
+
export type EchoEdgeReplicatorParams = {
|
|
38
|
+
edgeConnection: EdgeConnection;
|
|
39
|
+
disableSharePolicy?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class EchoEdgeReplicator implements EchoReplicator {
|
|
43
|
+
private readonly _edgeConnection: EdgeConnection;
|
|
44
|
+
private readonly _mutex = new Mutex();
|
|
45
|
+
|
|
46
|
+
private _ctx?: Context = undefined;
|
|
47
|
+
private _context: EchoReplicatorContext | null = null;
|
|
48
|
+
private _connectedSpaces = new Set<SpaceId>();
|
|
49
|
+
private _connections = new Map<SpaceId, EdgeReplicatorConnection>();
|
|
50
|
+
private _sharePolicyEnabled = true;
|
|
51
|
+
|
|
52
|
+
constructor({ edgeConnection, disableSharePolicy }: EchoEdgeReplicatorParams) {
|
|
53
|
+
this._edgeConnection = edgeConnection;
|
|
54
|
+
this._sharePolicyEnabled = !disableSharePolicy;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async connect(context: EchoReplicatorContext): Promise<void> {
|
|
58
|
+
log.info('connect', { peerId: context.peerId, connectedSpaces: this._connectedSpaces.size });
|
|
59
|
+
this._context = context;
|
|
60
|
+
|
|
61
|
+
this._ctx = Context.default();
|
|
62
|
+
this._edgeConnection.reconnect.on(this._ctx, async () => {
|
|
63
|
+
using _guard = await this._mutex.acquire();
|
|
64
|
+
|
|
65
|
+
const spaces = [...this._connectedSpaces];
|
|
66
|
+
for (const connection of this._connections.values()) {
|
|
67
|
+
await connection.close();
|
|
68
|
+
}
|
|
69
|
+
this._connections.clear();
|
|
70
|
+
|
|
71
|
+
if (this._context !== null) {
|
|
72
|
+
for (const spaceId of spaces) {
|
|
73
|
+
await this._openConnection(spaceId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
for (const spaceId of this._connectedSpaces) {
|
|
79
|
+
await this._openConnection(spaceId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async disconnect(): Promise<void> {
|
|
84
|
+
using _guard = await this._mutex.acquire();
|
|
85
|
+
await this._ctx?.dispose();
|
|
86
|
+
|
|
87
|
+
for (const connection of this._connections.values()) {
|
|
88
|
+
await connection.close();
|
|
89
|
+
}
|
|
90
|
+
this._connections.clear();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async connectToSpace(spaceId: SpaceId) {
|
|
94
|
+
using _guard = await this._mutex.acquire();
|
|
95
|
+
|
|
96
|
+
this._connectedSpaces.add(spaceId);
|
|
97
|
+
|
|
98
|
+
// Check if AM-repo requested that we connect to remote peers.
|
|
99
|
+
if (this._context !== null) {
|
|
100
|
+
await this._openConnection(spaceId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async disconnectFromSpace(spaceId: SpaceId) {
|
|
105
|
+
using _guard = await this._mutex.acquire();
|
|
106
|
+
|
|
107
|
+
this._connectedSpaces.delete(spaceId);
|
|
108
|
+
|
|
109
|
+
const connection = this._connections.get(spaceId);
|
|
110
|
+
if (connection) {
|
|
111
|
+
await connection.close();
|
|
112
|
+
this._connections.delete(spaceId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async _openConnection(spaceId: SpaceId) {
|
|
117
|
+
invariant(this._context);
|
|
118
|
+
invariant(!this._connections.has(spaceId));
|
|
119
|
+
const connection = new EdgeReplicatorConnection({
|
|
120
|
+
edgeConnection: this._edgeConnection,
|
|
121
|
+
ownPeerId: this._context.peerId,
|
|
122
|
+
spaceId,
|
|
123
|
+
context: this._context,
|
|
124
|
+
sharedPolicyEnabled: this._sharePolicyEnabled,
|
|
125
|
+
onRemoteConnected: async () => {
|
|
126
|
+
this._context?.onConnectionOpen(connection);
|
|
127
|
+
},
|
|
128
|
+
onRemoteDisconnected: async () => {
|
|
129
|
+
this._context?.onConnectionClosed(connection);
|
|
130
|
+
},
|
|
131
|
+
onRestartRequested: async () => {
|
|
132
|
+
using _guard = await this._mutex.acquire();
|
|
133
|
+
|
|
134
|
+
const ctx = this._ctx;
|
|
135
|
+
await connection.close(); // Will call onRemoteDisconnected
|
|
136
|
+
this._connections.delete(spaceId);
|
|
137
|
+
if (ctx?.disposed) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await this._openConnection(spaceId);
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
this._connections.set(spaceId, connection);
|
|
144
|
+
|
|
145
|
+
await connection.open();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type EdgeReplicatorConnectionsParams = {
|
|
150
|
+
edgeConnection: EdgeConnection;
|
|
151
|
+
ownPeerId: string;
|
|
152
|
+
spaceId: SpaceId;
|
|
153
|
+
context: EchoReplicatorContext;
|
|
154
|
+
sharedPolicyEnabled: boolean;
|
|
155
|
+
onRemoteConnected: () => Promise<void>;
|
|
156
|
+
onRemoteDisconnected: () => Promise<void>;
|
|
157
|
+
onRestartRequested: () => Promise<void>;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
class EdgeReplicatorConnection extends Resource implements ReplicatorConnection {
|
|
161
|
+
private readonly _edgeConnection: EdgeConnection;
|
|
162
|
+
private _remotePeerId: string | null = null;
|
|
163
|
+
private readonly _ownPeerId: string;
|
|
164
|
+
private readonly _targetServiceId: string;
|
|
165
|
+
private readonly _spaceId: SpaceId;
|
|
166
|
+
private readonly _context: EchoReplicatorContext;
|
|
167
|
+
private readonly _sharedPolicyEnabled: boolean;
|
|
168
|
+
private readonly _onRemoteConnected: () => Promise<void>;
|
|
169
|
+
private readonly _onRemoteDisconnected: () => Promise<void>;
|
|
170
|
+
private readonly _onRestartRequested: () => Promise<void>;
|
|
171
|
+
|
|
172
|
+
private _readableStreamController!: ReadableStreamDefaultController<AutomergeProtocolMessage>;
|
|
173
|
+
private _restartScheduled = false;
|
|
174
|
+
|
|
175
|
+
public readable: ReadableStream<AutomergeProtocolMessage>;
|
|
176
|
+
public writable: WritableStream<AutomergeProtocolMessage>;
|
|
177
|
+
|
|
178
|
+
constructor({
|
|
179
|
+
edgeConnection,
|
|
180
|
+
ownPeerId,
|
|
181
|
+
spaceId,
|
|
182
|
+
context,
|
|
183
|
+
sharedPolicyEnabled,
|
|
184
|
+
onRemoteConnected,
|
|
185
|
+
onRemoteDisconnected,
|
|
186
|
+
onRestartRequested,
|
|
187
|
+
}: EdgeReplicatorConnectionsParams) {
|
|
188
|
+
super();
|
|
189
|
+
this._edgeConnection = edgeConnection;
|
|
190
|
+
this._ownPeerId = ownPeerId;
|
|
191
|
+
this._spaceId = spaceId;
|
|
192
|
+
this._context = context;
|
|
193
|
+
|
|
194
|
+
// Generate a unique peer id for every connection.
|
|
195
|
+
// This way automerge-repo will have separate sync states for every connection.
|
|
196
|
+
// This is important because the previous connection might have had some messages that failed to deliver
|
|
197
|
+
// abd if we don't clear the sync-state, automerge will not attempt to deliver them again.
|
|
198
|
+
this._remotePeerId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}-${randomUUID()}`;
|
|
199
|
+
this._targetServiceId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}`;
|
|
200
|
+
this._sharedPolicyEnabled = sharedPolicyEnabled;
|
|
201
|
+
this._onRemoteConnected = onRemoteConnected;
|
|
202
|
+
this._onRemoteDisconnected = onRemoteDisconnected;
|
|
203
|
+
this._onRestartRequested = onRestartRequested;
|
|
204
|
+
|
|
205
|
+
this.readable = new ReadableStream<AutomergeProtocolMessage>({
|
|
206
|
+
start: (controller) => {
|
|
207
|
+
this._readableStreamController = controller;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
this.writable = new WritableStream<AutomergeProtocolMessage>({
|
|
212
|
+
write: async (message: AutomergeProtocolMessage, controller) => {
|
|
213
|
+
await this._sendMessage(message);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
protected override async _open(ctx: Context): Promise<void> {
|
|
219
|
+
log('open');
|
|
220
|
+
// TODO: handle reconnects
|
|
221
|
+
this._ctx.onDispose(
|
|
222
|
+
this._edgeConnection.addListener((msg: RouterMessage) => {
|
|
223
|
+
this._onMessage(msg);
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
await this._onRemoteConnected();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
protected override async _close(): Promise<void> {
|
|
231
|
+
log('close');
|
|
232
|
+
this._readableStreamController.close();
|
|
233
|
+
await this._onRemoteDisconnected();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
get peerId(): string {
|
|
237
|
+
invariant(this._remotePeerId, 'Not connected');
|
|
238
|
+
return this._remotePeerId;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean> {
|
|
242
|
+
if (!this._sharedPolicyEnabled) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
const spaceId = await this._context.getContainingSpaceIdForDocument(params.documentId);
|
|
246
|
+
if (!spaceId) {
|
|
247
|
+
// There's no spaceId if the document is not present locally. This means the sharePolicy check is being
|
|
248
|
+
// performed on message reception, so spaceId check was already performed in _onMessage.
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
return spaceId === this._spaceId;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
shouldSyncCollection(params: ShouldSyncCollectionParams): boolean {
|
|
255
|
+
if (!this._sharedPolicyEnabled) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
const spaceId = getSpaceIdFromCollectionId(params.collectionId as CollectionId);
|
|
259
|
+
return spaceId === this._spaceId;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private _onMessage(message: RouterMessage) {
|
|
263
|
+
if (message.serviceId !== this._targetServiceId) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const payload = cbor.decode(message.payload!.value) as AutomergeProtocolMessage;
|
|
268
|
+
log('recv', () => {
|
|
269
|
+
const decodedData =
|
|
270
|
+
payload.type === 'sync' && payload.data
|
|
271
|
+
? A.decodeSyncMessage(payload.data)
|
|
272
|
+
: payload.type === 'collection-state'
|
|
273
|
+
? (payload as any).state
|
|
274
|
+
: payload;
|
|
275
|
+
return { from: message.serviceId, type: payload.type, decodedData };
|
|
276
|
+
});
|
|
277
|
+
// Fix the peer id.
|
|
278
|
+
payload.senderId = this._remotePeerId! as PeerId;
|
|
279
|
+
this._processMessage(payload);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private _processMessage(message: AutomergeProtocolMessage) {
|
|
283
|
+
// There's a race between the credentials being replicated that are needed for access control and the data replication.
|
|
284
|
+
// AutomergeReplicator might return a Forbidden error if the credentials are not yet replicated.
|
|
285
|
+
// We restart the connection with some delay to account for that.
|
|
286
|
+
if (isForbiddenErrorMessage(message)) {
|
|
287
|
+
if (!this._restartScheduled) {
|
|
288
|
+
log.warn('Forbidden error received, replicator will restart the connection', {
|
|
289
|
+
spaceId: this._spaceId,
|
|
290
|
+
delayMs: RESTART_DELAY,
|
|
291
|
+
remotePeerId: this._remotePeerId,
|
|
292
|
+
});
|
|
293
|
+
this._restartScheduled = true;
|
|
294
|
+
scheduleTask(
|
|
295
|
+
this._ctx,
|
|
296
|
+
async () => {
|
|
297
|
+
await this._onRestartRequested();
|
|
298
|
+
},
|
|
299
|
+
RESTART_DELAY,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this._readableStreamController.enqueue(message);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async _sendMessage(message: AutomergeProtocolMessage) {
|
|
309
|
+
// Fix the peer id.
|
|
310
|
+
(message as any).targetId = this._targetServiceId as PeerId;
|
|
311
|
+
|
|
312
|
+
log('send', {
|
|
313
|
+
type: message.type,
|
|
314
|
+
senderId: message.senderId,
|
|
315
|
+
targetId: (message as any).targetId,
|
|
316
|
+
documentId: (message as any).documentId,
|
|
317
|
+
});
|
|
318
|
+
const encoded = cbor.encode(message);
|
|
319
|
+
|
|
320
|
+
await this._edgeConnection.send(
|
|
321
|
+
buf.create(RouterMessageSchema, {
|
|
322
|
+
serviceId: this._targetServiceId,
|
|
323
|
+
source: {
|
|
324
|
+
identityKey: this._edgeConnection.identityKey,
|
|
325
|
+
peerKey: this._edgeConnection.peerKey,
|
|
326
|
+
},
|
|
327
|
+
payload: { value: bufferToArray(encoded) },
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* This message is sent by EDGE AutomergeReplicator when the authorization is denied.
|
|
335
|
+
*/
|
|
336
|
+
const isForbiddenErrorMessage = (message: AutomergeProtocolMessage) =>
|
|
337
|
+
message.type === 'error' && message.message === 'Forbidden';
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type IdentityRecord,
|
|
21
21
|
type SpaceCache,
|
|
22
22
|
type LargeSpaceMetadata,
|
|
23
|
+
type EdgeReplicationSetting,
|
|
23
24
|
} from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
24
25
|
import { type Directory, type File } from '@dxos/random-access-storage';
|
|
25
26
|
import { type Timeframe } from '@dxos/timeframe';
|
|
@@ -234,6 +235,15 @@ export class MetadataStore {
|
|
|
234
235
|
return space;
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
hasSpace(spaceKey: PublicKey): boolean {
|
|
239
|
+
if (this._metadata.identity?.haloSpace.key.equals(spaceKey)) {
|
|
240
|
+
// Check if the space is the identity space.
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return !!this.spaces.find((space) => space.key === spaceKey);
|
|
245
|
+
}
|
|
246
|
+
|
|
237
247
|
private _getLargeSpaceMetadata(key: PublicKey): LargeSpaceMetadata {
|
|
238
248
|
let entry = this._spaceLargeMetadata.get(key);
|
|
239
249
|
if (entry) {
|
|
@@ -336,6 +346,16 @@ export class MetadataStore {
|
|
|
336
346
|
await this._saveSpaceLargeMetadata(spaceKey);
|
|
337
347
|
await this.flush();
|
|
338
348
|
}
|
|
349
|
+
|
|
350
|
+
getSpaceEdgeReplicationSetting(spaceKey: PublicKey): EdgeReplicationSetting | undefined {
|
|
351
|
+
return this.hasSpace(spaceKey) ? this._getSpace(spaceKey).edgeReplication : undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async setSpaceEdgeReplicationSetting(spaceKey: PublicKey, setting: EdgeReplicationSetting) {
|
|
355
|
+
this._getSpace(spaceKey).edgeReplication = setting;
|
|
356
|
+
await this._save();
|
|
357
|
+
await this.flush();
|
|
358
|
+
}
|
|
339
359
|
}
|
|
340
360
|
|
|
341
361
|
const fromBytesInt32 = (buf: Buffer) => buf.readInt32LE(0);
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import expect from 'expect';
|
|
6
5
|
import * as fc from 'fast-check';
|
|
7
|
-
import { inspect } from 'util';
|
|
6
|
+
import { inspect } from 'node:util';
|
|
7
|
+
import { describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
9
|
import { asyncTimeout } from '@dxos/async';
|
|
10
10
|
import { type FeedStore, type FeedWrapper } from '@dxos/feed-store';
|
|
@@ -12,7 +12,6 @@ import { PublicKey } from '@dxos/keys';
|
|
|
12
12
|
import { log } from '@dxos/log';
|
|
13
13
|
import { type FeedMessageBlock } from '@dxos/protocols';
|
|
14
14
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
15
|
-
import { describe, test } from '@dxos/test';
|
|
16
15
|
import { Timeframe } from '@dxos/timeframe';
|
|
17
16
|
import { range } from '@dxos/util';
|
|
18
17
|
|
|
@@ -24,53 +23,51 @@ const NUM_MESSAGES = 10;
|
|
|
24
23
|
|
|
25
24
|
// TODO(burdon): Describe test.
|
|
26
25
|
describe('pipeline/stress test', () => {
|
|
27
|
-
test
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
|
|
60
|
-
} finally {
|
|
61
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
|
|
62
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
|
|
63
|
-
}
|
|
26
|
+
test.skip('stress', { timeout: 60_000 }, async () => {
|
|
27
|
+
const builder = new TestFeedBuilder();
|
|
28
|
+
|
|
29
|
+
const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
|
|
30
|
+
const anAgentId = fc.constantFrom(...agentIds);
|
|
31
|
+
|
|
32
|
+
const commands = fc.commands(
|
|
33
|
+
[
|
|
34
|
+
fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
|
|
35
|
+
fc.constant(new SyncCommand()),
|
|
36
|
+
anAgentId.map((agent) => new RestartCommand(agent)),
|
|
37
|
+
],
|
|
38
|
+
{ size: 'large' },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const model = fc.asyncProperty(commands, async (commands) => {
|
|
42
|
+
const feedStore = builder.createFeedStore();
|
|
43
|
+
|
|
44
|
+
const agents = new Map(agentIds.map((id) => [id, new Agent(builder, feedStore, id)]));
|
|
45
|
+
await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
|
|
46
|
+
await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
|
|
47
|
+
|
|
48
|
+
const setup: fc.ModelRunSetup<Model, Real> = () => ({
|
|
49
|
+
model: {},
|
|
50
|
+
real: {
|
|
51
|
+
feedStore,
|
|
52
|
+
agents,
|
|
53
|
+
},
|
|
64
54
|
});
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
try {
|
|
57
|
+
await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
|
|
58
|
+
} finally {
|
|
59
|
+
await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
|
|
60
|
+
await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
|
|
65
|
+
[[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
|
|
66
|
+
[[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]],
|
|
67
|
+
];
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.timeout(60_000);
|
|
69
|
+
await fc.assert(model, { examples });
|
|
70
|
+
});
|
|
74
71
|
});
|
|
75
72
|
|
|
76
73
|
class Agent {
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import expect from '
|
|
5
|
+
import { describe, expect, test, onTestFinished } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { Event, sleep } from '@dxos/async';
|
|
8
8
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
9
|
-
import { describe, test, afterTest } from '@dxos/test';
|
|
10
9
|
import { Timeframe } from '@dxos/timeframe';
|
|
11
10
|
import { range } from '@dxos/util';
|
|
12
11
|
|
|
@@ -62,7 +61,7 @@ describe('pipeline/Pipeline', () => {
|
|
|
62
61
|
|
|
63
62
|
test('reading and writing with cursor changes', async () => {
|
|
64
63
|
const pipeline = new Pipeline();
|
|
65
|
-
|
|
64
|
+
onTestFinished(() => pipeline.stop());
|
|
66
65
|
|
|
67
66
|
const builder = new TestFeedBuilder();
|
|
68
67
|
const feedStore = builder.createFeedStore();
|
|
@@ -105,7 +104,7 @@ describe('pipeline/Pipeline', () => {
|
|
|
105
104
|
|
|
106
105
|
test('cursor change while polling', async () => {
|
|
107
106
|
const pipeline = new Pipeline();
|
|
108
|
-
|
|
107
|
+
onTestFinished(() => pipeline.stop());
|
|
109
108
|
|
|
110
109
|
const builder = new TestFeedBuilder();
|
|
111
110
|
const feedStore = builder.createFeedStore();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import expect from '
|
|
5
|
+
import { describe, expect, test, onTestFinished } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { CredentialGenerator, createCredential } from '@dxos/credentials';
|
|
8
8
|
import { FeedFactory, FeedStore } from '@dxos/feed-store';
|
|
@@ -12,7 +12,6 @@ import { log } from '@dxos/log';
|
|
|
12
12
|
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
13
13
|
import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
14
14
|
import { createStorage, StorageType } from '@dxos/random-access-storage';
|
|
15
|
-
import { describe, test, afterTest } from '@dxos/test';
|
|
16
15
|
import { Timeframe } from '@dxos/timeframe';
|
|
17
16
|
|
|
18
17
|
import { ControlPipeline } from './control-pipeline';
|
|
@@ -62,7 +61,7 @@ describe('space/control-pipeline', () => {
|
|
|
62
61
|
await controlPipeline.setWriteFeed(genesisFeed);
|
|
63
62
|
await controlPipeline.start();
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
onTestFinished(() => controlPipeline.stop());
|
|
66
65
|
|
|
67
66
|
//
|
|
68
67
|
// Genesis
|
|
@@ -160,7 +160,7 @@ export class ControlPipeline {
|
|
|
160
160
|
};
|
|
161
161
|
await this._pipeline.unpause();
|
|
162
162
|
|
|
163
|
-
log('save snapshot', { key: this._spaceKey, snapshot });
|
|
163
|
+
log('save snapshot', { key: this._spaceKey, snapshot: getSnapshotLoggerContext(snapshot) });
|
|
164
164
|
await this._metadata.setSpaceControlPipelineSnapshot(this._spaceKey, snapshot);
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -229,3 +229,12 @@ export class ControlPipeline {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
+
|
|
233
|
+
const getSnapshotLoggerContext = (snapshot: ControlPipelineSnapshot) => {
|
|
234
|
+
return snapshot.messages?.map((msg) => {
|
|
235
|
+
const issuer = msg.credential.issuer;
|
|
236
|
+
const subject = msg.credential.subject.id;
|
|
237
|
+
const type = msg.credential.subject.assertion['@type'];
|
|
238
|
+
return { issuer, subject, type };
|
|
239
|
+
});
|
|
240
|
+
};
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
// Copyright 2021 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import expect from 'expect';
|
|
8
|
-
import waitForExpect from 'wait-for-expect';
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
9
6
|
|
|
10
7
|
import { FeedFactory, FeedStore } from '@dxos/feed-store';
|
|
11
8
|
import { Keyring } from '@dxos/keyring';
|
|
12
9
|
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
13
10
|
import { createStorage } from '@dxos/random-access-storage';
|
|
14
|
-
import { describe, test } from '@dxos/test';
|
|
15
11
|
import { Timeframe } from '@dxos/timeframe';
|
|
16
12
|
|
|
17
13
|
import { valueEncoding } from '../common';
|
|
@@ -57,8 +53,6 @@ describe('replication', () => {
|
|
|
57
53
|
timeframe: new Timeframe([[feed1.key, 123]]),
|
|
58
54
|
});
|
|
59
55
|
|
|
60
|
-
await
|
|
61
|
-
expect(feed2.properties.length).toEqual(1);
|
|
62
|
-
});
|
|
56
|
+
await expect.poll(() => feed2.properties.length).toEqual(1);
|
|
63
57
|
});
|
|
64
58
|
});
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { describe, test, onTestFinished } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { createStorage } from '@dxos/random-access-storage';
|
|
8
|
-
import { describe, test, afterTest } from '@dxos/test';
|
|
9
8
|
|
|
10
9
|
import { TestAgentBuilder, WebsocketNetworkManagerProvider } from '../testing';
|
|
11
10
|
|
|
@@ -20,7 +19,9 @@ describe('space-manager', () => {
|
|
|
20
19
|
storage: createStorage(),
|
|
21
20
|
networkManagerProvider: WebsocketNetworkManagerProvider(SIGNAL_URL),
|
|
22
21
|
});
|
|
23
|
-
|
|
22
|
+
onTestFinished(async () => {
|
|
23
|
+
await builder.close();
|
|
24
|
+
});
|
|
24
25
|
|
|
25
26
|
const peer1 = await builder.createPeer();
|
|
26
27
|
const spaceManager1 = peer1.spaceManager;
|
|
@@ -30,8 +31,8 @@ describe('space-manager', () => {
|
|
|
30
31
|
const spaceManager2 = peer2.spaceManager;
|
|
31
32
|
await spaceManager2.open();
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
onTestFinished(() => spaceManager1.close());
|
|
35
|
+
onTestFinished(() => spaceManager2.close());
|
|
35
36
|
|
|
36
37
|
// const space1 = await spaceManager1.createSpace();
|
|
37
38
|
// expect(space1.isOpen).to.be.true;
|