@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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import assert from 'node:assert';
|
|
6
6
|
|
|
7
7
|
import { DeferredTask, Event, scheduleTask, sleep, TimeoutError, Trigger } from '@dxos/async';
|
|
8
|
-
import { Context } from '@dxos/context';
|
|
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,14 +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
|
-
|
|
20
|
-
const RETRY_TIMEOUT = 1_000;
|
|
19
|
+
const DEFAULT_RETRY_TIMEOUT = 1_000;
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
const SUCCESS_DELAY = 1_000;
|
|
21
|
+
const DEFAULT_SUCCESS_DELAY = 1_000;
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
};
|
|
27
56
|
|
|
28
57
|
/**
|
|
29
58
|
* See NotarizationService proto.
|
|
@@ -46,7 +75,13 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
46
75
|
/**
|
|
47
76
|
* Request credentials to be notarized.
|
|
48
77
|
*/
|
|
49
|
-
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) {
|
|
50
85
|
log('notarize', { credentials });
|
|
51
86
|
assert(
|
|
52
87
|
credentials.every((credential) => credential.id),
|
|
@@ -61,15 +96,23 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
61
96
|
errors.throw(err);
|
|
62
97
|
}
|
|
63
98
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
}
|
|
73
116
|
|
|
74
117
|
const allNotarized = Promise.all(credentials.map((credential) => this._waitUntilProcessed(credential.id!)));
|
|
75
118
|
|
|
@@ -79,16 +122,15 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
79
122
|
const notarizeTask = new DeferredTask(ctx, async () => {
|
|
80
123
|
try {
|
|
81
124
|
if (this._extensions.size === 0) {
|
|
82
|
-
log.warn('No peers to notarize with');
|
|
83
125
|
return; // No peers to try.
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
// Pick a peer that we haven't tried yet.
|
|
87
129
|
const peer = [...this._extensions].find((peer) => !peersTried.has(peer));
|
|
88
130
|
if (!peer) {
|
|
89
|
-
log.warn('Exhausted all peers to notarize with', { retryIn:
|
|
131
|
+
log.warn('Exhausted all peers to notarize with', { retryIn: retryTimeout });
|
|
90
132
|
peersTried.clear();
|
|
91
|
-
scheduleTask(ctx, () => notarizeTask.schedule(),
|
|
133
|
+
scheduleTask(ctx, () => notarizeTask.schedule(), retryTimeout); // retry with all peers again
|
|
92
134
|
return;
|
|
93
135
|
}
|
|
94
136
|
|
|
@@ -98,7 +140,7 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
98
140
|
credentials: credentials.filter((credential) => !this._processedCredentials.has(credential.id!))
|
|
99
141
|
});
|
|
100
142
|
log('success');
|
|
101
|
-
await sleep(
|
|
143
|
+
await sleep(successDelay); // wait before trying with a new peer
|
|
102
144
|
} catch (err) {
|
|
103
145
|
log.warn('error notarizing (recoverable)', err);
|
|
104
146
|
notarizeTask.schedule(); // retry immediately with next peer
|
|
@@ -109,8 +151,7 @@ export class NotarizationPlugin implements CredentialProcessor {
|
|
|
109
151
|
this._extensionOpened.on(ctx, () => notarizeTask.schedule());
|
|
110
152
|
|
|
111
153
|
try {
|
|
112
|
-
|
|
113
|
-
await Promise.race([allNotarized, errors.wait()]);
|
|
154
|
+
await Promise.race([rejectOnDispose(ctx), allNotarized, errors.wait()]);
|
|
114
155
|
log('done');
|
|
115
156
|
} finally {
|
|
116
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 () => {
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
Space,
|
|
15
15
|
SpaceMember,
|
|
16
16
|
SpacesService,
|
|
17
|
-
SpaceStatus,
|
|
18
17
|
SubscribeMessagesRequest,
|
|
19
18
|
UpdateSpaceRequest,
|
|
20
19
|
WriteCredentialsRequest
|
|
@@ -27,6 +26,8 @@ import { IdentityManager } from '../identity';
|
|
|
27
26
|
import { DataSpace } from './data-space';
|
|
28
27
|
import { DataSpaceManager } from './data-space-manager';
|
|
29
28
|
|
|
29
|
+
const TIMEFRAME_UPDATE_DEBOUNCE_TIME = 500;
|
|
30
|
+
|
|
30
31
|
/**
|
|
31
32
|
*
|
|
32
33
|
*/
|
|
@@ -55,29 +56,36 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
55
56
|
return new Stream<QuerySpacesResponse>(({ next, ctx }) => {
|
|
56
57
|
const onUpdate = async () => {
|
|
57
58
|
const dataSpaceManager = await this._getDataSpaceManager();
|
|
58
|
-
const spaces = Array.from(dataSpaceManager.spaces.values())
|
|
59
|
-
// Skip spaces without data service available.
|
|
60
|
-
.filter((space) => this._dataServiceSubscriptions.getDataService(space.key))
|
|
61
|
-
.map((space) => this._transformSpace(space));
|
|
59
|
+
const spaces = Array.from(dataSpaceManager.spaces.values()).map((space) => this._transformSpace(space));
|
|
62
60
|
log('update', { spaces });
|
|
63
61
|
next({ spaces });
|
|
64
62
|
};
|
|
65
63
|
|
|
66
|
-
|
|
64
|
+
scheduleTask(ctx, async () => {
|
|
67
65
|
const dataSpaceManager = await this._getDataSpaceManager();
|
|
66
|
+
|
|
68
67
|
const subscriptions = new EventSubscriptions();
|
|
68
|
+
ctx.onDispose(() => subscriptions.clear());
|
|
69
|
+
|
|
69
70
|
// TODO(dmaretskyi): Create a pattern for subscribing to a set of objects.
|
|
70
71
|
const subscribeSpaces = () => {
|
|
71
72
|
subscriptions.clear();
|
|
72
73
|
|
|
73
74
|
for (const space of dataSpaceManager.spaces.values()) {
|
|
74
|
-
if (!this._dataServiceSubscriptions.getDataService(space.key)) {
|
|
75
|
-
// Skip spaces without data service available.
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
75
|
subscriptions.add(space.stateUpdate.on(ctx, onUpdate));
|
|
80
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
|
+
}
|
|
81
89
|
}
|
|
82
90
|
};
|
|
83
91
|
|
|
@@ -85,12 +93,9 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
85
93
|
subscribeSpaces();
|
|
86
94
|
void onUpdate();
|
|
87
95
|
});
|
|
96
|
+
subscribeSpaces();
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
scheduleTask(ctx, () => {
|
|
91
|
-
subscribeSpaces();
|
|
92
|
-
void onUpdate();
|
|
93
|
-
});
|
|
98
|
+
void onUpdate();
|
|
94
99
|
});
|
|
95
100
|
|
|
96
101
|
if (!this._identityManager.identity) {
|
|
@@ -142,7 +147,13 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
142
147
|
private _transformSpace(space: DataSpace): Space {
|
|
143
148
|
return {
|
|
144
149
|
spaceKey: space.key,
|
|
145
|
-
|
|
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
|
+
},
|
|
146
157
|
members: Array.from(space.inner.spaceState.members.values()).map((member) => ({
|
|
147
158
|
identity: {
|
|
148
159
|
identityKey: member.key,
|
|
@@ -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
|
};
|
|
@@ -142,7 +142,7 @@ export const syncItems = async (db1: DatabaseBackendProxy, db2: DatabaseBackendP
|
|
|
142
142
|
};
|
|
143
143
|
|
|
144
144
|
export const joinCommonSpace = async ([initialPeer, ...peers]: Client[], spaceKey?: PublicKey): Promise<PublicKey> => {
|
|
145
|
-
const rootSpace = spaceKey ? initialPeer.
|
|
145
|
+
const rootSpace = spaceKey ? initialPeer.getSpace(spaceKey) : await initialPeer.createSpace();
|
|
146
146
|
assert(rootSpace, 'Space not found.');
|
|
147
147
|
|
|
148
148
|
await Promise.all(
|
|
@@ -154,7 +154,7 @@ export const joinCommonSpace = async ([initialPeer, ...peers]: Client[], spaceKe
|
|
|
154
154
|
log('invitation created');
|
|
155
155
|
hostObservabli.subscribe({
|
|
156
156
|
onConnecting: (invitation) => {
|
|
157
|
-
const guestObservable = peer.
|
|
157
|
+
const guestObservable = peer.acceptInvitation(invitation);
|
|
158
158
|
log('invitation accepted');
|
|
159
159
|
|
|
160
160
|
guestObservable.subscribe({
|
|
@@ -7,7 +7,7 @@ import assert from 'node:assert';
|
|
|
7
7
|
import waitForExpect from 'wait-for-expect';
|
|
8
8
|
|
|
9
9
|
import { Trigger } from '@dxos/async';
|
|
10
|
-
import { Space } from '@dxos/client';
|
|
10
|
+
import { Client, Space } from '@dxos/client';
|
|
11
11
|
import { raise } from '@dxos/debug';
|
|
12
12
|
import { log } from '@dxos/log';
|
|
13
13
|
import { Invitation, SpaceMember } from '@dxos/protocols/proto/dxos/client/services';
|
|
@@ -19,6 +19,19 @@ import { syncItems, TestBuilder } from '../testing';
|
|
|
19
19
|
// TODO(burdon): Timeouts and progress callback/events.
|
|
20
20
|
|
|
21
21
|
describe('Client services', () => {
|
|
22
|
+
test('creates client with local host', async () => {
|
|
23
|
+
const testBuilder = new TestBuilder();
|
|
24
|
+
|
|
25
|
+
const servicesProvider = testBuilder.createLocal();
|
|
26
|
+
await servicesProvider.open();
|
|
27
|
+
afterTest(() => servicesProvider.close());
|
|
28
|
+
|
|
29
|
+
const client = new Client({ services: servicesProvider });
|
|
30
|
+
await client.initialize();
|
|
31
|
+
afterTest(() => client.destroy());
|
|
32
|
+
expect(client.initialized).to.be.true;
|
|
33
|
+
});
|
|
34
|
+
|
|
22
35
|
test('creates client with remote server', async () => {
|
|
23
36
|
const testBuilder = new TestBuilder();
|
|
24
37
|
|
|
@@ -141,16 +154,16 @@ describe('Client services', () => {
|
|
|
141
154
|
// Check same identity.
|
|
142
155
|
const [invitation1, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
|
|
143
156
|
expect(invitation1.identityKey).not.to.exist;
|
|
144
|
-
expect(invitation2.identityKey).to.deep.eq(client1.halo.identity!.identityKey);
|
|
145
|
-
expect(invitation2.identityKey).to.deep.eq(client2.halo.identity!.identityKey);
|
|
157
|
+
expect(invitation2.identityKey).to.deep.eq(client1.halo.identity.get()!.identityKey);
|
|
158
|
+
expect(invitation2.identityKey).to.deep.eq(client2.halo.identity.get()!.identityKey);
|
|
146
159
|
expect(invitation1.state).to.eq(Invitation.State.SUCCESS);
|
|
147
160
|
expect(invitation2.state).to.eq(Invitation.State.SUCCESS);
|
|
148
161
|
|
|
149
162
|
// Check devices.
|
|
150
163
|
// TODO(burdon): Incorrect number of devices.
|
|
151
164
|
await waitForExpect(async () => {
|
|
152
|
-
expect(client1.halo.
|
|
153
|
-
expect(client2.halo.
|
|
165
|
+
expect(client1.halo.devices.get()).to.have.lengthOf(2);
|
|
166
|
+
expect(client2.halo.devices.get()).to.have.lengthOf(2);
|
|
154
167
|
});
|
|
155
168
|
});
|
|
156
169
|
|
|
@@ -184,13 +197,13 @@ describe('Client services', () => {
|
|
|
184
197
|
const success1 = new Trigger<Invitation>();
|
|
185
198
|
const success2 = new Trigger<Invitation>();
|
|
186
199
|
|
|
187
|
-
const space1 = await client1.
|
|
200
|
+
const space1 = await client1.createSpace();
|
|
188
201
|
log('createSpace', { key: space1.key });
|
|
189
202
|
const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
|
|
190
203
|
|
|
191
204
|
observable1.subscribe({
|
|
192
205
|
onConnecting: (invitation) => {
|
|
193
|
-
const observable2 = client2.
|
|
206
|
+
const observable2 = client2.acceptInvitation(invitation);
|
|
194
207
|
observable2.subscribe({
|
|
195
208
|
onSuccess: (invitation: Invitation) => {
|
|
196
209
|
success2.wake(invitation);
|
|
@@ -214,7 +227,7 @@ describe('Client services', () => {
|
|
|
214
227
|
// TODO(burdon): Space should now be available?
|
|
215
228
|
const trigger = new Trigger<Space>();
|
|
216
229
|
await waitForExpect(() => {
|
|
217
|
-
const space2 = client2.
|
|
230
|
+
const space2 = client2.getSpace(invitation2.spaceKey!);
|
|
218
231
|
assert(space2);
|
|
219
232
|
expect(space2).to.exist;
|
|
220
233
|
trigger.wake(space2);
|
|
@@ -224,10 +237,10 @@ describe('Client services', () => {
|
|
|
224
237
|
|
|
225
238
|
for (const space of [space1, space2]) {
|
|
226
239
|
await waitForExpect(() => {
|
|
227
|
-
expect(space.
|
|
240
|
+
expect(space.members.get()).to.deep.equal([
|
|
228
241
|
{
|
|
229
242
|
identity: {
|
|
230
|
-
identityKey: client1.halo.identity!.identityKey,
|
|
243
|
+
identityKey: client1.halo.identity.get()!.identityKey,
|
|
231
244
|
profile: {
|
|
232
245
|
displayName: 'Peer 1'
|
|
233
246
|
}
|
|
@@ -236,7 +249,7 @@ describe('Client services', () => {
|
|
|
236
249
|
},
|
|
237
250
|
{
|
|
238
251
|
identity: {
|
|
239
|
-
identityKey: client2.halo.identity!.identityKey,
|
|
252
|
+
identityKey: client2.halo.identity.get()!.identityKey,
|
|
240
253
|
profile: {
|
|
241
254
|
displayName: 'Peer 2'
|
|
242
255
|
}
|
|
@@ -61,9 +61,7 @@ describe('Client', () => {
|
|
|
61
61
|
const client = new Client({ services: testBuilder.createLocal() });
|
|
62
62
|
await client.initialize();
|
|
63
63
|
afterTest(() => client.destroy());
|
|
64
|
-
await expect(client.
|
|
65
|
-
'This device has no HALO identity available.'
|
|
66
|
-
);
|
|
64
|
+
await expect(client.createSpace()).to.eventually.be.rejectedWith('This device has no HALO identity available.');
|
|
67
65
|
}).timeout(1_000);
|
|
68
66
|
|
|
69
67
|
// TODO(burdon): Memory store is reset on close (feed store is closed).
|
|
@@ -109,13 +107,12 @@ const getNodeConfig = async (reset = false) => {
|
|
|
109
107
|
const runTest = async (testBuilder: TestBuilder) => {
|
|
110
108
|
const client = new Client({ services: testBuilder.createLocal() });
|
|
111
109
|
const displayName = 'test-user';
|
|
112
|
-
|
|
113
110
|
{
|
|
114
111
|
// Create identity.
|
|
115
112
|
await client.initialize();
|
|
116
|
-
expect(client.halo.identity).not.to.exist;
|
|
113
|
+
expect(client.halo.identity.get()).not.to.exist;
|
|
117
114
|
const identity = await client.halo.createIdentity({ displayName });
|
|
118
|
-
expect(client.halo.identity).to.deep.eq(identity);
|
|
115
|
+
expect(client.halo.identity.get()).to.deep.eq(identity);
|
|
119
116
|
await client.destroy();
|
|
120
117
|
}
|
|
121
118
|
|
|
@@ -22,8 +22,8 @@ describe('Halo', () => {
|
|
|
22
22
|
await client.halo.createIdentity({ displayName: 'test-user' });
|
|
23
23
|
expect(client.halo.identity).exist;
|
|
24
24
|
|
|
25
|
-
expect(await client.halo.
|
|
26
|
-
expect(client.halo.identity!.profile?.displayName).to.equal('test-user');
|
|
25
|
+
expect(await client.halo.devices.get()).to.have.lengthOf(1);
|
|
26
|
+
expect(client.halo.identity.get()!.profile?.displayName).to.equal('test-user');
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
test('device invitations', async () => {
|
|
@@ -36,7 +36,7 @@ describe('Halo', () => {
|
|
|
36
36
|
await client1.halo.createIdentity({ displayName: 'test-user' });
|
|
37
37
|
expect(client1.halo.identity).exist;
|
|
38
38
|
|
|
39
|
-
expect(await client1.halo.
|
|
39
|
+
expect(await client1.halo.devices.get()).to.have.lengthOf(1);
|
|
40
40
|
|
|
41
41
|
const client2 = new Client({ services: testBuilder.createLocal() });
|
|
42
42
|
afterTest(() => client2.destroy());
|
|
@@ -68,7 +68,7 @@ describe('Halo', () => {
|
|
|
68
68
|
await done1.wait();
|
|
69
69
|
await done2.wait();
|
|
70
70
|
|
|
71
|
-
expect(await client1.halo.
|
|
72
|
-
expect(await client2.halo.
|
|
71
|
+
expect(await client1.halo.devices.get()).to.have.lengthOf(2);
|
|
72
|
+
expect(await client2.halo.devices.get()).to.have.lengthOf(2);
|
|
73
73
|
});
|
|
74
74
|
});
|
|
@@ -8,7 +8,7 @@ import { Trigger } from '@dxos/async';
|
|
|
8
8
|
import { Client, Invitation } from '@dxos/client';
|
|
9
9
|
import { raise } from '@dxos/debug';
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
|
-
import { describe, test
|
|
11
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
12
12
|
|
|
13
13
|
import { TestBuilder, testSpace } from '../testing';
|
|
14
14
|
|
|
@@ -31,13 +31,13 @@ describe('Spaces/invitations', () => {
|
|
|
31
31
|
const success1 = new Trigger<Invitation>();
|
|
32
32
|
const success2 = new Trigger<Invitation>();
|
|
33
33
|
|
|
34
|
-
const space1 = await client1.
|
|
34
|
+
const space1 = await client1.createSpace();
|
|
35
35
|
log('createSpace', { key: space1.key });
|
|
36
36
|
const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
|
|
37
37
|
|
|
38
38
|
observable1.subscribe({
|
|
39
39
|
onConnecting: (invitation) => {
|
|
40
|
-
const observable2 = client2.
|
|
40
|
+
const observable2 = client2.acceptInvitation(invitation);
|
|
41
41
|
observable2.subscribe({
|
|
42
42
|
onSuccess: (invitation: Invitation) => {
|
|
43
43
|
success2.wake(invitation);
|
|
@@ -57,7 +57,7 @@ describe('Spaces/invitations', () => {
|
|
|
57
57
|
expect(invitation1.state).to.eq(Invitation.State.SUCCESS);
|
|
58
58
|
|
|
59
59
|
{
|
|
60
|
-
const space = client2.
|
|
60
|
+
const space = await client2.getSpace(invitation2.spaceKey!)!.waitUntilReady();
|
|
61
61
|
await testSpace(space.internal.db);
|
|
62
62
|
}
|
|
63
63
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { expect } from 'chai';
|
|
6
6
|
|
|
7
7
|
import { asyncTimeout, Trigger } from '@dxos/async';
|
|
8
|
-
import { Client, Invitation } from '@dxos/client';
|
|
8
|
+
import { Client, Invitation, Space } from '@dxos/client';
|
|
9
9
|
import { Config } from '@dxos/config';
|
|
10
10
|
import { raise } from '@dxos/debug';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
@@ -25,10 +25,10 @@ describe('Spaces', () => {
|
|
|
25
25
|
await client.halo.createIdentity({ displayName: 'test-user' });
|
|
26
26
|
|
|
27
27
|
// TODO(burdon): Extend basic queries.
|
|
28
|
-
const space = await client.
|
|
28
|
+
const space = await client.createSpace();
|
|
29
29
|
await testSpace(space.internal.db);
|
|
30
30
|
|
|
31
|
-
expect(space.
|
|
31
|
+
expect(space.members.get()).to.be.length(1);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
test('creates a space re-opens the client', async () => {
|
|
@@ -42,12 +42,12 @@ describe('Spaces', () => {
|
|
|
42
42
|
let itemId: string;
|
|
43
43
|
{
|
|
44
44
|
// TODO(burdon): API (client.echo/client.halo).
|
|
45
|
-
const space = await client.
|
|
45
|
+
const space = await client.createSpace();
|
|
46
46
|
const {
|
|
47
47
|
objectsUpdated: [item]
|
|
48
48
|
} = await testSpace(space.internal.db);
|
|
49
49
|
itemId = item.id;
|
|
50
|
-
expect(space.
|
|
50
|
+
expect(space.members.get()).to.be.length(1);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
await client.destroy();
|
|
@@ -57,9 +57,18 @@ describe('Spaces', () => {
|
|
|
57
57
|
await client.initialize();
|
|
58
58
|
|
|
59
59
|
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// TODO(dmaretskyi): Replace with helper?.
|
|
61
|
+
const spaceTrigger = new Trigger<Space>();
|
|
62
|
+
if (client.spaces.get()[0]) {
|
|
63
|
+
spaceTrigger.wake(client.spaces.get()[0]);
|
|
64
|
+
}
|
|
65
|
+
client.spaces.subscribe(() => {
|
|
66
|
+
if (client.spaces.get()[0]) {
|
|
67
|
+
spaceTrigger.wake(client.spaces.get()[0]);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
const space = await spaceTrigger.wait({ timeout: 500 });
|
|
71
|
+
await space.waitUntilReady();
|
|
63
72
|
|
|
64
73
|
const item = space.internal.db._itemManager.getItem(itemId)!;
|
|
65
74
|
expect(item).to.exist;
|
|
@@ -86,13 +95,13 @@ describe('Spaces', () => {
|
|
|
86
95
|
const success1 = new Trigger<Invitation>();
|
|
87
96
|
const success2 = new Trigger<Invitation>();
|
|
88
97
|
|
|
89
|
-
const space1 = await client1.
|
|
98
|
+
const space1 = await client1.createSpace();
|
|
90
99
|
log('createSpace', { key: space1.key });
|
|
91
100
|
const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
|
|
92
101
|
|
|
93
102
|
observable1.subscribe({
|
|
94
103
|
onConnecting: (invitation) => {
|
|
95
|
-
const observable2 = client2.
|
|
104
|
+
const observable2 = client2.acceptInvitation(invitation);
|
|
96
105
|
observable2.subscribe({
|
|
97
106
|
onSuccess: (invitation: Invitation) => {
|
|
98
107
|
success2.wake(invitation);
|
|
@@ -109,7 +118,7 @@ describe('Spaces', () => {
|
|
|
109
118
|
|
|
110
119
|
const [_, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
|
|
111
120
|
|
|
112
|
-
const space2 = client2.
|
|
121
|
+
const space2 = await client2.getSpace(invitation2.spaceKey!)!.waitUntilReady();
|
|
113
122
|
|
|
114
123
|
const hello = new Trigger();
|
|
115
124
|
{
|
|
@@ -6,7 +6,7 @@ import * as fc from 'fast-check';
|
|
|
6
6
|
import { ModelRunSetup } from 'fast-check';
|
|
7
7
|
import waitForExpect from 'wait-for-expect';
|
|
8
8
|
|
|
9
|
-
import { Client,
|
|
9
|
+
import { Client, Expando, Text } from '@dxos/client';
|
|
10
10
|
import { PublicKey } from '@dxos/keys';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
import { describe, test } from '@dxos/test';
|
|
@@ -34,7 +34,7 @@ const assertState = async (model: Model, real: Real) => {
|
|
|
34
34
|
// Wait for replication.
|
|
35
35
|
await waitForExpect(() => {
|
|
36
36
|
for (const [peerId, peer] of real.peers.entries()) {
|
|
37
|
-
const space = peer.
|
|
37
|
+
const space = peer.getSpace(real.spaceKey);
|
|
38
38
|
if (space) {
|
|
39
39
|
if (!model.peers.has(peerId)) {
|
|
40
40
|
throw new Error(`Expected peer to not be in space: ${peerId.truncate()}`);
|
|
@@ -73,10 +73,10 @@ class CreatePeerCommand implements fc.AsyncCommand<Model, Real> {
|
|
|
73
73
|
await client.initialize();
|
|
74
74
|
await client.halo.createIdentity();
|
|
75
75
|
if (real.peers.size === 0) {
|
|
76
|
-
const space = await client.
|
|
76
|
+
const space = await client.createSpace();
|
|
77
77
|
const content = new Text();
|
|
78
78
|
content.doc?.getText('utf8').insert(0, initialContent);
|
|
79
|
-
await space.db.add(new
|
|
79
|
+
await space.db.add(new Expando({ content }));
|
|
80
80
|
real.spaceKey = space.key;
|
|
81
81
|
} else {
|
|
82
82
|
const host = Array.from(real.peers.values())[0];
|
|
@@ -114,7 +114,7 @@ class InsertTextCommand implements fc.AsyncCommand<Model, Real> {
|
|
|
114
114
|
model.text = model.text.slice(0, this.index) + this.text + model.text.slice(this.index);
|
|
115
115
|
|
|
116
116
|
const peer = real.peers.get(this.peerId);
|
|
117
|
-
const space = peer!.
|
|
117
|
+
const space = peer!.getSpace(real.spaceKey)!;
|
|
118
118
|
const [document] = space.db.query((obj) => !!obj.content).objects;
|
|
119
119
|
const text = (document.content.doc as Doc).getText('utf8');
|
|
120
120
|
text.insert(this.index, this.text);
|
|
@@ -140,7 +140,7 @@ class RemoveTextCommand implements fc.AsyncCommand<Model, Real> {
|
|
|
140
140
|
model.text = model.text.slice(0, this.index) + model.text.slice(this.index + this.length);
|
|
141
141
|
|
|
142
142
|
const peer = real.peers.get(this.peerId);
|
|
143
|
-
const space = peer!.
|
|
143
|
+
const space = peer!.getSpace(real.spaceKey)!;
|
|
144
144
|
const [document] = space.db.query((obj) => !!obj.content).objects;
|
|
145
145
|
const text = (document.content.doc as Doc).getText('utf8');
|
|
146
146
|
text.delete(this.index, this.length);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { asyncTimeout, Trigger } from '@dxos/async';
|
|
6
|
-
import { log } from '@dxos/log';
|
|
6
|
+
import { log, logInfo } from '@dxos/log';
|
|
7
7
|
import { MaybePromise } from '@dxos/util';
|
|
8
8
|
|
|
9
9
|
enum Message {
|
|
@@ -30,15 +30,24 @@ export class VaultResourceLock {
|
|
|
30
30
|
this._broadcastChannel.onmessage = this._onMessage.bind(this);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
@logInfo
|
|
34
|
+
get lockKey() {
|
|
35
|
+
return this._lockKey;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
async acquire() {
|
|
34
39
|
this._broadcastChannel.postMessage({
|
|
35
40
|
message: Message.ACQUIRING
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
try {
|
|
44
|
+
log('aquiring lock...');
|
|
39
45
|
await asyncTimeout(this._requestLock(), 3_000);
|
|
46
|
+
log('acquired lock');
|
|
40
47
|
} catch {
|
|
48
|
+
log('stealing lock...');
|
|
41
49
|
await this._requestLock(true);
|
|
50
|
+
log('stolen lock');
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
|