@dxos/feed-store 2.33.5-dev.22471d71 → 2.33.5-dev.33d2877e
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/benchmark.js +22 -26
- package/dist/src/benchmark.js.map +1 -1
- package/dist/src/create-batch-stream.d.ts +1 -1
- package/dist/src/create-batch-stream.d.ts.map +1 -1
- package/dist/src/create-batch-stream.js +14 -14
- package/dist/src/create-batch-stream.js.map +1 -1
- package/dist/src/create-batch-stream.test.js +2 -4
- package/dist/src/create-batch-stream.test.js.map +1 -1
- package/dist/src/feed-descriptor.d.ts +3 -3
- package/dist/src/feed-descriptor.d.ts.map +1 -1
- package/dist/src/feed-descriptor.js +13 -3
- package/dist/src/feed-descriptor.js.map +1 -1
- package/dist/src/feed-descriptor.test.js +10 -10
- package/dist/src/feed-descriptor.test.js.map +1 -1
- package/dist/src/feed-store.d.ts +5 -5
- package/dist/src/feed-store.d.ts.map +1 -1
- package/dist/src/feed-store.js +8 -8
- package/dist/src/feed-store.js.map +1 -1
- package/dist/src/feed-store.test.js +16 -39
- package/dist/src/feed-store.test.js.map +1 -1
- package/dist/src/stream.d.ts +4 -4
- package/dist/src/stream.d.ts.map +1 -1
- package/dist/src/stream.js +34 -42
- package/dist/src/stream.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/benchmark.ts +24 -28
- package/src/create-batch-stream.test.ts +2 -4
- package/src/create-batch-stream.ts +15 -15
- package/src/feed-descriptor.test.ts +10 -10
- package/src/feed-descriptor.ts +30 -7
- package/src/feed-store.test.ts +19 -48
- package/src/feed-store.ts +10 -10
- package/src/stream.ts +32 -40
|
@@ -20,7 +20,7 @@ describe('FeedDescriptor', () => {
|
|
|
20
20
|
beforeEach(async () => {
|
|
21
21
|
const { publicKey, secretKey } = createKeyPair();
|
|
22
22
|
fd = new FeedDescriptor({
|
|
23
|
-
|
|
23
|
+
directory: createStorage('', StorageType.RAM).directory('feed'),
|
|
24
24
|
key: PublicKey.from(publicKey),
|
|
25
25
|
secretKey,
|
|
26
26
|
hypercore: defaultHypercore
|
|
@@ -41,7 +41,7 @@ describe('FeedDescriptor', () => {
|
|
|
41
41
|
// When this behaviour was changed, suddenly `protocol-plugin-replicator` tests started hanging forever on network generation.
|
|
42
42
|
const { publicKey } = createKeyPair();
|
|
43
43
|
const key = PublicKey.from(publicKey);
|
|
44
|
-
const fd = new FeedDescriptor({ key,
|
|
44
|
+
const fd = new FeedDescriptor({ key, directory: createStorage('', StorageType.NODE).directory('feed'), hypercore: defaultHypercore });
|
|
45
45
|
expect(fd.key).toEqual(key);
|
|
46
46
|
expect(fd.secretKey).toBeUndefined();
|
|
47
47
|
});
|
|
@@ -50,7 +50,7 @@ describe('FeedDescriptor', () => {
|
|
|
50
50
|
const { publicKey, secretKey } = createKeyPair();
|
|
51
51
|
|
|
52
52
|
const fd = new FeedDescriptor({
|
|
53
|
-
|
|
53
|
+
directory: createStorage('', StorageType.RAM).directory('feed'),
|
|
54
54
|
key: PublicKey.from(publicKey),
|
|
55
55
|
secretKey,
|
|
56
56
|
valueEncoding: 'json',
|
|
@@ -92,7 +92,7 @@ describe('FeedDescriptor', () => {
|
|
|
92
92
|
// If we try to close a feed that is opening should wait for the open result.
|
|
93
93
|
const { publicKey, secretKey } = createKeyPair();
|
|
94
94
|
const fd2 = new FeedDescriptor({
|
|
95
|
-
|
|
95
|
+
directory: createStorage('', StorageType.RAM).directory('feed'),
|
|
96
96
|
key: PublicKey.from(publicKey),
|
|
97
97
|
secretKey,
|
|
98
98
|
hypercore: defaultHypercore
|
|
@@ -108,7 +108,7 @@ describe('FeedDescriptor', () => {
|
|
|
108
108
|
|
|
109
109
|
const { publicKey, secretKey } = createKeyPair();
|
|
110
110
|
const fd = new FeedDescriptor({
|
|
111
|
-
|
|
111
|
+
directory: createStorage(root, StorageType.NODE).directory('feed'),
|
|
112
112
|
key: PublicKey.from(publicKey),
|
|
113
113
|
secretKey,
|
|
114
114
|
valueEncoding: 'utf-8',
|
|
@@ -135,7 +135,7 @@ describe('FeedDescriptor', () => {
|
|
|
135
135
|
test('on open error should unlock the resource', async () => {
|
|
136
136
|
const { publicKey, secretKey } = createKeyPair();
|
|
137
137
|
const fd = new FeedDescriptor({
|
|
138
|
-
|
|
138
|
+
directory: createStorage('', StorageType.RAM).directory('feed'),
|
|
139
139
|
key: PublicKey.from(publicKey),
|
|
140
140
|
secretKey,
|
|
141
141
|
hypercore: () => {
|
|
@@ -149,16 +149,16 @@ describe('FeedDescriptor', () => {
|
|
|
149
149
|
test('on close error should unlock the resource', async () => {
|
|
150
150
|
const { publicKey, secretKey } = createKeyPair();
|
|
151
151
|
const fd = new FeedDescriptor({
|
|
152
|
-
|
|
152
|
+
directory: createStorage('', StorageType.RAM).directory('feed'),
|
|
153
153
|
key: PublicKey.from(publicKey),
|
|
154
154
|
secretKey,
|
|
155
155
|
hypercore: () => ({
|
|
156
156
|
opened: true,
|
|
157
|
-
on () {},
|
|
158
|
-
ready (cb: () => void) {
|
|
157
|
+
on: () => {},
|
|
158
|
+
ready: (cb: () => void) => {
|
|
159
159
|
cb();
|
|
160
160
|
},
|
|
161
|
-
close () {
|
|
161
|
+
close: () => {
|
|
162
162
|
throw new Error('close error');
|
|
163
163
|
}
|
|
164
164
|
} as any)
|
package/src/feed-descriptor.ts
CHANGED
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
import assert from 'assert';
|
|
6
6
|
import defaultHypercore from 'hypercore';
|
|
7
7
|
import pify from 'pify';
|
|
8
|
+
import { callbackify } from 'util';
|
|
8
9
|
|
|
9
10
|
import { Lock } from '@dxos/async';
|
|
10
11
|
import { PublicKey } from '@dxos/crypto';
|
|
11
|
-
import type {
|
|
12
|
+
import type { Directory } from '@dxos/random-access-multi-storage';
|
|
12
13
|
|
|
13
14
|
import type { HypercoreFeed, Hypercore } from './hypercore-types';
|
|
14
15
|
import type { ValueEncoding } from './types';
|
|
15
16
|
|
|
16
17
|
interface FeedDescriptorOptions {
|
|
17
|
-
|
|
18
|
+
directory: Directory,
|
|
18
19
|
key: PublicKey,
|
|
19
20
|
hypercore: Hypercore,
|
|
20
21
|
secretKey?: Buffer,
|
|
@@ -28,7 +29,7 @@ interface FeedDescriptorOptions {
|
|
|
28
29
|
* Abstract handler for an Hypercore instance.
|
|
29
30
|
*/
|
|
30
31
|
export class FeedDescriptor {
|
|
31
|
-
private readonly
|
|
32
|
+
private readonly _directory: Directory;
|
|
32
33
|
private readonly _key: PublicKey;
|
|
33
34
|
private readonly _secretKey?: Buffer;
|
|
34
35
|
private readonly _valueEncoding?: ValueEncoding;
|
|
@@ -40,7 +41,7 @@ export class FeedDescriptor {
|
|
|
40
41
|
|
|
41
42
|
constructor (options: FeedDescriptorOptions) {
|
|
42
43
|
const {
|
|
43
|
-
|
|
44
|
+
directory,
|
|
44
45
|
key,
|
|
45
46
|
secretKey,
|
|
46
47
|
valueEncoding,
|
|
@@ -48,7 +49,7 @@ export class FeedDescriptor {
|
|
|
48
49
|
disableSigning = false
|
|
49
50
|
} = options;
|
|
50
51
|
|
|
51
|
-
this.
|
|
52
|
+
this._directory = directory;
|
|
52
53
|
this._valueEncoding = valueEncoding;
|
|
53
54
|
this._hypercore = hypercore;
|
|
54
55
|
this._key = key;
|
|
@@ -119,9 +120,19 @@ export class FeedDescriptor {
|
|
|
119
120
|
* Defines the real path where the Hypercore is going
|
|
120
121
|
* to work with the RandomAccessStorage specified.
|
|
121
122
|
*/
|
|
122
|
-
|
|
123
|
+
|
|
124
|
+
private _createStorage (dir = ''): (name: string) => HypercoreFile {
|
|
123
125
|
return (name) => {
|
|
124
|
-
|
|
126
|
+
const file = this._directory.createOrOpen(`${dir}/${name}`);
|
|
127
|
+
// Separation between our internal File API and Hypercore's.
|
|
128
|
+
return {
|
|
129
|
+
read: callbackify(file.read.bind(file)),
|
|
130
|
+
write: callbackify(file.write.bind(file)),
|
|
131
|
+
del: callbackify(file.truncate.bind(file)),
|
|
132
|
+
stat: callbackify(file.stat.bind(file)),
|
|
133
|
+
close: callbackify(file.close.bind(file)),
|
|
134
|
+
destroy: callbackify(file.delete.bind(file))
|
|
135
|
+
} as HypercoreFile;
|
|
125
136
|
};
|
|
126
137
|
}
|
|
127
138
|
|
|
@@ -155,3 +166,15 @@ const MOCK_CRYPTO = {
|
|
|
155
166
|
cb(null, true);
|
|
156
167
|
}
|
|
157
168
|
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* File API that hypercore uses to read/write from storage.
|
|
172
|
+
*/
|
|
173
|
+
interface HypercoreFile {
|
|
174
|
+
read (offset: number, size: number, cb?: (err: Error | null, data?: Buffer) => void): void;
|
|
175
|
+
write (offset: number, data: Buffer, cb?: (err: Error | null) => void): void;
|
|
176
|
+
del (offset: number, size: number, cb?: (err: Error | null) => void): void;
|
|
177
|
+
stat (cb: (err: Error | null, data?: {size: number}) => void): void;
|
|
178
|
+
close (cb?: (err: Error | null) => void): void;
|
|
179
|
+
destroy (cb?: (err: Error | null) => void): void;
|
|
180
|
+
}
|
package/src/feed-store.test.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import assert from 'assert';
|
|
8
8
|
import hypercore from 'hypercore';
|
|
9
|
-
import hypertrie from 'hypertrie';
|
|
10
9
|
import pify from 'pify';
|
|
11
10
|
import tempy from 'tempy';
|
|
12
11
|
|
|
@@ -26,71 +25,43 @@ interface KeyPair {
|
|
|
26
25
|
const feedNames = ['booksFeed', 'usersFeed', 'groupsFeed'];
|
|
27
26
|
|
|
28
27
|
const createFeedStore = (storage: Storage, options = {}) => {
|
|
29
|
-
const feedStore = new FeedStore(storage, options);
|
|
28
|
+
const feedStore = new FeedStore(storage.directory('feed'), options);
|
|
30
29
|
return feedStore;
|
|
31
30
|
};
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
const createDefault = async () => {
|
|
34
33
|
const directory = tempy.directory();
|
|
35
34
|
|
|
36
35
|
return {
|
|
37
36
|
directory,
|
|
38
37
|
feedStore: createFeedStore(createStorage(directory, StorageType.NODE), { valueEncoding: 'utf-8' })
|
|
39
38
|
};
|
|
40
|
-
}
|
|
39
|
+
};
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)));
|
|
46
|
-
}
|
|
41
|
+
const defaultFeeds = async (feedStore: FeedStore, keys: Record<string, KeyPair>): Promise<Record<string, FeedDescriptor>> => Object.fromEntries(await Promise.all(Object.entries<KeyPair>(keys).map(async ([feed, keyPair]) =>
|
|
42
|
+
[feed, await feedStore.openReadWriteFeed(keyPair.key, keyPair.secretKey)]
|
|
43
|
+
)));
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
return pify(feed.append.bind(feed))(message);
|
|
50
|
-
}
|
|
45
|
+
const append = (feed: HypercoreFeed, message: any) => pify(feed.append.bind(feed))(message);
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
return pify(feed.head.bind(feed))();
|
|
54
|
-
}
|
|
47
|
+
const head = (feed: HypercoreFeed) => pify(feed.head.bind(feed))();
|
|
55
48
|
|
|
56
|
-
const createKeyPairs = () => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}));
|
|
61
|
-
};
|
|
49
|
+
const createKeyPairs = () => Object.fromEntries<KeyPair>(feedNames.map(feed => {
|
|
50
|
+
const { publicKey, secretKey } = createKeyPair();
|
|
51
|
+
return [feed, { key: PublicKey.from(publicKey), secretKey }];
|
|
52
|
+
}));
|
|
62
53
|
|
|
63
54
|
describe('FeedStore', () => {
|
|
64
55
|
const keys = createKeyPairs();
|
|
65
56
|
|
|
66
57
|
test('Config default', async () => {
|
|
67
|
-
const feedStore = await createFeedStore(createStorage('
|
|
58
|
+
const feedStore = await createFeedStore(createStorage('', StorageType.RAM));
|
|
68
59
|
expect(feedStore).toBeInstanceOf(FeedStore);
|
|
69
60
|
|
|
70
|
-
const feedStore2 = new FeedStore(createStorage('
|
|
61
|
+
const feedStore2 = new FeedStore(createStorage('', StorageType.RAM).directory('feed'));
|
|
71
62
|
expect(feedStore2).toBeInstanceOf(FeedStore);
|
|
72
63
|
});
|
|
73
64
|
|
|
74
|
-
test('Config default + custom database + custom hypercore', async () => {
|
|
75
|
-
const customHypercore = jest.fn((...args) => {
|
|
76
|
-
return hypercore(args[0], args[1], args[2]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const storage = createStorage('', StorageType.RAM);
|
|
80
|
-
const database = hypertrie(storage.createOrOpen.bind(storage), { valueEncoding: 'json' });
|
|
81
|
-
database.list = jest.fn((_, cb) => cb(null, []));
|
|
82
|
-
|
|
83
|
-
const feedStore = createFeedStore(createStorage('feed', StorageType.RAM), {
|
|
84
|
-
hypercore: customHypercore
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
expect(feedStore).toBeInstanceOf(FeedStore);
|
|
88
|
-
|
|
89
|
-
await feedStore.openReadOnlyFeed(PublicKey.random());
|
|
90
|
-
|
|
91
|
-
expect(customHypercore.mock.calls.length).toBe(1);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
65
|
test('Create feed', async () => {
|
|
95
66
|
const { feedStore } = await createDefault();
|
|
96
67
|
const { booksFeed: { feed: booksFeed } } = await defaultFeeds(feedStore, keys);
|
|
@@ -161,7 +132,7 @@ describe('FeedStore', () => {
|
|
|
161
132
|
});
|
|
162
133
|
|
|
163
134
|
test('Default codec: binary', async () => {
|
|
164
|
-
const feedStore = createFeedStore(createStorage('
|
|
135
|
+
const feedStore = createFeedStore(createStorage('', StorageType.RAM));
|
|
165
136
|
expect(feedStore).toBeInstanceOf(FeedStore);
|
|
166
137
|
|
|
167
138
|
const { publicKey, secretKey } = createKeyPair();
|
|
@@ -172,14 +143,14 @@ describe('FeedStore', () => {
|
|
|
172
143
|
});
|
|
173
144
|
|
|
174
145
|
test('on close error should unlock the descriptor', async () => {
|
|
175
|
-
const feedStore = createFeedStore(createStorage('
|
|
146
|
+
const feedStore = createFeedStore(createStorage('', StorageType.RAM), {
|
|
176
147
|
hypercore: () => ({
|
|
177
148
|
opened: true,
|
|
178
|
-
ready (cb: () => void) {
|
|
149
|
+
ready: (cb: () => void) => {
|
|
179
150
|
cb();
|
|
180
151
|
},
|
|
181
|
-
on () {},
|
|
182
|
-
close () {
|
|
152
|
+
on: () => {},
|
|
153
|
+
close: () => {
|
|
183
154
|
throw new Error('close error');
|
|
184
155
|
}
|
|
185
156
|
})
|
package/src/feed-store.ts
CHANGED
|
@@ -7,7 +7,7 @@ import defaultHypercore from 'hypercore';
|
|
|
7
7
|
|
|
8
8
|
import { synchronized, Event } from '@dxos/async';
|
|
9
9
|
import { PublicKey } from '@dxos/crypto';
|
|
10
|
-
import {
|
|
10
|
+
import { Directory } from '@dxos/random-access-multi-storage';
|
|
11
11
|
|
|
12
12
|
import FeedDescriptor from './feed-descriptor';
|
|
13
13
|
import type { Hypercore } from './hypercore-types';
|
|
@@ -45,7 +45,7 @@ export interface FeedStoreOptions {
|
|
|
45
45
|
* into a persist repository storage.
|
|
46
46
|
*/
|
|
47
47
|
export class FeedStore {
|
|
48
|
-
private
|
|
48
|
+
private _directory: Directory;
|
|
49
49
|
private _valueEncoding: ValueEncoding | undefined;
|
|
50
50
|
private _hypercore: Hypercore;
|
|
51
51
|
private _descriptors: Map<string, FeedDescriptor>;
|
|
@@ -56,13 +56,13 @@ export class FeedStore {
|
|
|
56
56
|
readonly feedOpenedEvent = new Event<FeedDescriptor>();
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* @param
|
|
59
|
+
* @param directory RandomAccessStorage to use by default by the feeds.
|
|
60
60
|
* @param options Feedstore options.
|
|
61
61
|
*/
|
|
62
|
-
constructor (
|
|
63
|
-
assert(
|
|
62
|
+
constructor (directory: Directory, options: FeedStoreOptions = {}) {
|
|
63
|
+
assert(directory, 'The storage is required.');
|
|
64
64
|
|
|
65
|
-
this.
|
|
65
|
+
this._directory = directory;
|
|
66
66
|
|
|
67
67
|
const {
|
|
68
68
|
valueEncoding,
|
|
@@ -79,7 +79,7 @@ export class FeedStore {
|
|
|
79
79
|
* @type {RandomAccessStorage}
|
|
80
80
|
*/
|
|
81
81
|
get storage () {
|
|
82
|
-
return this.
|
|
82
|
+
return this._directory;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
@synchronized
|
|
@@ -114,7 +114,7 @@ export class FeedStore {
|
|
|
114
114
|
const { key, secretKey } = options;
|
|
115
115
|
|
|
116
116
|
const descriptor = new FeedDescriptor({
|
|
117
|
-
|
|
117
|
+
directory: this._directory,
|
|
118
118
|
key,
|
|
119
119
|
secretKey,
|
|
120
120
|
valueEncoding: this._valueEncoding,
|
|
@@ -133,7 +133,7 @@ export class FeedStore {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
const patchBufferCodec = (encoding: ValueEncoding): ValueEncoding => {
|
|
137
137
|
if (typeof encoding === 'string') {
|
|
138
138
|
return encoding;
|
|
139
139
|
}
|
|
@@ -141,4 +141,4 @@ function patchBufferCodec (encoding: ValueEncoding): ValueEncoding {
|
|
|
141
141
|
encode: (x: any) => Buffer.from(encoding.encode(x)),
|
|
142
142
|
decode: encoding.decode.bind(encoding)
|
|
143
143
|
};
|
|
144
|
-
}
|
|
144
|
+
};
|
package/src/stream.ts
CHANGED
|
@@ -20,62 +20,54 @@ const error = debug('dxos:stream:error');
|
|
|
20
20
|
* @returns {NodeJS.WritableStream}
|
|
21
21
|
*/
|
|
22
22
|
// TODO(burdon): Move to @dxos/codec.
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
});
|
|
30
|
-
}
|
|
23
|
+
export const createWritableFeedStream = (feed: HypercoreFeed) => new Writable({
|
|
24
|
+
objectMode: true,
|
|
25
|
+
write: (message, _, callback) => {
|
|
26
|
+
feed.append(message, callback);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
31
29
|
|
|
32
30
|
/**
|
|
33
31
|
* Creates a readStream stream that can be used as a buffer into which messages can be pushed.
|
|
34
32
|
*/
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
}
|
|
33
|
+
export const createReadable = (): Readable => new Readable({
|
|
34
|
+
objectMode: true,
|
|
35
|
+
read: () => {}
|
|
36
|
+
});
|
|
41
37
|
|
|
42
38
|
/**
|
|
43
39
|
* Creates a writeStream object stream.
|
|
44
40
|
* @param callback
|
|
45
41
|
*/
|
|
46
|
-
export
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
next(err);
|
|
56
|
-
}
|
|
42
|
+
export const createWritable = <T>(callback: (message: T) => Promise<void>): NodeJS.WritableStream => new Writable({
|
|
43
|
+
objectMode: true,
|
|
44
|
+
write: async (message: T, _, next) => {
|
|
45
|
+
try {
|
|
46
|
+
await callback(message);
|
|
47
|
+
next();
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
error(err);
|
|
50
|
+
next(err);
|
|
57
51
|
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
60
54
|
|
|
61
55
|
/**
|
|
62
56
|
* Creates a transform object stream.
|
|
63
57
|
* @param callback
|
|
64
58
|
*/
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
next(err);
|
|
75
|
-
}
|
|
59
|
+
export const createTransform = <R, W>(callback: (message: R) => Promise<W | undefined>): Transform => new Transform({
|
|
60
|
+
objectMode: true,
|
|
61
|
+
transform: async (message: R, _, next) => {
|
|
62
|
+
try {
|
|
63
|
+
const response = await callback(message);
|
|
64
|
+
next(null, response);
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
error(err);
|
|
67
|
+
next(err);
|
|
76
68
|
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
79
71
|
|
|
80
72
|
/**
|
|
81
73
|
* Wriable stream that collects objects (e.g., for testing).
|