@dxos/echo-db 2.33.5-dev.c37556a4 → 2.33.5-dev.c6e30fbc

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.
Files changed (163) hide show
  1. package/dist/src/api/index.js +5 -1
  2. package/dist/src/api/index.js.map +1 -1
  3. package/dist/src/echo.d.ts.map +1 -1
  4. package/dist/src/echo.js +1 -1
  5. package/dist/src/echo.js.map +1 -1
  6. package/dist/src/echo.test.js +12 -4
  7. package/dist/src/echo.test.js.map +1 -1
  8. package/dist/src/halo/halo-factory.d.ts +4 -4
  9. package/dist/src/halo/halo-factory.d.ts.map +1 -1
  10. package/dist/src/halo/halo-factory.js +3 -2
  11. package/dist/src/halo/halo-factory.js.map +1 -1
  12. package/dist/src/halo/halo-party.d.ts +4 -3
  13. package/dist/src/halo/halo-party.d.ts.map +1 -1
  14. package/dist/src/halo/halo-party.js +8 -4
  15. package/dist/src/halo/halo-party.js.map +1 -1
  16. package/dist/src/halo/halo.d.ts +2 -2
  17. package/dist/src/halo/halo.d.ts.map +1 -1
  18. package/dist/src/halo/index.js +5 -1
  19. package/dist/src/halo/index.js.map +1 -1
  20. package/dist/src/halo/party-opener.d.ts.map +1 -1
  21. package/dist/src/halo/party-opener.js +3 -1
  22. package/dist/src/halo/party-opener.js.map +1 -1
  23. package/dist/src/index.js +5 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/invitations/greeting-initiator.d.ts +4 -5
  26. package/dist/src/invitations/greeting-initiator.d.ts.map +1 -1
  27. package/dist/src/invitations/greeting-initiator.js +4 -4
  28. package/dist/src/invitations/greeting-initiator.js.map +1 -1
  29. package/dist/src/invitations/halo-recovery-initiator.d.ts +1 -1
  30. package/dist/src/invitations/halo-recovery-initiator.d.ts.map +1 -1
  31. package/dist/src/invitations/halo-recovery-initiator.js +1 -1
  32. package/dist/src/invitations/halo-recovery-initiator.js.map +1 -1
  33. package/dist/src/invitations/index.js +5 -1
  34. package/dist/src/invitations/index.js.map +1 -1
  35. package/dist/src/invitations/invitation-descriptor.js +5 -1
  36. package/dist/src/invitations/invitation-descriptor.js.map +1 -1
  37. package/dist/src/invitations/offline-invitation-claimer.js +1 -1
  38. package/dist/src/invitations/offline-invitation-claimer.js.map +1 -1
  39. package/dist/src/packlets/database/data-mirror.test.js +2 -2
  40. package/dist/src/packlets/database/data-mirror.test.js.map +1 -1
  41. package/dist/src/packlets/database/database-backend.d.ts +4 -5
  42. package/dist/src/packlets/database/database-backend.d.ts.map +1 -1
  43. package/dist/src/packlets/database/database-backend.js +5 -6
  44. package/dist/src/packlets/database/database-backend.js.map +1 -1
  45. package/dist/src/packlets/database/index.js +5 -1
  46. package/dist/src/packlets/database/index.js.map +1 -1
  47. package/dist/src/packlets/database/item-demuxer.d.ts +2 -2
  48. package/dist/src/packlets/database/item-demuxer.d.ts.map +1 -1
  49. package/dist/src/packlets/database/item-demuxer.js +2 -3
  50. package/dist/src/packlets/database/item-demuxer.js.map +1 -1
  51. package/dist/src/packlets/database/item-demuxer.test.js +13 -9
  52. package/dist/src/packlets/database/item-demuxer.test.js.map +1 -1
  53. package/dist/src/packlets/database/selection/index.js +5 -1
  54. package/dist/src/packlets/database/selection/index.js.map +1 -1
  55. package/dist/src/packlets/database/testing.d.ts.map +1 -1
  56. package/dist/src/packlets/database/testing.js +3 -4
  57. package/dist/src/packlets/database/testing.js.map +1 -1
  58. package/dist/src/packlets/database/timeframe-clock.d.ts +1 -0
  59. package/dist/src/packlets/database/timeframe-clock.d.ts.map +1 -1
  60. package/dist/src/packlets/database/timeframe-clock.js +3 -0
  61. package/dist/src/packlets/database/timeframe-clock.js.map +1 -1
  62. package/dist/src/parties/data-party.d.ts +6 -5
  63. package/dist/src/parties/data-party.d.ts.map +1 -1
  64. package/dist/src/parties/data-party.js +26 -5
  65. package/dist/src/parties/data-party.js.map +1 -1
  66. package/dist/src/parties/data-party.test.js +24 -5
  67. package/dist/src/parties/data-party.test.js.map +1 -1
  68. package/dist/src/parties/index.js +5 -1
  69. package/dist/src/parties/index.js.map +1 -1
  70. package/dist/src/parties/party-factory.d.ts +5 -4
  71. package/dist/src/parties/party-factory.d.ts.map +1 -1
  72. package/dist/src/parties/party-factory.js +15 -11
  73. package/dist/src/parties/party-factory.js.map +1 -1
  74. package/dist/src/parties/party-manager.d.ts +3 -4
  75. package/dist/src/parties/party-manager.d.ts.map +1 -1
  76. package/dist/src/parties/party-manager.js +8 -7
  77. package/dist/src/parties/party-manager.js.map +1 -1
  78. package/dist/src/parties/party-manager.test.js +9 -9
  79. package/dist/src/parties/party-manager.test.js.map +1 -1
  80. package/dist/src/pipeline/{pipeline.d.ts → feed-muxer.d.ts} +8 -11
  81. package/dist/src/pipeline/feed-muxer.d.ts.map +1 -0
  82. package/dist/src/pipeline/{pipeline.js → feed-muxer.js} +34 -41
  83. package/dist/src/pipeline/feed-muxer.js.map +1 -0
  84. package/dist/src/pipeline/feed-muxer.test.d.ts +2 -0
  85. package/dist/src/pipeline/feed-muxer.test.d.ts.map +1 -0
  86. package/dist/src/pipeline/{pipeline.test.js → feed-muxer.test.js} +17 -15
  87. package/dist/src/pipeline/feed-muxer.test.js.map +1 -0
  88. package/dist/src/pipeline/index.d.ts +2 -2
  89. package/dist/src/pipeline/index.d.ts.map +1 -1
  90. package/dist/src/pipeline/index.js +7 -3
  91. package/dist/src/pipeline/index.js.map +1 -1
  92. package/dist/src/pipeline/message-selector.d.ts.map +1 -1
  93. package/dist/src/pipeline/message-selector.js +6 -5
  94. package/dist/src/pipeline/message-selector.js.map +1 -1
  95. package/dist/src/pipeline/metadata-store.d.ts +2 -1
  96. package/dist/src/pipeline/metadata-store.d.ts.map +1 -1
  97. package/dist/src/pipeline/metadata-store.js +7 -1
  98. package/dist/src/pipeline/metadata-store.js.map +1 -1
  99. package/dist/src/pipeline/party-feed-provider.d.ts +2 -3
  100. package/dist/src/pipeline/party-feed-provider.d.ts.map +1 -1
  101. package/dist/src/pipeline/party-feed-provider.js +2 -17
  102. package/dist/src/pipeline/party-feed-provider.js.map +1 -1
  103. package/dist/src/pipeline/{party-core.d.ts → party-pipeline.d.ts} +23 -9
  104. package/dist/src/pipeline/party-pipeline.d.ts.map +1 -0
  105. package/dist/src/pipeline/{party-core.js → party-pipeline.js} +26 -25
  106. package/dist/src/pipeline/party-pipeline.js.map +1 -0
  107. package/dist/src/pipeline/party-pipeline.test.d.ts +2 -0
  108. package/dist/src/pipeline/party-pipeline.test.d.ts.map +1 -0
  109. package/dist/src/pipeline/{party-core.test.js → party-pipeline.test.js} +50 -33
  110. package/dist/src/pipeline/party-pipeline.test.js.map +1 -0
  111. package/dist/src/protocol/index.js +5 -1
  112. package/dist/src/protocol/index.js.map +1 -1
  113. package/dist/src/snapshots/index.js +5 -1
  114. package/dist/src/snapshots/index.js.map +1 -1
  115. package/dist/src/snapshots/snapshot-generator.d.ts +2 -2
  116. package/dist/src/snapshots/snapshot-generator.d.ts.map +1 -1
  117. package/dist/src/snapshots/snapshot-generator.js.map +1 -1
  118. package/dist/src/snapshots/snapshot-store.js +1 -1
  119. package/dist/src/snapshots/snapshot-store.js.map +1 -1
  120. package/dist/src/testing/index.js +5 -1
  121. package/dist/src/testing/index.js.map +1 -1
  122. package/dist/tsconfig.tsbuildinfo +1 -1
  123. package/package.json +18 -18
  124. package/src/echo.test.ts +15 -4
  125. package/src/echo.ts +1 -0
  126. package/src/halo/halo-factory.ts +6 -6
  127. package/src/halo/halo-party.ts +10 -8
  128. package/src/halo/halo.ts +2 -2
  129. package/src/halo/party-opener.ts +3 -1
  130. package/src/invitations/greeting-initiator.ts +10 -9
  131. package/src/invitations/halo-recovery-initiator.ts +3 -3
  132. package/src/invitations/offline-invitation-claimer.ts +2 -2
  133. package/src/packlets/database/data-mirror.test.ts +2 -2
  134. package/src/packlets/database/database-backend.ts +7 -6
  135. package/src/packlets/database/item-demuxer.test.ts +20 -18
  136. package/src/packlets/database/item-demuxer.ts +5 -4
  137. package/src/packlets/database/testing.ts +3 -6
  138. package/src/packlets/database/timeframe-clock.ts +4 -0
  139. package/src/parties/data-party.test.ts +39 -8
  140. package/src/parties/data-party.ts +31 -9
  141. package/src/parties/party-factory.ts +19 -17
  142. package/src/parties/party-manager.test.ts +15 -9
  143. package/src/parties/party-manager.ts +9 -6
  144. package/src/pipeline/{pipeline.test.ts → feed-muxer.test.ts} +20 -16
  145. package/src/pipeline/{pipeline.ts → feed-muxer.ts} +39 -51
  146. package/src/pipeline/index.ts +2 -2
  147. package/src/pipeline/message-selector.ts +6 -5
  148. package/src/pipeline/metadata-store.ts +8 -2
  149. package/src/pipeline/party-feed-provider.ts +3 -16
  150. package/src/pipeline/{party-core.test.ts → party-pipeline.test.ts} +49 -27
  151. package/src/pipeline/{party-core.ts → party-pipeline.ts} +53 -24
  152. package/src/snapshots/snapshot-generator.ts +2 -2
  153. package/src/snapshots/snapshot-store.ts +1 -1
  154. package/dist/src/pipeline/party-core.d.ts.map +0 -1
  155. package/dist/src/pipeline/party-core.js.map +0 -1
  156. package/dist/src/pipeline/party-core.test.d.ts +0 -2
  157. package/dist/src/pipeline/party-core.test.d.ts.map +0 -1
  158. package/dist/src/pipeline/party-core.test.js.map +0 -1
  159. package/dist/src/pipeline/pipeline.d.ts.map +0 -1
  160. package/dist/src/pipeline/pipeline.js.map +0 -1
  161. package/dist/src/pipeline/pipeline.test.d.ts +0 -2
  162. package/dist/src/pipeline/pipeline.test.d.ts.map +0 -1
  163. package/dist/src/pipeline/pipeline.test.js.map +0 -1
@@ -5,7 +5,6 @@
5
5
  import assert from 'assert';
6
6
 
7
7
  import { synchronized, Event } from '@dxos/async';
8
- import { KeyHint } from '@dxos/credentials';
9
8
  import { PublicKey } from '@dxos/crypto';
10
9
  import { timed } from '@dxos/debug';
11
10
  import { PartyKey, PartySnapshot, Timeframe } from '@dxos/echo-protocol';
@@ -18,7 +17,7 @@ import { ResultSet } from '../api';
18
17
  import { ActivationOptions, PartyPreferences, Preferences } from '../halo';
19
18
  import { InvitationFactory } from '../invitations';
20
19
  import { Database, Item } from '../packlets/database';
21
- import { PartyFeedProvider, PartyProtocolFactory, PartyCore, PartyOptions } from '../pipeline';
20
+ import { PartyFeedProvider, PartyProtocolFactory, PartyPipeline, PipelineOptions, MetadataStore } from '../pipeline';
22
21
  import { createAuthPlugin, createOfflineInvitationPlugin, createAuthenticator, createCredentialsProvider } from '../protocol';
23
22
  import { CredentialsSigner } from '../protocol/credentials-signer';
24
23
  import { createReplicatorPlugin } from '../protocol/replicator-plugin';
@@ -43,31 +42,31 @@ export interface PartyMember {
43
42
  export class DataParty {
44
43
  public readonly update = new Event<void>();
45
44
 
46
- private readonly _partyCore: PartyCore;
45
+ private readonly _partyCore: PartyPipeline;
47
46
  private readonly _preferences?: PartyPreferences;
48
47
  private _invitationManager?: InvitationFactory;
49
48
  private _protocol?: PartyProtocolFactory;
49
+ private _feedHints: PublicKey[] = []
50
50
 
51
51
  constructor (
52
52
  partyKey: PartyKey,
53
53
  modelFactory: ModelFactory,
54
54
  snapshotStore: SnapshotStore,
55
55
  private readonly _feedProvider: PartyFeedProvider,
56
+ private readonly _metadataStore: MetadataStore,
56
57
  private readonly _credentialsSigner: CredentialsSigner,
57
58
  // TODO(dmaretskyi): Pull this out to a higher level. Should preferences be part of client API instead?
58
59
  private readonly _profilePreferences: Preferences | undefined,
59
60
  private readonly _networkManager: NetworkManager,
60
- private readonly _hints: KeyHint[] = [],
61
- _initialTimeframe?: Timeframe,
62
- _options: PartyOptions = {}
61
+ private readonly _initialTimeframe?: Timeframe,
62
+ _options: PipelineOptions = {}
63
63
  ) {
64
- this._partyCore = new PartyCore(
64
+ this._partyCore = new PartyPipeline(
65
65
  partyKey,
66
66
  _feedProvider,
67
67
  modelFactory,
68
68
  snapshotStore,
69
69
  this._credentialsSigner.getIdentityKey().publicKey,
70
- _initialTimeframe,
71
70
  _options
72
71
  );
73
72
 
@@ -142,6 +141,13 @@ export class DataParty {
142
141
  await this._preferences?.setLastKnownTitle(title);
143
142
  }
144
143
 
144
+ /**
145
+ * @internal
146
+ */
147
+ _setFeedHints (feedHints: PublicKey[]) {
148
+ this._feedHints = feedHints;
149
+ }
150
+
145
151
  /**
146
152
  * Opens the pipeline and connects the streams.
147
153
  */
@@ -152,7 +158,20 @@ export class DataParty {
152
158
  return this;
153
159
  }
154
160
 
155
- await this._partyCore.open(this._hints);
161
+ // TODO(dmaretskyi): May be undefined in some tests.
162
+ const party = this._metadataStore.getParty(this._partyCore.key);
163
+
164
+ await this._partyCore.open({
165
+ feedHints: this._feedHints,
166
+ initialTimeframe: this._initialTimeframe,
167
+ targetTimeframe: party?.latestTimeframe
168
+ });
169
+
170
+ // Keep updating latest reached timeframe in the metadata.
171
+ // This timeframe will be waited for when opening the party next time.
172
+ this._partyCore.timeframeUpdate.on(timeframe => {
173
+ void this._metadataStore.setTimeframe(this._partyCore.key, timeframe);
174
+ });
156
175
 
157
176
  this._invitationManager = new InvitationFactory(
158
177
  this._partyCore.processor,
@@ -197,6 +216,9 @@ export class DataParty {
197
216
  return this;
198
217
  }
199
218
 
219
+ // Save the latest reached timeframe.
220
+ await this._metadataStore.setTimeframe(this._partyCore.key, this._partyCore.timeframe);
221
+
200
222
  await this._partyCore.close();
201
223
  await this._protocol?.stop();
202
224
 
@@ -9,7 +9,6 @@ import {
9
9
  createEnvelopeMessage,
10
10
  createFeedAdmitMessage,
11
11
  createPartyGenesisMessage,
12
- KeyHint,
13
12
  KeyType,
14
13
  SecretProvider,
15
14
  wrapMessage
@@ -26,7 +25,7 @@ import {
26
25
  GreetingInitiator, InvitationDescriptor, InvitationDescriptorType, OfflineInvitationClaimer
27
26
  } from '../invitations';
28
27
  import { IdentityNotInitializedError } from '../packlets/errors';
29
- import { PartyFeedProvider, PartyOptions } from '../pipeline';
28
+ import { MetadataStore, PartyFeedProvider, PipelineOptions } from '../pipeline';
30
29
  import { IdentityCredentialsProvider } from '../protocol/identity-credentials';
31
30
  import { SnapshotStore } from '../snapshots';
32
31
  import { DataParty, PARTY_ITEM_TYPE } from './data-party';
@@ -43,7 +42,8 @@ export class PartyFactory {
43
42
  private readonly _modelFactory: ModelFactory,
44
43
  private readonly _snapshotStore: SnapshotStore,
45
44
  private readonly _feedProviderFactory: (partyKey: PublicKey) => PartyFeedProvider,
46
- private readonly _options: PartyOptions = {}
45
+ private readonly _metadataStore: MetadataStore,
46
+ private readonly _options: PipelineOptions = {}
47
47
  ) {}
48
48
 
49
49
  /**
@@ -57,11 +57,13 @@ export class PartyFactory {
57
57
  const partyKey = await identity.keyring.createKeyRecord({ type: KeyType.PARTY });
58
58
  const party = await this.constructParty(partyKey.publicKey);
59
59
 
60
+ const writableFeed = await party.getWriteFeed();
61
+ // Hint at the newly created writable feed so that we can start replicating from it.
62
+ party._setFeedHints([writableFeed.key]);
63
+
60
64
  // Connect the pipeline.
61
65
  await party.open();
62
66
 
63
- const writableFeed = await party.getWriteFeed();
64
-
65
67
  // PartyGenesis (self-signed by Party).
66
68
  await party.credentialsWriter.write(createPartyGenesisMessage(
67
69
  identity.keyring,
@@ -111,12 +113,9 @@ export class PartyFactory {
111
113
  * @param partyKey
112
114
  * @param hints
113
115
  */
114
- async constructParty (partyKey: PartyKey, hints: KeyHint[] = [], initialTimeframe?: Timeframe) {
116
+ async constructParty (partyKey: PartyKey, initialTimeframe?: Timeframe) {
115
117
  const identity = this._identityProvider() ?? raise(new IdentityNotInitializedError());
116
118
 
117
- // TODO(marik-d): Support read-only parties if this feed doesn't exist?
118
- const feedProvider = this._feedProviderFactory(partyKey);
119
-
120
119
  //
121
120
  // Create the party.
122
121
  //
@@ -124,11 +123,11 @@ export class PartyFactory {
124
123
  partyKey,
125
124
  this._modelFactory,
126
125
  this._snapshotStore,
127
- feedProvider,
126
+ this._feedProviderFactory(partyKey),
127
+ this._metadataStore,
128
128
  identity.createCredentialsSigner(),
129
129
  identity.preferences,
130
130
  this._networkManager,
131
- hints,
132
131
  initialTimeframe,
133
132
  this._options
134
133
  );
@@ -140,7 +139,7 @@ export class PartyFactory {
140
139
  assert(snapshot.partyKey);
141
140
  log(`Constructing ${humanize(snapshot.partyKey)} from snapshot at ${JSON.stringify(snapshot.timeframe)}.`);
142
141
 
143
- const party = await this.constructParty(PublicKey.from(snapshot.partyKey), [], snapshot.timeframe);
142
+ const party = await this.constructParty(PublicKey.from(snapshot.partyKey), snapshot.timeframe);
144
143
  await party.restoreFromSnapshot(snapshot);
145
144
  return party;
146
145
  }
@@ -173,7 +172,8 @@ export class PartyFactory {
173
172
 
174
173
  await initiator.connect();
175
174
  const { partyKey, hints } = await initiator.redeemInvitation(secretProvider);
176
- const party = await this.constructParty(partyKey, hints);
175
+ const party = await this.constructParty(partyKey);
176
+ party._setFeedHints(hints);
177
177
  await party.open();
178
178
  await initiator.destroy();
179
179
 
@@ -198,11 +198,13 @@ export class PartyFactory {
198
198
  const partyKey = await identity.keyring.createKeyRecord({ type: KeyType.PARTY });
199
199
  const party = await this.constructParty(partyKey.publicKey);
200
200
 
201
+ const writableFeed = await party.getWriteFeed();
202
+ // Hint at the newly created writable feed so that we can start replicating from it.
203
+ party._setFeedHints([writableFeed.key]);
204
+
201
205
  // Connect the pipeline.
202
206
  await party.open();
203
207
 
204
- const writableFeed = await party.getWriteFeed();
205
-
206
208
  // PartyGenesis (self-signed by Party).
207
209
  await party.credentialsWriter.write(createPartyGenesisMessage(
208
210
  identity.keyring,
@@ -254,6 +256,7 @@ export class PartyFactory {
254
256
  const feedWriter = createFeedWriter(writableFeed.feed);
255
257
  for (const item of snapshot.database?.items || []) {
256
258
  const message: FeedMessage = {
259
+ timeframe: new Timeframe(),
257
260
  echo: {
258
261
  itemId: item.itemId ?? failUndefined(),
259
262
  genesis: {
@@ -264,8 +267,7 @@ export class PartyFactory {
264
267
  itemMutation: {
265
268
  parentId: item.parentId
266
269
  },
267
- snapshot: item.model,
268
- timeframe: new Timeframe()
270
+ snapshot: item.model
269
271
  }
270
272
  };
271
273
  await feedWriter.write(message);
@@ -71,6 +71,7 @@ const setup = async () => {
71
71
  modelFactory,
72
72
  snapshotStore,
73
73
  feedProviderFactory,
74
+ metadataStore,
74
75
  {
75
76
  writeLogger: messageLogger('<<<'),
76
77
  readLogger: messageLogger('>>>')
@@ -140,16 +141,12 @@ describe('Party manager', () => {
140
141
  const feedStream = createWritableFeedStream(feed);
141
142
  feedStream.write(createPartyGenesisMessage(keyring, partyKey, feedKey.publicKey, identityKey));
142
143
 
143
- await partyManager.addParty(partyKey.publicKey, [{
144
- type: KeyType.FEED,
145
- publicKey: PublicKey.from(feed.key)
146
- }]);
144
+ await partyManager.addParty(partyKey.publicKey, [PublicKey.from(feed.key)]);
147
145
 
148
146
  await update;
149
147
  });
150
148
 
151
149
  test('Create from cold start', async () => {
152
-
153
150
  const storage = createStorage('', StorageType.RAM);
154
151
  const feedStore = new FeedStore(storage.directory('feed'), { valueEncoding: codec });
155
152
  const keyring = new Keyring();
@@ -165,7 +162,8 @@ describe('Party manager', () => {
165
162
  networkManager,
166
163
  modelFactory,
167
164
  snapshotStore,
168
- feedProviderFactory
165
+ feedProviderFactory,
166
+ metadataStore
169
167
  );
170
168
  const partyManager = new PartyManager(metadataStore, snapshotStore, () => identity, partyFactory);
171
169
 
@@ -191,15 +189,23 @@ describe('Party manager', () => {
191
189
  assert(feedKey);
192
190
 
193
191
  const feedStream = createWritableFeedStream(feed);
194
- feedStream.write({ halo: createPartyGenesisMessage(keyring, partyKey, feedKey.publicKey, identity.identityKey) });
195
192
  feedStream.write({
193
+ timeframe: new Timeframe(),
194
+ halo: createPartyGenesisMessage(
195
+ keyring,
196
+ partyKey,
197
+ feedKey.publicKey,
198
+ identity.identityKey
199
+ )
200
+ });
201
+ feedStream.write({
202
+ timeframe: new Timeframe(),
196
203
  echo: checkType<EchoEnvelope>({
197
204
  itemId: 'foo',
198
205
  genesis: {
199
206
  itemType: PARTY_ITEM_TYPE,
200
207
  modelType: ObjectModel.meta.type
201
- },
202
- timeframe: new Timeframe()
208
+ }
203
209
  })
204
210
  });
205
211
  }
@@ -9,7 +9,7 @@ import unionWith from 'lodash.unionwith';
9
9
  import { Event, synchronized } from '@dxos/async';
10
10
  import { KeyHint, KeyType, SecretProvider } from '@dxos/credentials';
11
11
  import { PublicKey } from '@dxos/crypto';
12
- import { timed } from '@dxos/debug';
12
+ import { failUndefined, timed } from '@dxos/debug';
13
13
  import { PartyKey, PartySnapshot } from '@dxos/echo-protocol';
14
14
  import { ComplexMap, boolGuard } from '@dxos/util';
15
15
 
@@ -94,9 +94,13 @@ export class PartyManager {
94
94
  const partyKey = partyKeys[i];
95
95
  if (!this._parties.has(partyKey)) {
96
96
  const snapshot = await this._snapshotStore.load(partyKey);
97
+
98
+ const metadata = this._metadataStore.getParty(partyKey) ?? failUndefined();
99
+
97
100
  const party = snapshot
98
101
  ? await this._partyFactory.constructPartyFromSnapshot(snapshot)
99
102
  : await this._partyFactory.constructParty(partyKey);
103
+ party._setFeedHints(metadata.feedKeys ?? []);
100
104
 
101
105
  const isActive = identity?.preferences?.isPartyActive(partyKey) ?? true;
102
106
  if (isActive) {
@@ -157,11 +161,9 @@ export class PartyManager {
157
161
 
158
162
  /**
159
163
  * Construct a party object and start replicating with the remote peer that created that party.
160
- * @param partyKey
161
- * @param hints
162
164
  */
163
165
  @synchronized
164
- async addParty (partyKey: PartyKey, hints: KeyHint[] = []) {
166
+ async addParty (partyKey: PartyKey, feedHints: PublicKey[] = []) {
165
167
  assert(this._open, 'PartyManager is not open.');
166
168
 
167
169
  /*
@@ -174,8 +176,9 @@ export class PartyManager {
174
176
  return this._parties.get(partyKey);
175
177
  }
176
178
 
177
- log(`Adding party partyKey=${partyKey.toHex()} hints=${hints.length}`);
178
- const party = await this._partyFactory.constructParty(partyKey, hints);
179
+ log(`Adding party partyKey=${partyKey.toHex()} hints=${feedHints.length}`);
180
+ const party = await this._partyFactory.constructParty(partyKey);
181
+ party._setFeedHints(feedHints);
179
182
  await party.open();
180
183
  await this._metadataStore.addParty(party.key);
181
184
  this._setParty(party);
@@ -10,19 +10,19 @@ import { waitForCondition, latch } from '@dxos/async';
10
10
  import { createPartyGenesisMessage, Keyring, KeyType } from '@dxos/credentials';
11
11
  import { createId, createKeyPair, PublicKey } from '@dxos/crypto';
12
12
  import { codec, createFeedWriter, FeedSelector, FeedStoreIterator, IEchoStream, Timeframe } from '@dxos/echo-protocol';
13
- import { FeedStore, createWritableFeedStream, createWritable, WritableArray } from '@dxos/feed-store';
13
+ import { FeedStore, createWritableFeedStream } from '@dxos/feed-store';
14
14
  import { createSetPropertyMutation } from '@dxos/model-factory';
15
15
  import { createStorage, StorageType } from '@dxos/random-access-multi-storage';
16
16
  import { jsonReplacer } from '@dxos/util';
17
17
 
18
18
  import { TimeframeClock } from '../packlets/database';
19
+ import { FeedMuxer } from './feed-muxer';
19
20
  import { PartyProcessor } from './party-processor';
20
- import { Pipeline } from './pipeline';
21
21
 
22
22
  const log = debug('dxos:echo:pipeline:test');
23
23
 
24
24
  // TODO(burdon): Test read-only.
25
- describe('pipeline', () => {
25
+ describe('FeedMuxer', () => {
26
26
  test('streams', async () => {
27
27
  const storage = createStorage('', StorageType.RAM);
28
28
  const feedStore = new FeedStore(storage.directory('feed'), { valueEncoding: codec });
@@ -56,9 +56,6 @@ describe('pipeline', () => {
56
56
  seq: 0
57
57
  }
58
58
  });
59
- const pipeline = new Pipeline(partyProcessor, feedReadStream, new TimeframeClock());
60
- const [readStream] = await pipeline.open();
61
- expect(readStream).toBeTruthy();
62
59
 
63
60
  //
64
61
  // Pipeline consumer.
@@ -66,10 +63,14 @@ describe('pipeline', () => {
66
63
  //
67
64
  const numMessages = 5;
68
65
  const [counter, updateCounter] = latch(numMessages);
69
- readStream.pipe(createWritable<IEchoStream>(async message => {
66
+ const echoProcessor = async (message: IEchoStream) => {
70
67
  log('Processed:', JSON.stringify(message, jsonReplacer, 2));
71
68
  updateCounter();
72
- }));
69
+ };
70
+
71
+ const pipeline = new FeedMuxer(partyProcessor, feedReadStream, new TimeframeClock());
72
+ pipeline.setEchoProcessor(echoProcessor);
73
+ await pipeline.open();
73
74
 
74
75
  //
75
76
  // Write directly to feed store.
@@ -105,17 +106,21 @@ describe('pipeline', () => {
105
106
  });
106
107
 
107
108
  const partyProcessor = new PartyProcessor(partyKey.publicKey);
108
- const pipeline = new Pipeline(
109
+
110
+ const echoMessages: IEchoStream[] = [];
111
+ const echoProcessor = async (msg: IEchoStream) => {
112
+ echoMessages.push(msg);
113
+ };
114
+
115
+ const pipeline = new FeedMuxer(
109
116
  partyProcessor,
110
117
  feedReadStream,
111
118
  new TimeframeClock(),
112
119
  createFeedWriter(feed)
113
120
  );
121
+ pipeline.setEchoProcessor(echoProcessor);
114
122
  await pipeline.open();
115
123
 
116
- const writable = new WritableArray();
117
- pipeline.inboundEchoStream!.pipe(writable);
118
-
119
124
  await pipeline.outboundHaloStream!.write(createPartyGenesisMessage(keyring, partyKey, feedKey.publicKey, identityKey));
120
125
  await waitForCondition(() => !partyProcessor.genesisRequired);
121
126
 
@@ -126,15 +131,14 @@ describe('pipeline', () => {
126
131
  }
127
132
  });
128
133
 
129
- await waitForCondition(() => writable.objects.length === 1);
134
+ await waitForCondition(() => echoMessages.length === 1);
130
135
 
131
136
  expect(partyProcessor.genesisRequired).toEqual(false);
132
- expect((writable.objects[0] as any).data).toEqual({
137
+ expect((echoMessages[0] as any).data).toEqual({
133
138
  itemId: '123',
134
139
  genesis: {
135
140
  itemType: 'foo'
136
- },
137
- timeframe: expect.any(Timeframe)
141
+ }
138
142
  });
139
143
  });
140
144
  });
@@ -4,7 +4,6 @@
4
4
 
5
5
  import assert from 'assert';
6
6
  import debug from 'debug';
7
- import { Readable } from 'stream';
8
7
 
9
8
  import { Event } from '@dxos/async';
10
9
  import { Message as HaloMessage } from '@dxos/credentials';
@@ -13,10 +12,9 @@ import { checkType } from '@dxos/debug';
13
12
  import {
14
13
  createFeedMeta, EchoEnvelope, FeedMessage, FeedStoreIterator, FeedWriter, IEchoStream, mapFeedWriter, Timeframe
15
14
  } from '@dxos/echo-protocol';
16
- import { createReadable } from '@dxos/feed-store';
17
15
  import { jsonReplacer } from '@dxos/util';
18
16
 
19
- import { TimeframeClock } from '../packlets/database';
17
+ import { EchoProcessor, TimeframeClock } from '../packlets/database';
20
18
  import { CredentialProcessor, PartyStateProvider } from './party-processor';
21
19
 
22
20
  interface Options {
@@ -28,14 +26,12 @@ const log = debug('dxos:echo-db:pipeline');
28
26
 
29
27
  /**
30
28
  * Manages the inbound and outbound message streams for an individual party.
29
+ * Reads messages from individual feeds and splits them into ECHO and HALO streams.
31
30
  */
32
- export class Pipeline {
31
+ export class FeedMuxer {
33
32
  private readonly _errors = new Event<Error>();
34
33
 
35
- /**
36
- * Messages to be consumed from the pipeline (e.g., mutations to model).
37
- */
38
- private _inboundEchoStream: Readable | undefined;
34
+ private _isOpen = false;
39
35
 
40
36
  /**
41
37
  * Messages to write into pipeline (e.g., mutations from model).
@@ -47,6 +43,8 @@ export class Pipeline {
47
43
  */
48
44
  private _outboundHaloStream: FeedWriter<HaloMessage> | undefined;
49
45
 
46
+ private _echoProcessor: EchoProcessor | undefined
47
+
50
48
  /**
51
49
  * @param _partyProcessor Processes HALO messages to update party state.
52
50
  * @param _feedStorIterator Inbound messages from the feed store.
@@ -60,20 +58,32 @@ export class Pipeline {
60
58
  private readonly _timeframeClock: TimeframeClock,
61
59
  private readonly _feedWriter?: FeedWriter<FeedMessage>,
62
60
  private readonly _options: Options = {}
63
- ) {}
61
+ ) {
62
+ if (this._feedWriter) {
63
+ const loggingWriter = mapFeedWriter<FeedMessage, FeedMessage>(async msg => {
64
+ this._options.writeLogger?.(msg);
65
+ return msg;
66
+ }, this._feedWriter);
67
+
68
+ this._outboundEchoStream = mapFeedWriter<EchoEnvelope, FeedMessage>(async message => ({
69
+ timeframe: this._timeframeClock.timeframe,
70
+ echo: message
71
+ }), loggingWriter);
72
+ this._outboundHaloStream = mapFeedWriter<HaloMessage, FeedMessage>(async message => ({
73
+ timeframe: this._timeframeClock.timeframe,
74
+ halo: message
75
+ }), loggingWriter);
76
+ }
77
+ }
64
78
 
65
79
  get isOpen () {
66
- return this._inboundEchoStream !== undefined;
80
+ return this._isOpen;
67
81
  }
68
82
 
69
83
  get readOnly () {
70
84
  return this._outboundEchoStream === undefined;
71
85
  }
72
86
 
73
- get inboundEchoStream () {
74
- return this._inboundEchoStream;
75
- }
76
-
77
87
  get outboundEchoStream () {
78
88
  return this._outboundEchoStream;
79
89
  }
@@ -86,6 +96,10 @@ export class Pipeline {
86
96
  return this._errors;
87
97
  }
88
98
 
99
+ setEchoProcessor (processor: EchoProcessor) {
100
+ this._echoProcessor = processor;
101
+ }
102
+
89
103
  /**
90
104
  * Create inbound and outbound pipielines.
91
105
  * https://nodejs.org/api/stream.html#stream_stream_pipeline_source_transforms_destination_callback
@@ -96,10 +110,8 @@ export class Pipeline {
96
110
  * Transform(dxos.echo.IEchoEnvelope => dxos.IFeedMessage): update clock
97
111
  * Feed
98
112
  */
99
- async open (): Promise<[NodeJS.ReadableStream, FeedWriter<EchoEnvelope>?]> {
100
- const { readLogger, writeLogger } = this._options;
101
-
102
- this._inboundEchoStream = createReadable();
113
+ async open (): Promise<FeedWriter<EchoEnvelope> | undefined> {
114
+ const { readLogger } = this._options;
103
115
 
104
116
  // This will exit cleanly once FeedStoreIterator is closed.
105
117
  setImmediate(async () => {
@@ -125,20 +137,19 @@ export class Pipeline {
125
137
  //
126
138
 
127
139
  if (message.echo) {
128
- this._timeframeClock.updateTimeframe(PublicKey.from(block.key), block.seq);
129
140
  const memberKey = this._partyProcessor.getFeedOwningMember(PublicKey.from(block.key));
130
141
  assert(memberKey, `Ownership of feed ${keyToString(block.key)} could not be determined.`);
131
142
 
132
143
  // Validate messge.
133
144
  const { itemId } = message.echo;
134
145
  if (itemId) {
135
- assert(this._inboundEchoStream);
136
- this._inboundEchoStream.push(checkType<IEchoStream>({
146
+ assert(this._echoProcessor);
147
+ await this._echoProcessor(checkType<IEchoStream>({
137
148
  meta: {
138
149
  seq: block.seq,
139
150
  feedKey: block.key,
140
151
  memberKey,
141
- timeframe: message.echo.timeframe ?? new Timeframe()
152
+ timeframe: message.timeframe ?? new Timeframe()
142
153
  },
143
154
  data: message.echo
144
155
  }));
@@ -149,6 +160,8 @@ export class Pipeline {
149
160
  // TODO(burdon): Can we throw and have the pipeline log (without breaking the stream)?
150
161
  log(`Skipping invalid message: ${JSON.stringify(message, jsonReplacer)}`);
151
162
  }
163
+
164
+ this._timeframeClock.updateTimeframe(PublicKey.from(block.key), block.seq);
152
165
  } catch (err: any) {
153
166
  console.error('Error in message processing.');
154
167
  console.error(err);
@@ -156,30 +169,7 @@ export class Pipeline {
156
169
  }
157
170
  });
158
171
 
159
- //
160
- // Processes outbound messages (piped to the feed).
161
- // Sets the current timeframe.
162
- //
163
- if (this._feedWriter) {
164
- const loggingWriter = mapFeedWriter<FeedMessage, FeedMessage>(async msg => {
165
- writeLogger?.(msg);
166
- return msg;
167
- }, this._feedWriter);
168
-
169
- this._outboundEchoStream = mapFeedWriter<EchoEnvelope, FeedMessage>(async message => ({
170
- echo: {
171
- ...message,
172
- timeframe: this._timeframeClock.timeframe
173
- }
174
- }), loggingWriter);
175
- this._outboundHaloStream =
176
- mapFeedWriter<HaloMessage, FeedMessage>(async message => ({ halo: message }), loggingWriter);
177
- }
178
-
179
- return [
180
- this._inboundEchoStream,
181
- this._outboundEchoStream
182
- ];
172
+ return this._outboundEchoStream;
183
173
  }
184
174
 
185
175
  /**
@@ -189,11 +179,9 @@ export class Pipeline {
189
179
  async close () {
190
180
  await this._feedStorIterator.close();
191
181
 
192
- if (this._inboundEchoStream) {
193
- this._inboundEchoStream.destroy();
194
- this._inboundEchoStream = undefined;
195
- }
196
-
197
182
  this._outboundEchoStream = undefined;
183
+ this._outboundHaloStream = undefined;
184
+ this._echoProcessor = undefined;
185
+ this._isOpen = false;
198
186
  }
199
187
  }
@@ -6,6 +6,6 @@ export * from './message-selector';
6
6
  export * from './party-feed-provider';
7
7
  export * from './party-processor';
8
8
  export * from '../protocol/party-protocol-factory';
9
- export * from './pipeline';
10
- export * from './party-core';
9
+ export * from './feed-muxer';
10
+ export * from './party-pipeline';
11
11
  export * from './metadata-store';
@@ -26,27 +26,28 @@ const log = debug('dxos:echo-db:message-selector');
26
26
  export const createMessageSelector = (partyProcessor: PartyStateProvider, timeframeClock: TimeframeClock): MessageSelector => candidates => {
27
27
  // Check ECHO message candidates first since they are less expensive than HALO cancidates.
28
28
  for (let i = 0; i < candidates.length; i++) {
29
- const { data: { echo } } = candidates[i];
29
+ const { data: { timeframe, echo } } = candidates[i];
30
30
  const feedKey = PublicKey.from(candidates[i].key);
31
31
  if (!echo) {
32
32
  continue;
33
33
  }
34
34
 
35
- assert(echo.timeframe);
36
- if (partyProcessor.isFeedAdmitted(feedKey) && !timeframeClock.hasGaps(echo.timeframe)) {
35
+ assert(timeframe);
36
+ if (partyProcessor.isFeedAdmitted(feedKey) && !timeframeClock.hasGaps(timeframe)) {
37
37
  return i;
38
38
  }
39
39
  }
40
40
 
41
41
  // Check HALO message candidates.
42
42
  for (let i = 0; i < candidates.length; i++) {
43
- const { data: { halo } } = candidates[i];
43
+ const { data: { timeframe, halo } } = candidates[i];
44
44
  const feedKey = PublicKey.from(candidates[i].key);
45
45
  if (!halo) {
46
46
  continue;
47
47
  }
48
48
 
49
- if (partyProcessor.isFeedAdmitted(feedKey)) {
49
+ assert(timeframe);
50
+ if (partyProcessor.isFeedAdmitted(feedKey) && !timeframeClock.hasGaps(timeframe)) {
50
51
  return i;
51
52
  }
52
53
 
@@ -7,7 +7,7 @@ import debug from 'debug';
7
7
 
8
8
  import { PublicKey } from '@dxos/crypto';
9
9
  import { failUndefined } from '@dxos/debug';
10
- import { EchoMetadata, PartyMetadata, schema } from '@dxos/echo-protocol';
10
+ import { EchoMetadata, PartyMetadata, schema, Timeframe } from '@dxos/echo-protocol';
11
11
  import { Directory } from '@dxos/random-access-multi-storage';
12
12
 
13
13
  /**
@@ -90,7 +90,7 @@ export class MetadataStore {
90
90
  */
91
91
  async clear (): Promise<void> {
92
92
  log('Clearing all echo metadata...');
93
- await this._directory.destroy();
93
+ await this._directory.delete();
94
94
  }
95
95
 
96
96
  /**
@@ -158,4 +158,10 @@ export class MetadataStore {
158
158
  }
159
159
  return !!party.feedKeys?.find(fk => feedKey.equals(fk));
160
160
  }
161
+
162
+ async setTimeframe (partyKey: PublicKey, timeframe: Timeframe) {
163
+ const party = this.getParty(partyKey) ?? failUndefined();
164
+ party.latestTimeframe = timeframe;
165
+ await this._save();
166
+ }
161
167
  }