@dxos/echo-db 2.33.1-dev.3a7c6ac9 → 2.33.1-dev.83d113fe
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/src/echo.test.js +70 -0
- package/dist/src/echo.test.js.map +1 -1
- package/dist/src/parties/party-core.d.ts.map +1 -1
- package/dist/src/parties/party-core.js +1 -0
- package/dist/src/parties/party-core.js.map +1 -1
- package/dist/src/parties/party-internal.js +1 -1
- package/dist/src/parties/party-internal.js.map +1 -1
- package/dist/src/pipeline/party-feed-provider.d.ts +5 -1
- package/dist/src/pipeline/party-feed-provider.d.ts.map +1 -1
- package/dist/src/pipeline/party-feed-provider.js +36 -13
- package/dist/src/pipeline/party-feed-provider.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/echo.test.ts +100 -1
- package/src/parties/party-core.ts +1 -0
- package/src/parties/party-internal.ts +1 -1
- package/src/pipeline/party-feed-provider.ts +40 -14
package/src/echo.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import debug from 'debug';
|
|
|
7
7
|
import expect from 'expect';
|
|
8
8
|
import { it as test } from 'mocha';
|
|
9
9
|
|
|
10
|
-
import { latch, waitForCondition } from '@dxos/async';
|
|
10
|
+
import { latch, promiseTimeout, waitForCondition } from '@dxos/async';
|
|
11
11
|
import { defaultSecretProvider, defaultSecretValidator } from '@dxos/credentials';
|
|
12
12
|
import { generateSeedPhrase, keyPairFromSeedPhrase, createKeyPair } from '@dxos/crypto';
|
|
13
13
|
import { ObjectModel } from '@dxos/object-model';
|
|
@@ -329,6 +329,105 @@ describe('ECHO', () => {
|
|
|
329
329
|
expect(a.queryParties().value[1].key).toEqual(b.queryParties().value[1].key);
|
|
330
330
|
}).timeout(10_000);
|
|
331
331
|
|
|
332
|
+
test('Mutations from another device', async () => {
|
|
333
|
+
const a = await setup({ createProfile: true });
|
|
334
|
+
const b = await setup();
|
|
335
|
+
|
|
336
|
+
await a.createParty();
|
|
337
|
+
|
|
338
|
+
const invitation = await a.halo.createInvitation(defaultInvitationAuthenticator);
|
|
339
|
+
await b.halo.join(invitation, defaultSecretProvider);
|
|
340
|
+
|
|
341
|
+
// Check the initial party is opened.
|
|
342
|
+
await waitForCondition(() => b.queryParties().value.length === 1, 1000);
|
|
343
|
+
|
|
344
|
+
const partyA = a.queryParties().value[0];
|
|
345
|
+
await partyA.open();
|
|
346
|
+
const partyB = b.queryParties().value[0];
|
|
347
|
+
await partyB.open();
|
|
348
|
+
|
|
349
|
+
{
|
|
350
|
+
// Subscribe to Item updates on B.
|
|
351
|
+
const selection = partyA.database.select({ type: 'example:item/test' }).exec();
|
|
352
|
+
const updated = selection.update.waitFor(result => result.entities.length > 0);
|
|
353
|
+
|
|
354
|
+
// Create a new Item on A.
|
|
355
|
+
const itemB = await partyB.database
|
|
356
|
+
.createItem({ model: ObjectModel, type: 'example:item/test' }) as Item<any>;
|
|
357
|
+
log(`A created ${itemB.id}`);
|
|
358
|
+
|
|
359
|
+
// Now wait to see it on B.
|
|
360
|
+
await updated;
|
|
361
|
+
log(`B has ${itemB.id}`);
|
|
362
|
+
|
|
363
|
+
expect(selection.entities[0].id).toEqual(itemB.id);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
await a.close();
|
|
367
|
+
await b.close();
|
|
368
|
+
|
|
369
|
+
await a.open();
|
|
370
|
+
|
|
371
|
+
{
|
|
372
|
+
const partyA = a.queryParties().first;
|
|
373
|
+
|
|
374
|
+
expect(partyA.database.select({ type: 'example:item/test' }).exec().entities.length > 0).toEqual(true);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
}).timeout(10_000);
|
|
378
|
+
|
|
379
|
+
test.skip('3 devices', async () => {
|
|
380
|
+
const a = await setup({ createProfile: true });
|
|
381
|
+
const b = await setup();
|
|
382
|
+
|
|
383
|
+
await a.createParty();
|
|
384
|
+
|
|
385
|
+
await b.halo.join(await a.halo.createInvitation(defaultInvitationAuthenticator), defaultSecretProvider);
|
|
386
|
+
|
|
387
|
+
// Check the initial party is opened.
|
|
388
|
+
await waitForCondition(() => b.queryParties().value.length === 1, 1000);
|
|
389
|
+
|
|
390
|
+
const partyA = a.queryParties().value[0];
|
|
391
|
+
await partyA.open();
|
|
392
|
+
const partyB = b.queryParties().value[0];
|
|
393
|
+
await partyB.open();
|
|
394
|
+
|
|
395
|
+
{
|
|
396
|
+
// Subscribe to Item updates on B.
|
|
397
|
+
const selection = partyA.database.select({ type: 'example:item/test' }).exec();
|
|
398
|
+
const updated = selection.update.waitFor(result => result.entities.length > 0);
|
|
399
|
+
|
|
400
|
+
// Create a new Item on A.
|
|
401
|
+
const itemB = await partyB.database
|
|
402
|
+
.createItem({ model: ObjectModel, type: 'example:item/test' }) as Item<any>;
|
|
403
|
+
log(`A created ${itemB.id}`);
|
|
404
|
+
|
|
405
|
+
// Now wait to see it on B.
|
|
406
|
+
await updated;
|
|
407
|
+
log(`B has ${itemB.id}`);
|
|
408
|
+
|
|
409
|
+
expect(selection.entities[0].id).toEqual(itemB.id);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
await a.close();
|
|
413
|
+
await b.close();
|
|
414
|
+
|
|
415
|
+
await a.open();
|
|
416
|
+
|
|
417
|
+
{
|
|
418
|
+
const partyA = a.queryParties().first;
|
|
419
|
+
|
|
420
|
+
expect(partyA.database.select({ type: 'example:item/test' }).exec().entities.length > 0).toEqual(true);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const c = await setup();
|
|
424
|
+
await c.halo.join(await a.halo.createInvitation(defaultInvitationAuthenticator), defaultSecretProvider);
|
|
425
|
+
await waitForCondition(() => c.queryParties().value.length === 1, 1000);
|
|
426
|
+
const partyC = c.queryParties().first;
|
|
427
|
+
|
|
428
|
+
await promiseTimeout(partyC.database.waitForItem({ type: 'example:item/test' }), 1000, new Error('timeout'));
|
|
429
|
+
}).timeout(10_000);
|
|
430
|
+
|
|
332
431
|
test('Two users, two devices each', async () => {
|
|
333
432
|
const a1 = await setup({ createProfile: true });
|
|
334
433
|
const a2 = await setup();
|
|
@@ -122,6 +122,7 @@ export class PartyCore {
|
|
|
122
122
|
// Pipeline
|
|
123
123
|
//
|
|
124
124
|
|
|
125
|
+
await this._feedProvider.openKnownFeeds();
|
|
125
126
|
const iterator = await this._feedProvider.createIterator(
|
|
126
127
|
createMessageSelector(this._partyProcessor, this._timeframeClock),
|
|
127
128
|
this._initialTimeframe
|
|
@@ -78,7 +78,7 @@ export class PartyInternal {
|
|
|
78
78
|
key: this.key.toHex(),
|
|
79
79
|
isOpen: this.isOpen,
|
|
80
80
|
isActive: this.isActive,
|
|
81
|
-
feedKeys: this._feedProvider.
|
|
81
|
+
feedKeys: this._feedProvider.getFeeds().length,
|
|
82
82
|
timeframe: this.isOpen ? this._partyCore.timeframe : undefined,
|
|
83
83
|
properties: this.isOpen ? this.getPropertiesSet().expectOne().model.toObject() : undefined
|
|
84
84
|
};
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
import assert from 'assert';
|
|
6
6
|
import debug from 'debug';
|
|
7
7
|
|
|
8
|
+
import { Event } from '@dxos/async';
|
|
8
9
|
import { Keyring, KeyType } from '@dxos/credentials';
|
|
9
10
|
import { PublicKey } from '@dxos/crypto';
|
|
10
11
|
import { FeedStoreIterator, MessageSelector, Timeframe } from '@dxos/echo-protocol';
|
|
11
12
|
import { FeedDescriptor, FeedStore } from '@dxos/feed-store';
|
|
13
|
+
import { ComplexMap } from '@dxos/util';
|
|
12
14
|
|
|
13
15
|
import { MetadataStore } from '../metadata';
|
|
14
16
|
|
|
@@ -16,6 +18,9 @@ const STALL_TIMEOUT = 1000;
|
|
|
16
18
|
const warn = debug('dxos:echo-db:party-feed-provider:warn');
|
|
17
19
|
|
|
18
20
|
export class PartyFeedProvider {
|
|
21
|
+
private readonly _feeds = new ComplexMap<PublicKey, FeedDescriptor>(x => x.toHex())
|
|
22
|
+
readonly feedOpened = new Event<FeedDescriptor>();
|
|
23
|
+
|
|
19
24
|
constructor (
|
|
20
25
|
private readonly _metadataStore: MetadataStore,
|
|
21
26
|
private readonly _keyring: Keyring,
|
|
@@ -23,14 +28,13 @@ export class PartyFeedProvider {
|
|
|
23
28
|
private readonly _partyKey: PublicKey
|
|
24
29
|
) {}
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
getFeeds (): FeedDescriptor[] {
|
|
32
|
+
return Array.from(this._feeds.values());
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
async createOrOpenWritableFeed () {
|
|
28
36
|
const partyMetadata = this._metadataStore.getParty(this._partyKey);
|
|
29
|
-
if (!partyMetadata) {
|
|
30
|
-
return this._createReadWriteFeed();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!partyMetadata.dataFeedKey) {
|
|
37
|
+
if (!partyMetadata?.dataFeedKey) {
|
|
34
38
|
return this._createReadWriteFeed();
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -39,22 +43,42 @@ export class PartyFeedProvider {
|
|
|
39
43
|
return this._createReadWriteFeed();
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
if (this._feeds.has(fullKey.publicKey)) {
|
|
47
|
+
return this._feeds.get(fullKey.publicKey)!;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
const feed = await this._feedStore.openReadWriteFeed(fullKey.publicKey, fullKey.secretKey);
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
this._feeds.set(fullKey.publicKey, feed);
|
|
52
|
+
this.feedOpened.emit(feed);
|
|
45
53
|
return feed;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
async openKnownFeeds () {
|
|
57
|
+
for (const feedKey of this._metadataStore.getParty(this._partyKey)?.feedKeys ?? []) {
|
|
58
|
+
if (!this._feeds.has(feedKey)) {
|
|
59
|
+
const fullKey = this._keyring.getFullKey(feedKey);
|
|
60
|
+
const feed = fullKey?.secretKey
|
|
61
|
+
? await this._feedStore.openReadWriteFeed(fullKey.publicKey, fullKey.secretKey)
|
|
62
|
+
: await this._feedStore.openReadOnlyFeed(feedKey);
|
|
63
|
+
this._feeds.set(feedKey, feed);
|
|
64
|
+
this.feedOpened.emit(feed);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
async createOrOpenReadOnlyFeed (feedKey: PublicKey): Promise<FeedDescriptor> {
|
|
70
|
+
if (this._feeds.has(feedKey)) {
|
|
71
|
+
return this._feeds.get(feedKey)!;
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
await this._metadataStore.addPartyFeed(this._partyKey, feedKey);
|
|
54
75
|
if (!this._keyring.hasKey(feedKey)) {
|
|
55
76
|
await this._keyring.addPublicKey({ type: KeyType.FEED, publicKey: feedKey });
|
|
56
77
|
}
|
|
57
|
-
|
|
78
|
+
const feed = await this._feedStore.openReadOnlyFeed(feedKey);
|
|
79
|
+
this._feeds.set(feedKey, feed);
|
|
80
|
+
this.feedOpened.emit(feed);
|
|
81
|
+
return feed;
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
private async _createReadWriteFeed () {
|
|
@@ -63,16 +87,18 @@ export class PartyFeedProvider {
|
|
|
63
87
|
assert(fullKey && fullKey.secretKey);
|
|
64
88
|
await this._metadataStore.setDataFeed(this._partyKey, fullKey.publicKey);
|
|
65
89
|
const feed = await this._feedStore.openReadWriteFeed(fullKey.publicKey, fullKey.secretKey);
|
|
90
|
+
this._feeds.set(fullKey.publicKey, feed);
|
|
91
|
+
this.feedOpened.emit(feed);
|
|
66
92
|
return feed;
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
async createIterator (messageSelector: MessageSelector, initialTimeframe?: Timeframe) {
|
|
70
96
|
const iterator = new FeedStoreIterator(() => true, messageSelector, initialTimeframe ?? new Timeframe());
|
|
71
|
-
for (const
|
|
72
|
-
iterator.addFeedDescriptor(
|
|
97
|
+
for (const feed of this._feeds.values()) {
|
|
98
|
+
iterator.addFeedDescriptor(feed);
|
|
73
99
|
}
|
|
74
100
|
|
|
75
|
-
this.
|
|
101
|
+
this.feedOpened.on((descriptor) => {
|
|
76
102
|
if (this._metadataStore.getParty(this._partyKey)?.feedKeys?.find(feedKey => feedKey.equals(descriptor.key))) {
|
|
77
103
|
iterator.addFeedDescriptor(descriptor);
|
|
78
104
|
}
|