@dxos/feed-store 2.12.20 → 2.13.0

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.
@@ -4,6 +4,7 @@
4
4
 
5
5
  /* eslint-disable jest/no-done-callback */
6
6
 
7
+ import assert from 'assert';
7
8
  import hypercore from 'hypercore';
8
9
  import hypertrie from 'hypertrie';
9
10
  import pify from 'pify';
@@ -13,6 +14,7 @@ import { sleep } from '@dxos/async';
13
14
  import { PublicKey, createKeyPair } from '@dxos/crypto';
14
15
  import { IStorage, STORAGE_NODE, STORAGE_RAM, createStorage } from '@dxos/random-access-multi-storage';
15
16
 
17
+ import { FeedDescriptor } from './feed-descriptor';
16
18
  import { FeedStore } from './feed-store';
17
19
  import { HypercoreFeed } from './hypercore-types';
18
20
 
@@ -23,9 +25,8 @@ interface KeyPair {
23
25
 
24
26
  const feedNames = ['booksFeed', 'usersFeed', 'groupsFeed'];
25
27
 
26
- const createFeedStore = async (storage: IStorage, options = {}) => {
28
+ const createFeedStore = (storage: IStorage, options = {}) => {
27
29
  const feedStore = new FeedStore(storage, options);
28
- await feedStore.open();
29
30
  return feedStore;
30
31
  };
31
32
 
@@ -34,23 +35,21 @@ async function createDefault () {
34
35
 
35
36
  return {
36
37
  directory,
37
- feedStore: await createFeedStore(createStorage(directory, STORAGE_NODE), { valueEncoding: 'utf-8' })
38
+ feedStore: createFeedStore(createStorage(directory, STORAGE_NODE), { valueEncoding: 'utf-8' })
38
39
  };
39
40
  }
40
41
 
41
- async function defaultFeeds (feedStore: FeedStore, keys: Record<string, KeyPair>) : Promise<Record<string, HypercoreFeed>> {
42
+ async function defaultFeeds (feedStore: FeedStore, keys: Record<string, KeyPair>) : Promise<Record<string, FeedDescriptor>> {
42
43
  return Object.fromEntries(await Promise.all(Object.entries<KeyPair>(keys).map(async ([feed, keyPair]) =>
43
- feed === 'booksFeed'
44
- ? [feed, await feedStore.createReadWriteFeed({ key: keyPair.key, secretKey: keyPair.secretKey, metadata: { topic: 'books' } })]
45
- : [feed, await feedStore.createReadWriteFeed({ key: keyPair.key, secretKey: keyPair.secretKey })]
44
+ [feed, await feedStore.openReadWriteFeed(keyPair.key, keyPair.secretKey)]
46
45
  )));
47
46
  }
48
47
 
49
- function append (feed: any, message: any) {
48
+ function append (feed: HypercoreFeed, message: any) {
50
49
  return pify(feed.append.bind(feed))(message);
51
50
  }
52
51
 
53
- function head (feed: any) {
52
+ function head (feed: HypercoreFeed) {
54
53
  return pify(feed.head.bind(feed))();
55
54
  }
56
55
 
@@ -67,13 +66,9 @@ describe('FeedStore', () => {
67
66
  test('Config default', async () => {
68
67
  const feedStore = await createFeedStore(createStorage('', STORAGE_RAM));
69
68
  expect(feedStore).toBeInstanceOf(FeedStore);
70
- expect(feedStore.opened).toBeTruthy();
71
69
 
72
70
  const feedStore2 = new FeedStore(createStorage('', STORAGE_RAM));
73
71
  expect(feedStore2).toBeInstanceOf(FeedStore);
74
- expect(feedStore2.opened).toBeFalsy();
75
- await feedStore2.open();
76
- expect(feedStore2.opened).toBeTruthy();
77
72
  });
78
73
 
79
74
  test('Config default + custom database + custom hypercore', async () => {
@@ -85,140 +80,99 @@ describe('FeedStore', () => {
85
80
  const database = hypertrie(storage.createOrOpen.bind(storage), { valueEncoding: 'json' });
86
81
  database.list = jest.fn((_, cb) => cb(null, []));
87
82
 
88
- const feedStore = await createFeedStore(createStorage('', STORAGE_RAM), {
83
+ const feedStore = createFeedStore(createStorage('', STORAGE_RAM), {
89
84
  hypercore: customHypercore
90
85
  });
91
86
 
92
87
  expect(feedStore).toBeInstanceOf(FeedStore);
93
88
 
94
- await feedStore.createReadOnlyFeed({ key: PublicKey.random() });
89
+ await feedStore.openReadOnlyFeed(PublicKey.random());
95
90
 
96
91
  expect(customHypercore.mock.calls.length).toBe(1);
97
92
  });
98
93
 
99
94
  test('Create feed', async () => {
100
95
  const { feedStore } = await createDefault();
101
- const { booksFeed } = await defaultFeeds(feedStore, keys);
96
+ const { booksFeed: { feed: booksFeed } } = await defaultFeeds(feedStore, keys);
102
97
 
103
98
  expect(booksFeed).toBeInstanceOf(hypercore);
104
99
 
105
- const booksFeedDescriptor = feedStore.getDescriptors().find(fd => fd.key.equals(booksFeed.key));
100
+ const booksFeedDescriptor = await feedStore.openReadWriteFeed(PublicKey.from(booksFeed.key), booksFeed.secretKey);
106
101
  expect(booksFeedDescriptor).toHaveProperty('key', PublicKey.from(booksFeed.key));
107
- expect(booksFeedDescriptor?.metadata).toHaveProperty('topic', 'books');
108
102
 
109
103
  await append(booksFeed, 'Foundation and Empire');
110
104
  await expect(head(booksFeed)).resolves.toBe('Foundation and Empire');
111
105
 
112
106
  // It should return the same opened instance.
113
- await expect(feedStore.openFeed(PublicKey.from(booksFeed.key))).resolves.toBe(booksFeed);
107
+ await expect(feedStore.openReadOnlyFeed(PublicKey.from(booksFeed.key))).resolves.toBe(booksFeedDescriptor);
114
108
  });
115
109
 
116
110
  test('Create duplicate feed', async () => {
117
111
  const { feedStore } = await createDefault();
118
112
 
119
- const fds = await feedStore.createReadWriteFeed({ key: keys.usersFeed.key, secretKey: keys.usersFeed.secretKey });
113
+ const { feed: fds } = await feedStore.openReadWriteFeed(keys.usersFeed.key, keys.usersFeed.secretKey);
114
+ assert(fds.secretKey);
120
115
 
121
116
  const [usersFeed, feed2] = await Promise.all([
122
- feedStore.openFeed(PublicKey.from(fds.key)),
123
- feedStore.openFeed(PublicKey.from(fds.key))
117
+ feedStore.openReadWriteFeed(PublicKey.from(fds.key), fds.secretKey),
118
+ feedStore.openReadWriteFeed(PublicKey.from(fds.key), fds.secretKey)
124
119
  ]);
125
- expect(usersFeed).toBe(feed2);
120
+ expect(usersFeed.feed).toBe(feed2.feed);
126
121
 
127
- await append(usersFeed, 'alice');
128
- await expect(head(usersFeed)).resolves.toBe('alice');
122
+ await append(usersFeed.feed, 'alice');
123
+ await expect(head(usersFeed.feed)).resolves.toBe('alice');
129
124
  });
130
125
 
131
126
  test('Create and close a feed', async () => {
132
127
  const { feedStore } = await createDefault();
133
128
  const publicKey = PublicKey.random();
134
129
 
135
- await expect(feedStore.closeFeed(publicKey)).rejects.toThrow(/Feed not found/);
136
-
137
- const foo = await feedStore.createReadOnlyFeed({ key: publicKey });
130
+ const foo = await feedStore.openReadOnlyFeed(publicKey);
138
131
  expect(foo.opened).toBeTruthy();
139
- expect(foo.closed).toBeFalsy();
140
132
 
141
- await feedStore.closeFeed(publicKey);
142
- expect(foo.closed).toBeTruthy();
133
+ await feedStore.close();
134
+ expect(foo.opened).toBeFalsy();
143
135
  });
144
136
 
145
137
  test('Descriptors', async () => {
146
138
  const { feedStore } = await createDefault();
147
- const { booksFeed } = await defaultFeeds(feedStore, keys);
139
+ await defaultFeeds(feedStore, keys);
148
140
 
149
- expect(feedStore.getDescriptors().map(fd => fd.key)).toEqual(Object.entries(keys).map(([, keyPair]) => keyPair.key));
150
- expect(feedStore.getDescriptorByDiscoveryKey(booksFeed.discoveryKey)?.key).toEqual(keys.booksFeed.key);
141
+ expect(Array.from((feedStore as any)._descriptors.values()).map((fd: any) => fd.key))
142
+ .toEqual(Object.entries(keys).map(([, keyPair]) => keyPair.key)
143
+ );
151
144
  });
152
145
 
153
146
  test('Feeds', async () => {
154
147
  const { feedStore } = await createDefault();
155
148
  const { booksFeed, usersFeed, groupsFeed } = await defaultFeeds(feedStore, keys);
156
149
 
157
- expect(feedStore.getOpenFeeds().map(f => f.key)).toEqual([booksFeed.key, usersFeed.key, groupsFeed.key]);
158
- expect(feedStore.getOpenFeed(fd => fd.key.equals(booksFeed.key))).toBe(booksFeed);
159
- expect(feedStore.getOpenFeed(() => false)).toBeUndefined();
160
- expect(feedStore.getOpenFeeds(fd => fd.key.equals(keys.booksFeed.key))).toEqual([booksFeed]);
161
- });
162
-
163
- test('Close/Load feed', async () => {
164
- const { feedStore } = await createDefault();
165
- const { booksFeed } = await defaultFeeds(feedStore, keys);
166
-
167
- await feedStore.closeFeed(keys.booksFeed.key);
168
- expect(feedStore.getDescriptors().find(fd => fd.key.equals(keys.booksFeed.key))).toHaveProperty('opened', false);
169
-
170
- const [feed] = await feedStore.openFeeds(fd => fd.key.equals(keys.booksFeed.key));
171
- expect(feed).toBeDefined();
172
- expect(feed.key).toEqual(booksFeed.key);
173
- expect(feedStore.getDescriptors().find(fd => fd.key.equals(keys.booksFeed.key))).toHaveProperty('opened', true);
150
+ expect(Array.from((feedStore as any)._descriptors.values()).map((fd: any) => fd.key))
151
+ .toEqual([booksFeed.key, usersFeed.key, groupsFeed.key]);
174
152
  });
175
153
 
176
154
  test('Close feedStore and their feeds', async () => {
177
155
  const { feedStore } = await createDefault();
178
156
  await defaultFeeds(feedStore, keys);
179
157
 
180
- expect(feedStore.opened).toBe(true);
181
- expect(feedStore.closed).toBe(false);
182
- expect(feedStore.getDescriptors().filter(fd => fd.opened).length).toBe(3);
183
-
158
+ expect(Array.from((feedStore as any)._descriptors.values()).map((fd: any) => fd.key).length).toBe(3);
184
159
  await feedStore.close();
185
- expect(feedStore.getDescriptors().filter(fd => fd.opened).length).toBe(0);
186
- expect(feedStore.opened).toBe(false);
187
- expect(feedStore.closed).toBe(true);
160
+ expect(Array.from((feedStore as any)._descriptors.values()).map((fd: any) => fd.key).length).toBe(0);
188
161
  });
189
162
 
190
163
  test('Default codec: binary', async () => {
191
- const feedStore = await createFeedStore(createStorage('', STORAGE_RAM));
164
+ const feedStore = createFeedStore(createStorage('', STORAGE_RAM));
192
165
  expect(feedStore).toBeInstanceOf(FeedStore);
193
166
 
194
167
  const { publicKey, secretKey } = createKeyPair();
195
- const feed = await feedStore.createReadWriteFeed({ key: PublicKey.from(publicKey), secretKey });
168
+ const { feed } = await feedStore.openReadWriteFeed(PublicKey.from(publicKey), secretKey);
196
169
  expect(feed).toBeInstanceOf(hypercore);
197
170
  await append(feed, 'test');
198
171
  await expect(head(feed)).resolves.toBeInstanceOf(Buffer);
199
172
  });
200
173
 
201
- test('on open error should unlock the descriptor', async () => {
202
- const feedStore = await createFeedStore(createStorage('', STORAGE_RAM), {
203
- hypercore: () => {
204
- throw new Error('open error');
205
- }
206
- });
207
-
208
- const publicKey = PublicKey.random();
209
- await expect(feedStore.createReadOnlyFeed({ key: publicKey })).rejects.toThrow(/open error/);
210
-
211
- const fd = feedStore.getDescriptors().find(fd => fd.key.equals(publicKey));
212
-
213
- if (!fd) {
214
- throw new Error('Descriptor not found');
215
- }
216
-
217
- await expect(fd.lock.executeSynchronized(async () => 'Unlocked')).resolves.toBe('Unlocked');
218
- });
219
-
220
174
  test('on close error should unlock the descriptor', async () => {
221
- const feedStore = await createFeedStore(createStorage('', STORAGE_RAM), {
175
+ const feedStore = createFeedStore(createStorage('', STORAGE_RAM), {
222
176
  hypercore: () => ({
223
177
  opened: true,
224
178
  ready (cb: () => void) {
@@ -232,38 +186,9 @@ describe('FeedStore', () => {
232
186
  });
233
187
 
234
188
  const publicKey = PublicKey.random();
235
- await feedStore.createReadOnlyFeed({ key: publicKey });
236
- const fd = feedStore.getDescriptors().find(fd => fd.key.equals(publicKey));
237
-
238
- if (!fd) {
239
- throw new Error('Descriptor not found');
240
- }
189
+ await feedStore.openReadOnlyFeed(publicKey);
241
190
 
242
- await expect(feedStore.closeFeed(publicKey)).rejects.toThrow(/close error/);
243
191
  await expect(feedStore.close()).rejects.toThrow(/close error/);
244
-
245
- await expect(fd.lock.executeSynchronized(async () => 'Unlocked')).resolves.toBe('Unlocked');
246
- });
247
-
248
- test('append event', async (done) => {
249
- const feedStore = await createFeedStore(createStorage('', STORAGE_RAM));
250
- const { publicKey, secretKey } = createKeyPair();
251
- const feed = await feedStore.createReadWriteFeed({ key: PublicKey.from(publicKey), secretKey });
252
-
253
- feedStore.appendEvent.on((descriptor) => {
254
- expect(descriptor.feed).toBe(feed);
255
- done();
256
- });
257
-
258
- feed.append('test');
259
- });
260
-
261
- test('openFeed should wait until FeedStore is ready', async () => {
262
- const feedStore = new FeedStore(createStorage('', STORAGE_RAM));
263
- await feedStore.open();
264
- const publicKey = PublicKey.random();
265
- const feed = await feedStore.createReadOnlyFeed({ key: publicKey });
266
- expect(feed).toBeDefined();
267
192
  });
268
193
 
269
194
  test('feed event does not get called twice', async () => {
@@ -275,11 +200,11 @@ describe('FeedStore', () => {
275
200
  });
276
201
 
277
202
  const key = PublicKey.random();
278
- await feedStore.createReadOnlyFeed({ key });
203
+ await feedStore.openReadOnlyFeed(key);
279
204
 
280
- await feedStore.openFeed(key);
281
- await feedStore.openFeed(key);
282
- await feedStore.openFeed(key);
205
+ await feedStore.openReadOnlyFeed(key);
206
+ await feedStore.openReadOnlyFeed(key);
207
+ await feedStore.openReadOnlyFeed(key);
283
208
 
284
209
  await sleep(20); // To flush events
285
210
 
package/src/feed-store.ts CHANGED
@@ -6,30 +6,25 @@ import assert from 'assert';
6
6
  import defaultHypercore from 'hypercore';
7
7
 
8
8
  import { synchronized, Event } from '@dxos/async';
9
- import { PublicKey, PublicKeyLike } from '@dxos/crypto';
9
+ import { PublicKey } from '@dxos/crypto';
10
10
  import { IStorage } from '@dxos/random-access-multi-storage';
11
11
 
12
12
  import FeedDescriptor from './feed-descriptor';
13
- import type { HypercoreFeed, Hypercore } from './hypercore-types';
13
+ import type { Hypercore } from './hypercore-types';
14
14
  import type { ValueEncoding } from './types';
15
15
 
16
- type DescriptorCallback = (descriptor: FeedDescriptor) => boolean;
17
-
18
16
  export interface CreateDescriptorOptions {
19
17
  key: PublicKey,
20
- secretKey?: Buffer,
21
- metadata?: any
18
+ secretKey?: Buffer
22
19
  }
23
20
 
24
21
  export interface CreateReadWriteFeedOptions {
25
22
  key: PublicKey,
26
23
  secretKey: Buffer
27
- metadata?: any
28
24
  }
29
25
 
30
26
  export interface CreateReadOnlyFeedOptions {
31
27
  key: PublicKey
32
- metadata?: any
33
28
  }
34
29
 
35
30
  export interface FeedStoreOptions {
@@ -54,12 +49,6 @@ export class FeedStore {
54
49
  private _valueEncoding: ValueEncoding | undefined;
55
50
  private _hypercore: Hypercore;
56
51
  private _descriptors: Map<string, FeedDescriptor>;
57
- private _open: boolean;
58
-
59
- /**
60
- * Is emitted when data gets appended to one of the FeedStore's feeds represented by FeedDescriptors.
61
- */
62
- readonly appendEvent = new Event<FeedDescriptor>();
63
52
 
64
53
  /**
65
54
  * Is emitted when a new feed represented by FeedDescriptor is opened.
@@ -84,16 +73,6 @@ export class FeedStore {
84
73
  this._hypercore = hypercore;
85
74
 
86
75
  this._descriptors = new Map();
87
-
88
- this._open = false;
89
- }
90
-
91
- get opened () {
92
- return this._open;
93
- }
94
-
95
- get closed () {
96
- return !this._open;
97
76
  }
98
77
 
99
78
  /**
@@ -103,187 +82,52 @@ export class FeedStore {
103
82
  return this._storage;
104
83
  }
105
84
 
106
- @synchronized
107
- async open () {
108
- if (this._open) {
109
- return;
110
- }
111
- this._open = true;
112
- }
113
-
114
85
  @synchronized
115
86
  async close () {
116
- if (!this._open) {
117
- return;
118
- }
119
- this._open = false;
120
-
121
- await Promise.all(this
122
- .getDescriptors()
123
- .map(descriptor => descriptor.close())
124
- );
125
-
87
+ await Promise.all(Array.from(this._descriptors.values()).map(descriptor => descriptor.close()));
126
88
  this._descriptors.clear();
127
89
  }
128
90
 
129
- /**
130
- * Get the list of descriptors.
131
- */
132
- getDescriptors () {
133
- return Array.from(this._descriptors.values());
134
- }
135
-
136
- /**
137
- * Get desciptor by its public key
138
- */
139
- getDescriptor (key: PublicKeyLike) {
140
- return this.getDescriptors().find(descriptor => descriptor.key.equals(key));
141
- }
142
-
143
- /**
144
- * Fast access to a descriptor
145
- */
146
- getDescriptorByDiscoveryKey (discoverKey: Buffer) {
147
- return this._descriptors.get(discoverKey.toString('hex'));
148
- }
149
-
150
- /**
151
- * Get the list of opened feeds, with optional filter.
152
- */
153
- getOpenFeeds (callback?: DescriptorCallback): HypercoreFeed[] {
154
- const notNull = <T>(value: T | null): value is T => Boolean(value);
155
- return this.getDescriptors()
156
- .filter(descriptor => descriptor.opened && (!callback || callback(descriptor)))
157
- .map(descriptor => descriptor.feed)
158
- .filter(notNull);
159
- }
160
-
161
- /**
162
- * Find an opened feed using a filter callback.
163
- */
164
- getOpenFeed (callback: DescriptorCallback): HypercoreFeed | undefined {
165
- const descriptor = this.getDescriptors()
166
- .find(descriptor => descriptor.opened && callback(descriptor));
167
-
168
- if (descriptor && descriptor.feed) {
169
- return descriptor.feed;
170
- }
171
-
172
- return undefined;
173
- }
174
-
175
- /**
176
- * Checks if feedstore has a feed with specified key.
177
- */
178
- hasFeed (key: PublicKeyLike): boolean {
179
- return this.getDescriptors().some(fd => fd.key.equals(key));
180
- }
181
-
182
- /**
183
- * Open multiple feeds using a filter callback.
184
- */
185
- @synchronized
186
- async openFeeds (callback: DescriptorCallback): Promise<HypercoreFeed[]> {
187
- assert(this._open, 'FeedStore closed');
188
-
189
- const descriptors = this.getDescriptors()
190
- .filter(descriptor => callback(descriptor));
191
-
192
- return Promise.all(descriptors.map(descriptor => descriptor.open()));
193
- }
194
-
195
- /**
196
- * Open a feed to FeedStore.
197
- */
198
- @synchronized
199
- async openFeed (key: PublicKey): Promise<HypercoreFeed> {
200
- assert(this._open, 'FeedStore closed');
201
-
202
- const descriptor = this.getDescriptors().find(fd => fd.key.equals(key));
203
-
204
- assert(descriptor, 'Descriptor not found');
205
-
206
- const feed = await descriptor.open();
207
-
208
- return feed;
209
- }
210
-
211
91
  /**
212
92
  * Create a feed to Feedstore
213
93
  */
214
- async createReadWriteFeed (options: CreateReadWriteFeedOptions): Promise<HypercoreFeed> {
215
- this._createDescriptor(options);
216
- return this.openFeed(options.key);
94
+ async openReadWriteFeed (key: PublicKey, secretKey: Buffer): Promise<FeedDescriptor> {
95
+ const descriptor = this._descriptors.get(key.toString());
96
+ if (descriptor && descriptor.secretKey) {
97
+ return descriptor;
98
+ }
99
+ return this._createDescriptor({ key, secretKey });
217
100
  }
218
101
 
219
102
  /**
220
103
  * Create a readonly feed to Feedstore
221
104
  */
222
- async createReadOnlyFeed (options: CreateReadOnlyFeedOptions): Promise<HypercoreFeed> {
223
- this._createDescriptor(options);
224
- return this.openFeed(options.key);
225
- }
226
-
227
- /**
228
- * Close a feed by the key.
229
- */
230
- @synchronized
231
- async closeFeed (key: PublicKey) {
232
- assert(this._open, 'FeedStore closed');
233
-
234
- const descriptor = this.getDescriptors().find(fd => fd.key.equals(key));
235
-
236
- if (!descriptor) {
237
- throw new Error(`Feed not found: ${key.toString()}`);
238
- }
239
-
240
- await descriptor.close();
105
+ async openReadOnlyFeed (key: PublicKey): Promise<FeedDescriptor> {
106
+ const descriptor = this._descriptors.get(key.toString()) ?? await this._createDescriptor({ key });
107
+ return descriptor;
241
108
  }
242
109
 
243
110
  /**
244
111
  * Factory to create a new FeedDescriptor.
245
112
  */
246
- private _createDescriptor (options: CreateDescriptorOptions) {
247
- const { key, secretKey, metadata } = options;
248
-
249
- const existing = this.getDescriptors().find(fd => fd.key.equals(key));
250
- if (existing) {
251
- return existing;
252
- }
113
+ private async _createDescriptor (options: CreateDescriptorOptions) {
114
+ const { key, secretKey } = options;
253
115
 
254
116
  const descriptor = new FeedDescriptor({
255
117
  storage: this._storage,
256
118
  key,
257
119
  secretKey,
258
120
  valueEncoding: this._valueEncoding,
259
- metadata,
260
121
  hypercore: this._hypercore
261
122
  });
262
123
 
263
124
  this._descriptors.set(
264
- descriptor.discoveryKey.toString('hex'),
125
+ descriptor.key.toString(),
265
126
  descriptor
266
127
  );
267
128
 
268
- const append = () => this.appendEvent.emit(descriptor);
269
-
270
- descriptor.watch(async (event) => {
271
- if (event === 'updated') {
272
- return;
273
- }
274
-
275
- const { feed } = descriptor;
276
-
277
- if (event === 'opened' && feed) {
278
- feed.on('append', append);
279
- this.feedOpenedEvent.emit(descriptor);
280
- return;
281
- }
282
-
283
- if (event === 'closed' && feed) {
284
- feed.removeListener('append', append);
285
- }
286
- });
129
+ await descriptor.open();
130
+ this.feedOpenedEvent.emit(descriptor);
287
131
 
288
132
  return descriptor;
289
133
  }