@dxos/client-services 0.1.31 → 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-DFO3FW2W.mjs → chunk-E3FD43YN.mjs} +423 -209
- 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 +3 -3
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +424 -210
- 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 +432 -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 +24 -4
- 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.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/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 +30 -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 +17 -2
- package/src/packlets/spaces/data-space-manager.ts +76 -30
- package/src/packlets/spaces/data-space.ts +90 -15
- package/src/packlets/spaces/notarization-plugin.test.ts +3 -1
- package/src/packlets/spaces/notarization-plugin.ts +64 -23
- package/src/packlets/spaces/spaces-service.test.ts +1 -1
- package/src/packlets/spaces/spaces-service.ts +28 -17
- package/src/packlets/testing/host-test-builder.ts +2 -2
- package/src/packlets/testing/test-builder.ts +2 -2
- 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 +4 -4
- package/src/packlets/tests/spaces.test.ts +20 -11
- package/src/packlets/tests/text.test.ts +6 -6
- package/src/packlets/vault/vault-resource-lock.ts +10 -1
- package/dist/lib/browser/chunk-DFO3FW2W.mjs.map +0 -7
|
@@ -18,8 +18,9 @@ describe('services/ServiceContext', () => {
|
|
|
18
18
|
const device2 = createServiceContext({ signalContext: networkContext });
|
|
19
19
|
await performInvitation(device1.deviceInvitations, device2.deviceInvitations, undefined);
|
|
20
20
|
|
|
21
|
+
await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
|
|
21
22
|
const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
|
|
22
|
-
await syncItemsLocal(space1.
|
|
23
|
+
await syncItemsLocal(space1.dataPipeline, space2!.dataPipeline);
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
test('new space is synchronized on device invitations', async () => {
|
|
@@ -31,9 +32,9 @@ describe('services/ServiceContext', () => {
|
|
|
31
32
|
await performInvitation(device1.deviceInvitations, device2.deviceInvitations, undefined);
|
|
32
33
|
|
|
33
34
|
const space1 = await device1.dataSpaceManager!.createSpace();
|
|
34
|
-
await device2.dataSpaceManager
|
|
35
|
+
await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
|
|
35
36
|
const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
|
|
36
|
-
await syncItemsLocal(space1.
|
|
37
|
+
await syncItemsLocal(space1.dataPipeline, space2!.dataPipeline);
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
test('joined space is synchronized on device invitations', async () => {
|
|
@@ -49,8 +50,8 @@ describe('services/ServiceContext', () => {
|
|
|
49
50
|
const space1 = await identity2.dataSpaceManager!.createSpace();
|
|
50
51
|
await performInvitation(identity2.spaceInvitations!, device1.spaceInvitations!, space1);
|
|
51
52
|
|
|
52
|
-
await device2.dataSpaceManager
|
|
53
|
+
await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
|
|
53
54
|
const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
|
|
54
|
-
await syncItemsLocal(space1.
|
|
55
|
+
await syncItemsLocal(space1.dataPipeline, space2!.dataPipeline);
|
|
55
56
|
});
|
|
56
57
|
});
|
|
@@ -62,6 +62,7 @@ export class ClientServicesHost {
|
|
|
62
62
|
* @internal
|
|
63
63
|
*/
|
|
64
64
|
_serviceContext!: ServiceContext;
|
|
65
|
+
private _opening = false;
|
|
65
66
|
private _open = false;
|
|
66
67
|
|
|
67
68
|
constructor({
|
|
@@ -83,7 +84,9 @@ export class ClientServicesHost {
|
|
|
83
84
|
this._resourceLock = lockKey
|
|
84
85
|
? new VaultResourceLock({
|
|
85
86
|
lockKey,
|
|
86
|
-
onAcquire: () =>
|
|
87
|
+
onAcquire: () => {
|
|
88
|
+
this._opening || this.open();
|
|
89
|
+
},
|
|
87
90
|
onRelease: () => this.close()
|
|
88
91
|
})
|
|
89
92
|
: undefined;
|
|
@@ -163,7 +166,8 @@ export class ClientServicesHost {
|
|
|
163
166
|
assert(this._storage, 'storage not set');
|
|
164
167
|
assert(this._networkManager, 'network manager not set');
|
|
165
168
|
|
|
166
|
-
|
|
169
|
+
this._opening = true;
|
|
170
|
+
log('opening...', { lockKey: this._resourceLock?.lockKey });
|
|
167
171
|
await this._resourceLock?.acquire();
|
|
168
172
|
|
|
169
173
|
// TODO(wittjosiah): Make re-entrant.
|
|
@@ -213,6 +217,7 @@ export class ClientServicesHost {
|
|
|
213
217
|
});
|
|
214
218
|
|
|
215
219
|
await this._serviceContext.open();
|
|
220
|
+
this._opening = false;
|
|
216
221
|
this._open = true;
|
|
217
222
|
this._statusUpdate.emit();
|
|
218
223
|
const deviceKey = this._serviceContext.identityManager.identity?.deviceKey;
|
|
@@ -11,7 +11,7 @@ import { AuthStatus, DataServiceSubscriptions } from '@dxos/echo-pipeline';
|
|
|
11
11
|
import { writeMessages } from '@dxos/feed-store';
|
|
12
12
|
import { log } from '@dxos/log';
|
|
13
13
|
import { ModelFactory } from '@dxos/model-factory';
|
|
14
|
-
import { test, describe } from '@dxos/test';
|
|
14
|
+
import { test, describe, afterTest } from '@dxos/test';
|
|
15
15
|
|
|
16
16
|
import { createSigningContext, HostTestBuilder, syncItemsLocal } from '../testing';
|
|
17
17
|
import { DataSpaceManager } from './data-space-manager';
|
|
@@ -32,6 +32,8 @@ describe('DataSpaceManager', () => {
|
|
|
32
32
|
peer.feedStore,
|
|
33
33
|
peer.snapshotStore
|
|
34
34
|
);
|
|
35
|
+
await dataSpaceManager.open();
|
|
36
|
+
afterTest(() => dataSpaceManager.close());
|
|
35
37
|
const space = await dataSpaceManager.createSpace();
|
|
36
38
|
|
|
37
39
|
// Process all written mutations.
|
|
@@ -58,6 +60,8 @@ describe('DataSpaceManager', () => {
|
|
|
58
60
|
peer1.feedStore,
|
|
59
61
|
peer1.snapshotStore
|
|
60
62
|
);
|
|
63
|
+
await dataSpaceManager1.open();
|
|
64
|
+
afterTest(() => dataSpaceManager1.close());
|
|
61
65
|
|
|
62
66
|
const peer2 = builder.createPeer();
|
|
63
67
|
const identity2 = await createSigningContext(peer2.keyring);
|
|
@@ -71,6 +75,8 @@ describe('DataSpaceManager', () => {
|
|
|
71
75
|
peer2.feedStore,
|
|
72
76
|
peer1.snapshotStore
|
|
73
77
|
);
|
|
78
|
+
await dataSpaceManager2.open();
|
|
79
|
+
afterTest(() => dataSpaceManager2.close());
|
|
74
80
|
|
|
75
81
|
const space1 = await dataSpaceManager1.createSpace();
|
|
76
82
|
await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
|
|
@@ -91,6 +97,7 @@ describe('DataSpaceManager', () => {
|
|
|
91
97
|
spaceKey: space1.key,
|
|
92
98
|
genesisFeedKey: space1.inner.genesisFeedKey
|
|
93
99
|
});
|
|
100
|
+
await dataSpaceManager2.waitUntilSpaceReady(space2.key);
|
|
94
101
|
|
|
95
102
|
log('', {
|
|
96
103
|
peer1: {
|
|
@@ -122,7 +129,7 @@ describe('DataSpaceManager', () => {
|
|
|
122
129
|
});
|
|
123
130
|
log.break();
|
|
124
131
|
|
|
125
|
-
await syncItemsLocal(space1.
|
|
132
|
+
await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
|
|
126
133
|
|
|
127
134
|
expect(space1.inner.protocol.sessions.get(identity2.deviceKey)).to.exist;
|
|
128
135
|
expect(space1.inner.protocol.sessions.get(identity2.deviceKey)?.authStatus).to.equal(AuthStatus.SUCCESS);
|
|
@@ -145,6 +152,8 @@ describe('DataSpaceManager', () => {
|
|
|
145
152
|
peer1.feedStore,
|
|
146
153
|
peer1.snapshotStore
|
|
147
154
|
);
|
|
155
|
+
await dataSpaceManager1.open();
|
|
156
|
+
afterTest(() => dataSpaceManager1.close());
|
|
148
157
|
|
|
149
158
|
const peer2 = builder.createPeer();
|
|
150
159
|
const identity2 = await createSigningContext(peer2.keyring);
|
|
@@ -158,6 +167,8 @@ describe('DataSpaceManager', () => {
|
|
|
158
167
|
peer2.feedStore,
|
|
159
168
|
peer1.snapshotStore
|
|
160
169
|
);
|
|
170
|
+
await dataSpaceManager2.open();
|
|
171
|
+
afterTest(() => dataSpaceManager2.close());
|
|
161
172
|
|
|
162
173
|
const space1 = await dataSpaceManager1.createSpace();
|
|
163
174
|
await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
|
|
@@ -179,6 +190,10 @@ describe('DataSpaceManager', () => {
|
|
|
179
190
|
genesisFeedKey: space1.inner.genesisFeedKey
|
|
180
191
|
});
|
|
181
192
|
|
|
193
|
+
// Coincidentally, this also waits until a P2P connection is established between peers.
|
|
194
|
+
// TODO(dmaretskyi): Refine this to wait for connection specifically.
|
|
195
|
+
await dataSpaceManager2.waitUntilSpaceReady(space2.key);
|
|
196
|
+
|
|
182
197
|
const [receivedMessage, inc] = latch({ count: 1 });
|
|
183
198
|
space2.listen('test', (message) => {
|
|
184
199
|
expect(message.channelId).to.equal('test');
|
|
@@ -5,17 +5,18 @@
|
|
|
5
5
|
import assert from 'node:assert';
|
|
6
6
|
|
|
7
7
|
import { Event, synchronized, trackLeaks } from '@dxos/async';
|
|
8
|
-
import {
|
|
8
|
+
import { SpaceState } from '@dxos/client';
|
|
9
|
+
import { cancelWithContext, Context } from '@dxos/context';
|
|
9
10
|
import { getCredentialAssertion } from '@dxos/credentials';
|
|
10
11
|
import {
|
|
12
|
+
DataServiceSubscriptions,
|
|
11
13
|
MetadataStore,
|
|
12
14
|
SigningContext,
|
|
15
|
+
SnapshotManager,
|
|
16
|
+
SnapshotStore,
|
|
13
17
|
Space,
|
|
14
18
|
spaceGenesis,
|
|
15
|
-
SpaceManager
|
|
16
|
-
DataServiceSubscriptions,
|
|
17
|
-
SnapshotManager,
|
|
18
|
-
SnapshotStore
|
|
19
|
+
SpaceManager
|
|
19
20
|
} from '@dxos/echo-pipeline';
|
|
20
21
|
import { FeedStore } from '@dxos/feed-store';
|
|
21
22
|
import { Keyring } from '@dxos/keyring';
|
|
@@ -25,16 +26,27 @@ import { ModelFactory } from '@dxos/model-factory';
|
|
|
25
26
|
import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
26
27
|
import { SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
27
28
|
import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
|
|
29
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
28
30
|
import { ComplexMap, deferFunction } from '@dxos/util';
|
|
29
31
|
|
|
30
32
|
import { createAuthProvider } from '../identity';
|
|
31
33
|
import { DataSpace } from './data-space';
|
|
32
34
|
|
|
33
|
-
const DATA_PIPELINE_READY_TIMEOUT = 3_000;
|
|
34
|
-
|
|
35
35
|
export type AcceptSpaceOptions = {
|
|
36
36
|
spaceKey: PublicKey;
|
|
37
37
|
genesisFeedKey: PublicKey;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Latest known timeframe for the control pipeline.
|
|
41
|
+
* We will try to catch up to this timeframe before starting the data pipeline.
|
|
42
|
+
*/
|
|
43
|
+
controlTimeframe?: Timeframe;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Latest known timeframe for the data pipeline.
|
|
47
|
+
* We will try to catch up to this timeframe before initializing the database.
|
|
48
|
+
*/
|
|
49
|
+
dataTimeframe?: Timeframe;
|
|
38
50
|
};
|
|
39
51
|
|
|
40
52
|
@trackLeaks('open', 'close')
|
|
@@ -45,6 +57,8 @@ export class DataSpaceManager {
|
|
|
45
57
|
|
|
46
58
|
private readonly _spaces = new ComplexMap<PublicKey, DataSpace>(PublicKey.hash);
|
|
47
59
|
|
|
60
|
+
private _isOpen = false;
|
|
61
|
+
|
|
48
62
|
constructor(
|
|
49
63
|
private readonly _spaceManager: SpaceManager,
|
|
50
64
|
private readonly _metadataStore: MetadataStore,
|
|
@@ -63,29 +77,24 @@ export class DataSpaceManager {
|
|
|
63
77
|
|
|
64
78
|
@synchronized
|
|
65
79
|
async open() {
|
|
80
|
+
log('open');
|
|
66
81
|
await this._metadataStore.load();
|
|
67
82
|
log('metadata loaded', { spaces: this._metadataStore.spaces.length });
|
|
68
83
|
|
|
69
84
|
for (const spaceMetadata of this._metadataStore.spaces) {
|
|
70
85
|
log('load space', { spaceMetadata });
|
|
71
86
|
const space = await this._constructSpace(spaceMetadata);
|
|
72
|
-
|
|
73
|
-
if (spaceMetadata.dataTimeframe) {
|
|
74
|
-
log('waiting for latest timeframe', { spaceMetadata });
|
|
75
|
-
await space.dataPipelineController.pipelineState!.setTargetTimeframe(spaceMetadata.dataTimeframe);
|
|
76
|
-
}
|
|
77
|
-
await space.dataPipelineController.pipelineState!.waitUntilReachedTargetTimeframe({
|
|
78
|
-
timeout: DATA_PIPELINE_READY_TIMEOUT
|
|
79
|
-
});
|
|
80
|
-
this._dataServiceSubscriptions.registerSpace(
|
|
81
|
-
space.key,
|
|
82
|
-
space.dataPipelineController.databaseBackend!.createDataServiceHost()
|
|
83
|
-
);
|
|
87
|
+
space.initializeDataPipelineAsync();
|
|
84
88
|
}
|
|
89
|
+
|
|
90
|
+
this._isOpen = true;
|
|
91
|
+
this.updated.emit();
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
@synchronized
|
|
88
95
|
async close() {
|
|
96
|
+
log('close');
|
|
97
|
+
this._isOpen = false;
|
|
89
98
|
await this._ctx.dispose();
|
|
90
99
|
for (const space of this._spaces.values()) {
|
|
91
100
|
await space.close();
|
|
@@ -97,6 +106,7 @@ export class DataSpaceManager {
|
|
|
97
106
|
*/
|
|
98
107
|
@synchronized
|
|
99
108
|
async createSpace() {
|
|
109
|
+
assert(this._isOpen, 'Not open.');
|
|
100
110
|
const spaceKey = await this._keyring.createKey();
|
|
101
111
|
const controlFeedKey = await this._keyring.createKey();
|
|
102
112
|
const dataFeedKey = await this._keyring.createKey();
|
|
@@ -118,10 +128,6 @@ export class DataSpaceManager {
|
|
|
118
128
|
await this._signingContext.recordCredential(memberCredential);
|
|
119
129
|
|
|
120
130
|
await space.initializeDataPipeline();
|
|
121
|
-
this._dataServiceSubscriptions.registerSpace(
|
|
122
|
-
space.key,
|
|
123
|
-
space.dataPipelineController.databaseBackend!.createDataServiceHost()
|
|
124
|
-
);
|
|
125
131
|
|
|
126
132
|
this.updated.emit();
|
|
127
133
|
return space;
|
|
@@ -130,26 +136,43 @@ export class DataSpaceManager {
|
|
|
130
136
|
// TODO(burdon): Rename join space.
|
|
131
137
|
@synchronized
|
|
132
138
|
async acceptSpace(opts: AcceptSpaceOptions): Promise<DataSpace> {
|
|
139
|
+
log('accept space', { opts });
|
|
140
|
+
assert(this._isOpen, 'Not open.');
|
|
133
141
|
assert(!this._spaces.has(opts.spaceKey), 'Space already exists.');
|
|
134
142
|
|
|
135
143
|
const metadata: SpaceMetadata = {
|
|
136
144
|
key: opts.spaceKey,
|
|
137
|
-
genesisFeedKey: opts.genesisFeedKey
|
|
145
|
+
genesisFeedKey: opts.genesisFeedKey,
|
|
146
|
+
controlTimeframe: opts.controlTimeframe,
|
|
147
|
+
dataTimeframe: opts.dataTimeframe
|
|
138
148
|
};
|
|
139
149
|
|
|
140
150
|
const space = await this._constructSpace(metadata);
|
|
141
151
|
await this._metadataStore.addSpace(metadata);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
space.key,
|
|
145
|
-
space.dataPipelineController.databaseBackend!.createDataServiceHost()
|
|
146
|
-
);
|
|
152
|
+
|
|
153
|
+
space.initializeDataPipelineAsync();
|
|
147
154
|
|
|
148
155
|
this.updated.emit();
|
|
149
156
|
return space;
|
|
150
157
|
}
|
|
151
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Wait until the space data pipeline is fully initialized.
|
|
161
|
+
* Used by invitation handler.
|
|
162
|
+
* TODO(dmaretskyi): Consider removing.
|
|
163
|
+
*/
|
|
164
|
+
async waitUntilSpaceReady(spaceKey: PublicKey) {
|
|
165
|
+
await cancelWithContext(
|
|
166
|
+
this._ctx,
|
|
167
|
+
this.updated.waitForCondition(() => {
|
|
168
|
+
const space = this._spaces.get(spaceKey);
|
|
169
|
+
return !!space && space.state === SpaceState.READY;
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
152
174
|
private async _constructSpace(metadata: SpaceMetadata) {
|
|
175
|
+
log('construct space', { metadata });
|
|
153
176
|
const gossip = new Gossip({
|
|
154
177
|
localPeerId: this._signingContext.deviceKey
|
|
155
178
|
});
|
|
@@ -195,10 +218,33 @@ export class DataSpaceManager {
|
|
|
195
218
|
keyring: this._keyring,
|
|
196
219
|
feedStore: this._feedStore,
|
|
197
220
|
signingContext: this._signingContext,
|
|
198
|
-
snapshotId: metadata.snapshot
|
|
221
|
+
snapshotId: metadata.snapshot,
|
|
222
|
+
callbacks: {
|
|
223
|
+
beforeReady: async () => {
|
|
224
|
+
log('before space ready', { space: space.key });
|
|
225
|
+
this._dataServiceSubscriptions.registerSpace(
|
|
226
|
+
space.key,
|
|
227
|
+
dataSpace.dataPipeline.databaseBackend!.createDataServiceHost()
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
afterReady: async () => {
|
|
231
|
+
log('after space ready', { space: space.key, open: this._isOpen });
|
|
232
|
+
if (this._isOpen) {
|
|
233
|
+
this.updated.emit();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
199
237
|
});
|
|
200
238
|
|
|
201
239
|
await dataSpace.open();
|
|
240
|
+
|
|
241
|
+
if (metadata.controlTimeframe) {
|
|
242
|
+
dataSpace.inner.controlPipeline.state.setTargetTimeframe(metadata.controlTimeframe);
|
|
243
|
+
}
|
|
244
|
+
if (metadata.dataTimeframe) {
|
|
245
|
+
dataSpace.dataPipeline.setTargetTimeframe(metadata.dataTimeframe);
|
|
246
|
+
}
|
|
247
|
+
|
|
202
248
|
this._spaces.set(metadata.key, dataSpace);
|
|
203
249
|
return dataSpace;
|
|
204
250
|
}
|
|
@@ -2,21 +2,24 @@
|
|
|
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';
|
|
@@ -29,6 +32,18 @@ import { NotarizationPlugin } from './notarization-plugin';
|
|
|
29
32
|
|
|
30
33
|
const AUTH_TIMEOUT = 30000;
|
|
31
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
|
+
|
|
32
47
|
export type DataSpaceParams = {
|
|
33
48
|
inner: Space;
|
|
34
49
|
modelFactory: ModelFactory;
|
|
@@ -41,13 +56,13 @@ export type DataSpaceParams = {
|
|
|
41
56
|
signingContext: SigningContext;
|
|
42
57
|
memberKey: PublicKey;
|
|
43
58
|
snapshotId?: string | undefined;
|
|
59
|
+
callbacks?: DataSpaceCallbacks;
|
|
44
60
|
};
|
|
45
61
|
|
|
46
|
-
const CONTROL_PIPELINE_READY_TIMEFRAME = 3000;
|
|
47
62
|
@trackLeaks('open', 'close')
|
|
48
63
|
export class DataSpace {
|
|
49
64
|
private readonly _ctx = new Context();
|
|
50
|
-
private readonly
|
|
65
|
+
private readonly _dataPipeline: DataPipeline;
|
|
51
66
|
private readonly _inner: Space;
|
|
52
67
|
private readonly _gossip: Gossip;
|
|
53
68
|
private readonly _presence: Presence;
|
|
@@ -56,18 +71,25 @@ export class DataSpace {
|
|
|
56
71
|
private readonly _metadataStore: MetadataStore;
|
|
57
72
|
private readonly _signingContext: SigningContext;
|
|
58
73
|
private readonly _notarizationPluginConsumer: CredentialConsumer<NotarizationPlugin>;
|
|
74
|
+
private readonly _callbacks: DataSpaceCallbacks;
|
|
75
|
+
|
|
76
|
+
private _state = SpaceState.CLOSED;
|
|
59
77
|
|
|
60
78
|
public readonly authVerifier: TrustedKeySetAuthVerifier;
|
|
79
|
+
public readonly stateUpdate = new Event();
|
|
61
80
|
|
|
62
81
|
constructor(params: DataSpaceParams) {
|
|
63
82
|
this._inner = params.inner;
|
|
83
|
+
this._inner.stateUpdate.on(this._ctx, () => this.stateUpdate.emit());
|
|
84
|
+
|
|
64
85
|
this._gossip = params.gossip;
|
|
65
86
|
this._presence = params.presence;
|
|
66
87
|
this._keyring = params.keyring;
|
|
67
88
|
this._feedStore = params.feedStore;
|
|
68
89
|
this._metadataStore = params.metadataStore;
|
|
69
90
|
this._signingContext = params.signingContext;
|
|
70
|
-
this.
|
|
91
|
+
this._callbacks = params.callbacks ?? {};
|
|
92
|
+
this._dataPipeline = new DataPipeline({
|
|
71
93
|
modelFactory: params.modelFactory,
|
|
72
94
|
metadataStore: params.metadataStore,
|
|
73
95
|
snapshotManager: params.snapshotManager,
|
|
@@ -94,17 +116,17 @@ export class DataSpace {
|
|
|
94
116
|
return this._inner.isOpen;
|
|
95
117
|
}
|
|
96
118
|
|
|
119
|
+
get state(): SpaceState {
|
|
120
|
+
return this._state;
|
|
121
|
+
}
|
|
122
|
+
|
|
97
123
|
// TODO(burdon): Can we mark this for debugging only?
|
|
98
124
|
get inner() {
|
|
99
125
|
return this._inner;
|
|
100
126
|
}
|
|
101
127
|
|
|
102
|
-
get
|
|
103
|
-
return this.
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
get stateUpdate() {
|
|
107
|
-
return this._inner.stateUpdate;
|
|
128
|
+
get dataPipeline(): DataPipeline {
|
|
129
|
+
return this._dataPipeline;
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
get presence() {
|
|
@@ -119,11 +141,14 @@ export class DataSpace {
|
|
|
119
141
|
await this.notarizationPlugin.open();
|
|
120
142
|
await this._notarizationPluginConsumer.open();
|
|
121
143
|
await this._inner.open();
|
|
144
|
+
this._state = SpaceState.INACTIVE;
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
async close() {
|
|
148
|
+
this._state = SpaceState.CLOSED;
|
|
125
149
|
await this._ctx.dispose();
|
|
126
150
|
|
|
151
|
+
await this._dataPipeline.close();
|
|
127
152
|
await this.authVerifier.close();
|
|
128
153
|
|
|
129
154
|
await this._inner.close();
|
|
@@ -141,12 +166,40 @@ export class DataSpace {
|
|
|
141
166
|
return this._gossip.listen(channel, callback);
|
|
142
167
|
}
|
|
143
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
|
+
|
|
144
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.
|
|
145
194
|
await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
|
|
146
|
-
|
|
195
|
+
ctx: this._ctx,
|
|
196
|
+
breakOnStall: false
|
|
147
197
|
});
|
|
148
198
|
|
|
149
199
|
await this._createWritableFeeds();
|
|
200
|
+
log('Writable feeds created');
|
|
201
|
+
this.stateUpdate.emit();
|
|
202
|
+
|
|
150
203
|
this.notarizationPlugin.setWriter(
|
|
151
204
|
createMappedFeedWriter<Credential, FeedMessage.Payload>(
|
|
152
205
|
(credential) => ({
|
|
@@ -156,7 +209,28 @@ export class DataSpace {
|
|
|
156
209
|
)
|
|
157
210
|
);
|
|
158
211
|
|
|
159
|
-
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?.();
|
|
160
234
|
}
|
|
161
235
|
|
|
162
236
|
@timed(10_000)
|
|
@@ -198,7 +272,8 @@ export class DataSpace {
|
|
|
198
272
|
}
|
|
199
273
|
|
|
200
274
|
if (credentials.length > 0) {
|
|
201
|
-
|
|
275
|
+
// Never times out
|
|
276
|
+
await this.notarizationPlugin.notarize({ ctx: this._ctx, credentials, timeout: 0 });
|
|
202
277
|
}
|
|
203
278
|
|
|
204
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;
|