@dxos/client-services 0.1.31-next.ea8f0bc → 0.1.32-next.77d513e
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-ZIQUYEJS.mjs → chunk-E3FD43YN.mjs} +460 -213
- package/dist/lib/browser/chunk-E3FD43YN.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +64 -6
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +462 -215
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +524 -218
- package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
- package/dist/types/src/packlets/identity/identity-manager.d.ts +6 -0
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +1 -0
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +19 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +32 -5
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +29 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +4 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/host-test-builder.d.ts +2 -2
- package/dist/types/src/packlets/testing/host-test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +2 -0
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/tests/text.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/text.test.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/vault-resource-lock.d.ts +1 -0
- package/dist/types/src/packlets/vault/vault-resource-lock.d.ts.map +1 -1
- package/package.json +31 -30
- package/src/packlets/identity/identity-manager.ts +32 -17
- package/src/packlets/invitations/device-invitations-handler.ts +5 -3
- package/src/packlets/invitations/space-invitations-handler.test.ts +20 -3
- package/src/packlets/invitations/space-invitations-handler.ts +15 -8
- package/src/packlets/services/service-context.test.ts +6 -5
- package/src/packlets/services/service-host.ts +7 -2
- package/src/packlets/spaces/data-space-manager.test.ts +77 -2
- package/src/packlets/spaces/data-space-manager.ts +87 -37
- package/src/packlets/spaces/data-space.ts +103 -16
- package/src/packlets/spaces/notarization-plugin.test.ts +3 -1
- package/src/packlets/spaces/notarization-plugin.ts +67 -19
- package/src/packlets/spaces/spaces-service.test.ts +1 -1
- package/src/packlets/spaces/spaces-service.ts +53 -17
- package/src/packlets/testing/host-test-builder.ts +2 -2
- package/src/packlets/testing/test-builder.ts +45 -5
- package/src/packlets/tests/client-services.test.ts +24 -11
- package/src/packlets/tests/client.test.ts +3 -6
- package/src/packlets/tests/halo-profile.test.ts +5 -5
- package/src/packlets/tests/spaces-invitations.test.ts +47 -7
- package/src/packlets/tests/spaces.test.ts +86 -9
- package/src/packlets/tests/text.test.ts +206 -0
- package/src/packlets/vault/vault-resource-lock.ts +10 -1
- package/dist/lib/browser/chunk-ZIQUYEJS.mjs.map +0 -7
|
@@ -2,25 +2,29 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { trackLeaks } from '@dxos/async';
|
|
5
|
+
import { Event, scheduleTask, trackLeaks } from '@dxos/async';
|
|
6
|
+
import { SpaceState } from '@dxos/client';
|
|
6
7
|
import { Context } from '@dxos/context';
|
|
7
8
|
import { CredentialConsumer } from '@dxos/credentials';
|
|
8
9
|
import { timed } from '@dxos/debug';
|
|
9
10
|
import {
|
|
10
|
-
DataPipelineControllerImpl,
|
|
11
11
|
MetadataStore,
|
|
12
12
|
Space,
|
|
13
13
|
SigningContext,
|
|
14
14
|
createMappedFeedWriter,
|
|
15
|
-
SnapshotManager
|
|
15
|
+
SnapshotManager,
|
|
16
|
+
DataPipeline
|
|
16
17
|
} from '@dxos/echo-pipeline';
|
|
18
|
+
import { CancelledError, SystemError } from '@dxos/errors';
|
|
17
19
|
import { FeedStore } from '@dxos/feed-store';
|
|
18
20
|
import { Keyring } from '@dxos/keyring';
|
|
19
21
|
import { PublicKey } from '@dxos/keys';
|
|
22
|
+
import { log } from '@dxos/log';
|
|
20
23
|
import { ModelFactory } from '@dxos/model-factory';
|
|
21
24
|
import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
22
25
|
import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
23
|
-
import {
|
|
26
|
+
import { GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';
|
|
27
|
+
import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
|
|
24
28
|
import { ComplexSet } from '@dxos/util';
|
|
25
29
|
|
|
26
30
|
import { TrustedKeySetAuthVerifier } from '../identity';
|
|
@@ -28,42 +32,64 @@ import { NotarizationPlugin } from './notarization-plugin';
|
|
|
28
32
|
|
|
29
33
|
const AUTH_TIMEOUT = 30000;
|
|
30
34
|
|
|
35
|
+
export type DataSpaceCallbacks = {
|
|
36
|
+
/**
|
|
37
|
+
* Called before transitioning to the ready state.
|
|
38
|
+
*/
|
|
39
|
+
beforeReady?: () => Promise<void>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Called after transitioning to the ready state.
|
|
43
|
+
*/
|
|
44
|
+
afterReady?: () => Promise<void>;
|
|
45
|
+
};
|
|
46
|
+
|
|
31
47
|
export type DataSpaceParams = {
|
|
32
48
|
inner: Space;
|
|
33
49
|
modelFactory: ModelFactory;
|
|
34
50
|
metadataStore: MetadataStore;
|
|
35
51
|
snapshotManager: SnapshotManager;
|
|
52
|
+
gossip: Gossip;
|
|
36
53
|
presence: Presence;
|
|
37
54
|
keyring: Keyring;
|
|
38
55
|
feedStore: FeedStore<FeedMessage>;
|
|
39
56
|
signingContext: SigningContext;
|
|
40
57
|
memberKey: PublicKey;
|
|
41
58
|
snapshotId?: string | undefined;
|
|
59
|
+
callbacks?: DataSpaceCallbacks;
|
|
42
60
|
};
|
|
43
61
|
|
|
44
|
-
const CONTROL_PIPELINE_READY_TIMEFRAME = 3000;
|
|
45
62
|
@trackLeaks('open', 'close')
|
|
46
63
|
export class DataSpace {
|
|
47
64
|
private readonly _ctx = new Context();
|
|
48
|
-
private readonly
|
|
65
|
+
private readonly _dataPipeline: DataPipeline;
|
|
49
66
|
private readonly _inner: Space;
|
|
67
|
+
private readonly _gossip: Gossip;
|
|
50
68
|
private readonly _presence: Presence;
|
|
51
69
|
private readonly _keyring: Keyring;
|
|
52
70
|
private readonly _feedStore: FeedStore<FeedMessage>;
|
|
53
71
|
private readonly _metadataStore: MetadataStore;
|
|
54
72
|
private readonly _signingContext: SigningContext;
|
|
55
73
|
private readonly _notarizationPluginConsumer: CredentialConsumer<NotarizationPlugin>;
|
|
74
|
+
private readonly _callbacks: DataSpaceCallbacks;
|
|
75
|
+
|
|
76
|
+
private _state = SpaceState.CLOSED;
|
|
56
77
|
|
|
57
78
|
public readonly authVerifier: TrustedKeySetAuthVerifier;
|
|
79
|
+
public readonly stateUpdate = new Event();
|
|
58
80
|
|
|
59
81
|
constructor(params: DataSpaceParams) {
|
|
60
82
|
this._inner = params.inner;
|
|
83
|
+
this._inner.stateUpdate.on(this._ctx, () => this.stateUpdate.emit());
|
|
84
|
+
|
|
85
|
+
this._gossip = params.gossip;
|
|
61
86
|
this._presence = params.presence;
|
|
62
87
|
this._keyring = params.keyring;
|
|
63
88
|
this._feedStore = params.feedStore;
|
|
64
89
|
this._metadataStore = params.metadataStore;
|
|
65
90
|
this._signingContext = params.signingContext;
|
|
66
|
-
this.
|
|
91
|
+
this._callbacks = params.callbacks ?? {};
|
|
92
|
+
this._dataPipeline = new DataPipeline({
|
|
67
93
|
modelFactory: params.modelFactory,
|
|
68
94
|
metadataStore: params.metadataStore,
|
|
69
95
|
snapshotManager: params.snapshotManager,
|
|
@@ -90,17 +116,17 @@ export class DataSpace {
|
|
|
90
116
|
return this._inner.isOpen;
|
|
91
117
|
}
|
|
92
118
|
|
|
119
|
+
get state(): SpaceState {
|
|
120
|
+
return this._state;
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
// TODO(burdon): Can we mark this for debugging only?
|
|
94
124
|
get inner() {
|
|
95
125
|
return this._inner;
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
get
|
|
99
|
-
return this.
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
get stateUpdate() {
|
|
103
|
-
return this._inner.stateUpdate;
|
|
128
|
+
get dataPipeline(): DataPipeline {
|
|
129
|
+
return this._dataPipeline;
|
|
104
130
|
}
|
|
105
131
|
|
|
106
132
|
get presence() {
|
|
@@ -115,11 +141,14 @@ export class DataSpace {
|
|
|
115
141
|
await this.notarizationPlugin.open();
|
|
116
142
|
await this._notarizationPluginConsumer.open();
|
|
117
143
|
await this._inner.open();
|
|
144
|
+
this._state = SpaceState.INACTIVE;
|
|
118
145
|
}
|
|
119
146
|
|
|
120
147
|
async close() {
|
|
148
|
+
this._state = SpaceState.CLOSED;
|
|
121
149
|
await this._ctx.dispose();
|
|
122
150
|
|
|
151
|
+
await this._dataPipeline.close();
|
|
123
152
|
await this.authVerifier.close();
|
|
124
153
|
|
|
125
154
|
await this._inner.close();
|
|
@@ -129,12 +158,48 @@ export class DataSpace {
|
|
|
129
158
|
await this._presence.destroy();
|
|
130
159
|
}
|
|
131
160
|
|
|
161
|
+
async postMessage(channel: string, message: any) {
|
|
162
|
+
return this._gossip.postMessage(channel, message);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
listen(channel: string, callback: (message: GossipMessage) => void) {
|
|
166
|
+
return this._gossip.listen(channel, callback);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Initialize the data pipeline in a separate task.
|
|
171
|
+
*/
|
|
172
|
+
initializeDataPipelineAsync() {
|
|
173
|
+
scheduleTask(this._ctx, async () => {
|
|
174
|
+
try {
|
|
175
|
+
await this.initializeDataPipeline();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof CancelledError) {
|
|
178
|
+
log('Data pipeline initialization cancelled', err);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
log.error('Error initializing data pipeline', err);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
132
187
|
async initializeDataPipeline() {
|
|
188
|
+
if (this._state !== SpaceState.INACTIVE) {
|
|
189
|
+
throw new SystemError('Invalid operation');
|
|
190
|
+
}
|
|
191
|
+
this._state = SpaceState.INITIALIZING;
|
|
192
|
+
|
|
193
|
+
// TODO(dmaretskyi): Cancel with context.
|
|
133
194
|
await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
|
|
134
|
-
|
|
195
|
+
ctx: this._ctx,
|
|
196
|
+
breakOnStall: false
|
|
135
197
|
});
|
|
136
198
|
|
|
137
199
|
await this._createWritableFeeds();
|
|
200
|
+
log('Writable feeds created');
|
|
201
|
+
this.stateUpdate.emit();
|
|
202
|
+
|
|
138
203
|
this.notarizationPlugin.setWriter(
|
|
139
204
|
createMappedFeedWriter<Credential, FeedMessage.Payload>(
|
|
140
205
|
(credential) => ({
|
|
@@ -144,7 +209,28 @@ export class DataSpace {
|
|
|
144
209
|
)
|
|
145
210
|
);
|
|
146
211
|
|
|
147
|
-
await this.
|
|
212
|
+
await this._dataPipeline.open({
|
|
213
|
+
openPipeline: async (start) => {
|
|
214
|
+
const pipeline = await this._inner.createDataPipeline({ start });
|
|
215
|
+
await pipeline.start();
|
|
216
|
+
return pipeline;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
log('waiting for data pipeline to reach target timeframe');
|
|
221
|
+
// Wait for the data pipeline to catch up to its desired timeframe.
|
|
222
|
+
await this._dataPipeline.pipelineState!.waitUntilReachedTargetTimeframe({
|
|
223
|
+
ctx: this._ctx,
|
|
224
|
+
breakOnStall: false
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
log('data pipeline ready');
|
|
228
|
+
await this._callbacks.beforeReady?.();
|
|
229
|
+
|
|
230
|
+
this._state = SpaceState.READY;
|
|
231
|
+
this.stateUpdate.emit();
|
|
232
|
+
|
|
233
|
+
await this._callbacks.afterReady?.();
|
|
148
234
|
}
|
|
149
235
|
|
|
150
236
|
@timed(10_000)
|
|
@@ -186,7 +272,8 @@ export class DataSpace {
|
|
|
186
272
|
}
|
|
187
273
|
|
|
188
274
|
if (credentials.length > 0) {
|
|
189
|
-
|
|
275
|
+
// Never times out
|
|
276
|
+
await this.notarizationPlugin.notarize({ ctx: this._ctx, credentials, timeout: 0 });
|
|
190
277
|
}
|
|
191
278
|
|
|
192
279
|
// Set this after credentials are notarized so that on failure we will retry.
|
|
@@ -70,7 +70,9 @@ describe('NotarizationPlugin', () => {
|
|
|
70
70
|
AdmittedFeed.Designation.CONTROL
|
|
71
71
|
);
|
|
72
72
|
|
|
73
|
-
const notarized = peer2.notarizationPlugin.notarize(
|
|
73
|
+
const notarized = peer2.notarizationPlugin.notarize({
|
|
74
|
+
credentials: [credential]
|
|
75
|
+
});
|
|
74
76
|
|
|
75
77
|
await testBuilder.connect(peer1, peer2);
|
|
76
78
|
await notarized;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import assert from 'node:assert';
|
|
6
6
|
|
|
7
|
-
import { DeferredTask, Event, scheduleTask, TimeoutError, Trigger } from '@dxos/async';
|
|
8
|
-
import { Context } from '@dxos/context';
|
|
7
|
+
import { DeferredTask, Event, scheduleTask, sleep, TimeoutError, Trigger } from '@dxos/async';
|
|
8
|
+
import { Context, rejectOnDispose } from '@dxos/context';
|
|
9
9
|
import { CredentialProcessor } from '@dxos/credentials';
|
|
10
10
|
import { FeedWriter } from '@dxos/feed-store';
|
|
11
11
|
import { PublicKey } from '@dxos/keys';
|
|
@@ -16,8 +16,43 @@ import { NotarizationService, NotarizeRequest } from '@dxos/protocols/proto/dxos
|
|
|
16
16
|
import { ExtensionContext, RpcExtension } from '@dxos/teleport';
|
|
17
17
|
import { ComplexMap, ComplexSet, entry } from '@dxos/util';
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
const DEFAULT_RETRY_TIMEOUT = 1_000;
|
|
20
|
+
|
|
21
|
+
const DEFAULT_SUCCESS_DELAY = 1_000;
|
|
22
|
+
|
|
23
|
+
const DEFAULT_NOTARIZE_TIMEOUT = 10_000;
|
|
24
|
+
|
|
25
|
+
export type NotarizeParams = {
|
|
26
|
+
/**
|
|
27
|
+
* For cancellation.
|
|
28
|
+
*/
|
|
29
|
+
ctx?: Context;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Credentials to notarize.
|
|
33
|
+
*/
|
|
34
|
+
credentials: Credential[];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Timeout for the whole notarization process.
|
|
38
|
+
* Set to 0 to disable.
|
|
39
|
+
* @default {@link DEFAULT_NOTARIZE_TIMEOUT}
|
|
40
|
+
*/
|
|
41
|
+
timeout?: number;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retry timeout.
|
|
45
|
+
* @default {@link DEFAULT_RETRY_TIMEOUT}
|
|
46
|
+
*/
|
|
47
|
+
retryTimeout?: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Minimum wait time after a peer confirms successful notarization before attempting with a new peer.
|
|
51
|
+
* This is to avoid spamming peers with notarization requests.
|
|
52
|
+
* @default {@link DEFAULT_SUCCESS_DELAY}
|
|
53
|
+
*/
|
|
54
|
+
successDelay?: number;
|
|
55
|
+
};
|
|
21
56
|
|
|
22
57
|
/**
|
|
23
58
|
* See NotarizationService proto.
|
|
@@ -40,7 +75,13 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
40
75
|
/**
|
|
41
76
|
* Request credentials to be notarized.
|
|
42
77
|
*/
|
|
43
|
-
async notarize(
|
|
78
|
+
async notarize({
|
|
79
|
+
ctx: opCtx,
|
|
80
|
+
credentials,
|
|
81
|
+
timeout = DEFAULT_NOTARIZE_TIMEOUT,
|
|
82
|
+
retryTimeout = DEFAULT_RETRY_TIMEOUT,
|
|
83
|
+
successDelay = DEFAULT_SUCCESS_DELAY
|
|
84
|
+
}: NotarizeParams) {
|
|
44
85
|
log('notarize', { credentials });
|
|
45
86
|
assert(
|
|
46
87
|
credentials.every((credential) => credential.id),
|
|
@@ -55,15 +96,23 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
55
96
|
errors.throw(err);
|
|
56
97
|
}
|
|
57
98
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
99
|
+
opCtx?.onDispose(() => ctx.dispose());
|
|
100
|
+
|
|
101
|
+
// Timeout/
|
|
102
|
+
if (timeout !== 0) {
|
|
103
|
+
scheduleTask(
|
|
104
|
+
ctx,
|
|
105
|
+
() => {
|
|
106
|
+
log.warn('Notarization timeout', {
|
|
107
|
+
timeout,
|
|
108
|
+
peers: Array.from(this._extensions).map((extension) => extension.remotePeerId)
|
|
109
|
+
});
|
|
110
|
+
void ctx.dispose();
|
|
111
|
+
errors.throw(new TimeoutError(timeout, 'Notarization timed out'));
|
|
112
|
+
},
|
|
113
|
+
timeout
|
|
114
|
+
);
|
|
115
|
+
}
|
|
67
116
|
|
|
68
117
|
const allNotarized = Promise.all(credentials.map((credential) => this._waitUntilProcessed(credential.id!)));
|
|
69
118
|
|
|
@@ -73,16 +122,15 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
73
122
|
const notarizeTask = new DeferredTask(ctx, async () => {
|
|
74
123
|
try {
|
|
75
124
|
if (this._extensions.size === 0) {
|
|
76
|
-
log.warn('No peers to notarize with');
|
|
77
125
|
return; // No peers to try.
|
|
78
126
|
}
|
|
79
127
|
|
|
80
128
|
// Pick a peer that we haven't tried yet.
|
|
81
129
|
const peer = [...this._extensions].find((peer) => !peersTried.has(peer));
|
|
82
130
|
if (!peer) {
|
|
83
|
-
log.warn('Exhausted all peers to notarize with', { retryIn:
|
|
131
|
+
log.warn('Exhausted all peers to notarize with', { retryIn: retryTimeout });
|
|
84
132
|
peersTried.clear();
|
|
85
|
-
scheduleTask(ctx, () => notarizeTask.schedule(),
|
|
133
|
+
scheduleTask(ctx, () => notarizeTask.schedule(), retryTimeout); // retry with all peers again
|
|
86
134
|
return;
|
|
87
135
|
}
|
|
88
136
|
|
|
@@ -92,6 +140,7 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
92
140
|
credentials: credentials.filter((credential) => !this._processedCredentials.has(credential.id!))
|
|
93
141
|
});
|
|
94
142
|
log('success');
|
|
143
|
+
await sleep(successDelay); // wait before trying with a new peer
|
|
95
144
|
} catch (err) {
|
|
96
145
|
log.warn('error notarizing (recoverable)', err);
|
|
97
146
|
notarizeTask.schedule(); // retry immediately with next peer
|
|
@@ -102,8 +151,7 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
102
151
|
this._extensionOpened.on(ctx, () => notarizeTask.schedule());
|
|
103
152
|
|
|
104
153
|
try {
|
|
105
|
-
|
|
106
|
-
await Promise.race([allNotarized, errors.wait()]);
|
|
154
|
+
await Promise.race([rejectOnDispose(ctx), allNotarized, errors.wait()]);
|
|
107
155
|
log('done');
|
|
108
156
|
} finally {
|
|
109
157
|
await ctx.dispose();
|
|
@@ -81,7 +81,7 @@ describe('SpacesService', () => {
|
|
|
81
81
|
|
|
82
82
|
const spaces = await result.wait();
|
|
83
83
|
expect(spaces).to.be.length(3);
|
|
84
|
-
expect(spaces).to.deep.equal(existingSpaces);
|
|
84
|
+
expect(spaces?.map((s) => s.spaceKey)).to.deep.equal(existingSpaces?.map((s) => s.spaceKey));
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
test('updates when new space is added', async () => {
|
|
@@ -8,22 +8,26 @@ import { raise, todo } from '@dxos/debug';
|
|
|
8
8
|
import { DataServiceSubscriptions, SpaceManager, SpaceNotFoundError } from '@dxos/echo-pipeline';
|
|
9
9
|
import { log } from '@dxos/log';
|
|
10
10
|
import {
|
|
11
|
+
PostMessageRequest,
|
|
11
12
|
QueryCredentialsRequest,
|
|
12
13
|
QuerySpacesResponse,
|
|
13
14
|
Space,
|
|
14
15
|
SpaceMember,
|
|
15
16
|
SpacesService,
|
|
16
|
-
|
|
17
|
+
SubscribeMessagesRequest,
|
|
17
18
|
UpdateSpaceRequest,
|
|
18
19
|
WriteCredentialsRequest
|
|
19
20
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
20
21
|
import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
22
|
+
import { GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';
|
|
21
23
|
import { humanize, Provider } from '@dxos/util';
|
|
22
24
|
|
|
23
25
|
import { IdentityManager } from '../identity';
|
|
24
26
|
import { DataSpace } from './data-space';
|
|
25
27
|
import { DataSpaceManager } from './data-space-manager';
|
|
26
28
|
|
|
29
|
+
const TIMEFRAME_UPDATE_DEBOUNCE_TIME = 500;
|
|
30
|
+
|
|
27
31
|
/**
|
|
28
32
|
*
|
|
29
33
|
*/
|
|
@@ -52,29 +56,36 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
52
56
|
return new Stream<QuerySpacesResponse>(({ next, ctx }) => {
|
|
53
57
|
const onUpdate = async () => {
|
|
54
58
|
const dataSpaceManager = await this._getDataSpaceManager();
|
|
55
|
-
const spaces = Array.from(dataSpaceManager.spaces.values())
|
|
56
|
-
// Skip spaces without data service available.
|
|
57
|
-
.filter((space) => this._dataServiceSubscriptions.getDataService(space.key))
|
|
58
|
-
.map((space) => this._transformSpace(space));
|
|
59
|
+
const spaces = Array.from(dataSpaceManager.spaces.values()).map((space) => this._transformSpace(space));
|
|
59
60
|
log('update', { spaces });
|
|
60
61
|
next({ spaces });
|
|
61
62
|
};
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
scheduleTask(ctx, async () => {
|
|
64
65
|
const dataSpaceManager = await this._getDataSpaceManager();
|
|
66
|
+
|
|
65
67
|
const subscriptions = new EventSubscriptions();
|
|
68
|
+
ctx.onDispose(() => subscriptions.clear());
|
|
69
|
+
|
|
66
70
|
// TODO(dmaretskyi): Create a pattern for subscribing to a set of objects.
|
|
67
71
|
const subscribeSpaces = () => {
|
|
68
72
|
subscriptions.clear();
|
|
69
73
|
|
|
70
74
|
for (const space of dataSpaceManager.spaces.values()) {
|
|
71
|
-
if (!this._dataServiceSubscriptions.getDataService(space.key)) {
|
|
72
|
-
// Skip spaces without data service available.
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
75
|
subscriptions.add(space.stateUpdate.on(ctx, onUpdate));
|
|
77
76
|
subscriptions.add(space.presence.updated.on(ctx, onUpdate));
|
|
77
|
+
|
|
78
|
+
// Pipeline progress.
|
|
79
|
+
space.inner.controlPipeline.state.timeframeUpdate
|
|
80
|
+
.debounce(TIMEFRAME_UPDATE_DEBOUNCE_TIME)
|
|
81
|
+
.on(ctx, onUpdate);
|
|
82
|
+
if (space.dataPipeline.pipelineState) {
|
|
83
|
+
subscriptions.add(
|
|
84
|
+
space.dataPipeline.pipelineState.timeframeUpdate
|
|
85
|
+
.debounce(TIMEFRAME_UPDATE_DEBOUNCE_TIME)
|
|
86
|
+
.on(ctx, onUpdate)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
78
89
|
}
|
|
79
90
|
};
|
|
80
91
|
|
|
@@ -82,12 +93,9 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
82
93
|
subscribeSpaces();
|
|
83
94
|
void onUpdate();
|
|
84
95
|
});
|
|
96
|
+
subscribeSpaces();
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
scheduleTask(ctx, () => {
|
|
88
|
-
subscribeSpaces();
|
|
89
|
-
void onUpdate();
|
|
90
|
-
});
|
|
98
|
+
void onUpdate();
|
|
91
99
|
});
|
|
92
100
|
|
|
93
101
|
if (!this._identityManager.identity) {
|
|
@@ -96,6 +104,25 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
96
104
|
});
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
async postMessage({ spaceKey, channel, message }: PostMessageRequest) {
|
|
108
|
+
const dataSpaceManager = await this._getDataSpaceManager();
|
|
109
|
+
const space = dataSpaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
|
|
110
|
+
await space.postMessage(getChannelId(channel), message);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
subscribeMessages({ spaceKey, channel }: SubscribeMessagesRequest) {
|
|
114
|
+
return new Stream<GossipMessage>(({ ctx, next }) => {
|
|
115
|
+
scheduleTask(ctx, async () => {
|
|
116
|
+
const dataSpaceManager = await this._getDataSpaceManager();
|
|
117
|
+
const space = dataSpaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
|
|
118
|
+
const handle = space.listen(getChannelId(channel), (message) => {
|
|
119
|
+
next(message);
|
|
120
|
+
});
|
|
121
|
+
ctx.onDispose(() => handle.unsubscribe());
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
99
126
|
queryCredentials({ spaceKey }: QueryCredentialsRequest): Stream<Credential> {
|
|
100
127
|
return new Stream(({ ctx, next }) => {
|
|
101
128
|
const space = this._spaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
|
|
@@ -120,7 +147,13 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
120
147
|
private _transformSpace(space: DataSpace): Space {
|
|
121
148
|
return {
|
|
122
149
|
spaceKey: space.key,
|
|
123
|
-
|
|
150
|
+
state: space.state,
|
|
151
|
+
pipeline: {
|
|
152
|
+
targetControlTimeframe: space.inner.controlPipeline.state.targetTimeframe,
|
|
153
|
+
currentControlTimeframe: space.inner.controlPipeline.state.timeframe,
|
|
154
|
+
currentDataTimeframe: space.dataPipeline.pipelineState?.timeframe,
|
|
155
|
+
targetDataTimeframe: space.dataPipeline.pipelineState?.targetTimeframe
|
|
156
|
+
},
|
|
124
157
|
members: Array.from(space.inner.spaceState.members.values()).map((member) => ({
|
|
125
158
|
identity: {
|
|
126
159
|
identityKey: member.key,
|
|
@@ -137,3 +170,6 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
137
170
|
};
|
|
138
171
|
}
|
|
139
172
|
}
|
|
173
|
+
|
|
174
|
+
// Add `user-channel` prefix to the channel name, so that it doesn't collide with the internal channels.
|
|
175
|
+
const getChannelId = (channel: string): string => `user-channel/${channel}`;
|
|
@@ -7,7 +7,7 @@ import { Config } from '@dxos/config';
|
|
|
7
7
|
import { createCredentialSignerWithChain, CredentialGenerator } from '@dxos/credentials';
|
|
8
8
|
import {
|
|
9
9
|
SnapshotStore,
|
|
10
|
-
|
|
10
|
+
DataPipeline,
|
|
11
11
|
MetadataStore,
|
|
12
12
|
SigningContext,
|
|
13
13
|
SpaceManager,
|
|
@@ -73,7 +73,7 @@ export const createIdentity = async (peer: ServiceContext) => {
|
|
|
73
73
|
|
|
74
74
|
// TODO(burdon): Remove @dxos/client-testing.
|
|
75
75
|
// TODO(burdon): Create builder and make configurable.
|
|
76
|
-
export const syncItemsLocal = async (db1:
|
|
76
|
+
export const syncItemsLocal = async (db1: DataPipeline, db2: DataPipeline) => {
|
|
77
77
|
await testLocalDatabase(db1, db2);
|
|
78
78
|
await testLocalDatabase(db2, db1);
|
|
79
79
|
};
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import assert from 'node:assert';
|
|
6
6
|
|
|
7
|
-
import { asyncTimeout } from '@dxos/async';
|
|
8
|
-
import { Client, ClientServices, ClientServicesProxy, createDefaultModelFactory } from '@dxos/client';
|
|
7
|
+
import { asyncTimeout, Trigger } from '@dxos/async';
|
|
8
|
+
import { Client, ClientServices, ClientServicesProxy, createDefaultModelFactory, Invitation } from '@dxos/client';
|
|
9
9
|
import { Config } from '@dxos/config';
|
|
10
|
+
import { raise } from '@dxos/debug';
|
|
10
11
|
import { DocumentModel } from '@dxos/document-model';
|
|
11
12
|
import { DatabaseBackendProxy, genesisMutation } from '@dxos/echo-db';
|
|
12
13
|
import { PublicKey } from '@dxos/keys';
|
|
14
|
+
import { log } from '@dxos/log';
|
|
13
15
|
import { MemorySignalManager, MemorySignalManagerContext, WebsocketSignalManager } from '@dxos/messaging';
|
|
14
16
|
import { createWebRTCTransportFactory, MemoryTransportFactory, NetworkManager } from '@dxos/network-manager';
|
|
15
17
|
import { Storage } from '@dxos/random-access-storage';
|
|
@@ -119,12 +121,12 @@ export const testSpace = async (create: DatabaseBackendProxy, check: DatabaseBac
|
|
|
119
121
|
|
|
120
122
|
const result = create.mutate(genesisMutation(objectId, DocumentModel.meta.type));
|
|
121
123
|
|
|
122
|
-
await result.getReceipt();
|
|
124
|
+
await result.batch.getReceipt();
|
|
123
125
|
// TODO(dmaretskiy): await result.waitToBeProcessed()
|
|
124
|
-
assert(create._itemManager.entities.has(result.
|
|
126
|
+
assert(create._itemManager.entities.has(result.objectsUpdated[0].id));
|
|
125
127
|
|
|
126
128
|
await asyncTimeout(
|
|
127
|
-
check.
|
|
129
|
+
check.itemUpdate.waitForCondition(() => check._itemManager.entities.has(objectId)),
|
|
128
130
|
1000
|
|
129
131
|
);
|
|
130
132
|
|
|
@@ -138,3 +140,41 @@ export const syncItems = async (db1: DatabaseBackendProxy, db2: DatabaseBackendP
|
|
|
138
140
|
// Check item replicated from 2 => 1.
|
|
139
141
|
await testSpace(db2, db1);
|
|
140
142
|
};
|
|
143
|
+
|
|
144
|
+
export const joinCommonSpace = async ([initialPeer, ...peers]: Client[], spaceKey?: PublicKey): Promise<PublicKey> => {
|
|
145
|
+
const rootSpace = spaceKey ? initialPeer.getSpace(spaceKey) : await initialPeer.createSpace();
|
|
146
|
+
assert(rootSpace, 'Space not found.');
|
|
147
|
+
|
|
148
|
+
await Promise.all(
|
|
149
|
+
peers.map(async (peer) => {
|
|
150
|
+
const hostDone = new Trigger<Invitation>();
|
|
151
|
+
const guestDone = new Trigger<Invitation>();
|
|
152
|
+
|
|
153
|
+
const hostObservabli = rootSpace.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
|
|
154
|
+
log('invitation created');
|
|
155
|
+
hostObservabli.subscribe({
|
|
156
|
+
onConnecting: (invitation) => {
|
|
157
|
+
const guestObservable = peer.acceptInvitation(invitation);
|
|
158
|
+
log('invitation accepted');
|
|
159
|
+
|
|
160
|
+
guestObservable.subscribe({
|
|
161
|
+
onSuccess: (invitation: Invitation) => {
|
|
162
|
+
guestDone.wake(invitation);
|
|
163
|
+
log('invitation guestDone');
|
|
164
|
+
},
|
|
165
|
+
onError: (err: Error) => raise(err)
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
onSuccess: (invitation) => {
|
|
169
|
+
hostDone.wake(invitation);
|
|
170
|
+
log('invitation hostDone');
|
|
171
|
+
},
|
|
172
|
+
onError: (err) => raise(err)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await Promise.all([hostDone.wait(), guestDone.wait()]);
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return rootSpace.key;
|
|
180
|
+
};
|