@dxos/hypercore 0.1.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.
- package/LICENSE +21 -0
- package/README.md +4 -0
- package/dist/src/crypto.d.ts +13 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +45 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/defaults.d.ts +18 -0
- package/dist/src/defaults.d.ts.map +1 -0
- package/dist/src/defaults.js +44 -0
- package/dist/src/defaults.js.map +1 -0
- package/dist/src/hypercore-factory.d.ts +23 -0
- package/dist/src/hypercore-factory.d.ts.map +1 -0
- package/dist/src/hypercore-factory.js +44 -0
- package/dist/src/hypercore-factory.js.map +1 -0
- package/dist/src/hypercore-factory.test.d.ts +2 -0
- package/dist/src/hypercore-factory.test.d.ts.map +1 -0
- package/dist/src/hypercore-factory.test.js +46 -0
- package/dist/src/hypercore-factory.test.js.map +1 -0
- package/dist/src/hypercore-protocol.test.d.ts +2 -0
- package/dist/src/hypercore-protocol.test.d.ts.map +1 -0
- package/dist/src/hypercore-protocol.test.js +191 -0
- package/dist/src/hypercore-protocol.test.js.map +1 -0
- package/dist/src/hypercore.test.d.ts +2 -0
- package/dist/src/hypercore.test.d.ts.map +1 -0
- package/dist/src/hypercore.test.js +79 -0
- package/dist/src/hypercore.test.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/iterator.d.ts +26 -0
- package/dist/src/iterator.d.ts.map +1 -0
- package/dist/src/iterator.js +37 -0
- package/dist/src/iterator.js.map +1 -0
- package/dist/src/iterator.test.d.ts +2 -0
- package/dist/src/iterator.test.d.ts.map +1 -0
- package/dist/src/iterator.test.js +61 -0
- package/dist/src/iterator.test.js.map +1 -0
- package/dist/src/multiplexer.d.ts +21 -0
- package/dist/src/multiplexer.d.ts.map +1 -0
- package/dist/src/multiplexer.js +103 -0
- package/dist/src/multiplexer.js.map +1 -0
- package/dist/src/multiplexer.test.d.ts +2 -0
- package/dist/src/multiplexer.test.d.ts.map +1 -0
- package/dist/src/multiplexer.test.js +142 -0
- package/dist/src/multiplexer.test.js.map +1 -0
- package/dist/src/replicate.test.d.ts +2 -0
- package/dist/src/replicate.test.d.ts.map +1 -0
- package/dist/src/replicate.test.js +197 -0
- package/dist/src/replicate.test.js.map +1 -0
- package/dist/src/streams.test.d.ts +2 -0
- package/dist/src/streams.test.d.ts.map +1 -0
- package/dist/src/streams.test.js +65 -0
- package/dist/src/streams.test.js.map +1 -0
- package/dist/src/testing.d.ts +11 -0
- package/dist/src/testing.d.ts.map +1 -0
- package/dist/src/testing.js +36 -0
- package/dist/src/testing.js.map +1 -0
- package/package.json +50 -0
- package/src/crypto.ts +46 -0
- package/src/defaults.ts +46 -0
- package/src/hypercore-factory.test.ts +53 -0
- package/src/hypercore-factory.ts +44 -0
- package/src/hypercore-protocol.test.ts +236 -0
- package/src/hypercore.test.ts +98 -0
- package/src/index.ts +8 -0
- package/src/iterator.test.ts +66 -0
- package/src/iterator.ts +35 -0
- package/src/multiplexer.test.ts +172 -0
- package/src/multiplexer.ts +122 -0
- package/src/replicate.test.ts +241 -0
- package/src/streams.test.ts +75 -0
- package/src/testing.ts +40 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'assert';
|
|
6
|
+
import { Hypercore } from 'hypercore';
|
|
7
|
+
import { ProtocolStream } from 'hypercore-protocol';
|
|
8
|
+
import { PassThrough, Transform, Writable } from 'streamx';
|
|
9
|
+
|
|
10
|
+
import { latch, Trigger } from '@dxos/async';
|
|
11
|
+
import { PublicKey } from '@dxos/keys';
|
|
12
|
+
import { log } from '@dxos/log';
|
|
13
|
+
import { ComplexMap } from '@dxos/util';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hypercore multiplexer.
|
|
17
|
+
*/
|
|
18
|
+
// TODO(burdon): Add encoding.
|
|
19
|
+
export class Multiplexer {
|
|
20
|
+
private readonly _feeds = new ComplexMap<PublicKey, Hypercore>(PublicKey.hash);
|
|
21
|
+
private readonly _streams = new ComplexMap<PublicKey, ProtocolStream>(PublicKey.hash);
|
|
22
|
+
|
|
23
|
+
private _isOpen = false;
|
|
24
|
+
private _closedTrigger = new Trigger({ autoReset: true });
|
|
25
|
+
|
|
26
|
+
public readonly input = new PassThrough();
|
|
27
|
+
public readonly output = new PassThrough();
|
|
28
|
+
|
|
29
|
+
// prettier-ignore
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly _id: string
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
get isOpen() {
|
|
35
|
+
return this._isOpen;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get feeds() {
|
|
39
|
+
return Array.from(this._feeds.values());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
addFeed(core: Hypercore, initiator = false) {
|
|
43
|
+
const stream = core.replicate(initiator, { live: true });
|
|
44
|
+
|
|
45
|
+
const feedKey = PublicKey.from(core.key);
|
|
46
|
+
this._feeds.set(feedKey, core);
|
|
47
|
+
this._streams.set(feedKey, stream);
|
|
48
|
+
log('added', { feedKey: PublicKey.from(feedKey) });
|
|
49
|
+
|
|
50
|
+
stream
|
|
51
|
+
.pipe(
|
|
52
|
+
new Transform({
|
|
53
|
+
transform: (data: Buffer, next: (err: Error | null, data: any) => void) => {
|
|
54
|
+
log('send', { id: this._id, feedKey: PublicKey.from(feedKey), payload: data.length });
|
|
55
|
+
next(null, {
|
|
56
|
+
feedKey: core.key,
|
|
57
|
+
payload: data
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
.pipe(this.output, () => {
|
|
63
|
+
log('pipeline closed', { id: this._id });
|
|
64
|
+
this._isOpen = false;
|
|
65
|
+
this._closedTrigger.wake();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async open() {
|
|
72
|
+
if (this._isOpen) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.input.pipe(
|
|
77
|
+
new Writable({
|
|
78
|
+
write: (data: any, next: () => void) => {
|
|
79
|
+
const { feedKey, payload } = data;
|
|
80
|
+
const feedStream = this._streams.get(PublicKey.from(feedKey));
|
|
81
|
+
assert(feedStream, `invalid stream: ${PublicKey.from(feedKey).truncate()}`);
|
|
82
|
+
log('recv', { id: this._id, feedKey: PublicKey.from(feedKey), payload: (payload as Buffer).length });
|
|
83
|
+
feedStream.write(payload);
|
|
84
|
+
next();
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
log('opened');
|
|
90
|
+
this._isOpen = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// TODO(burdon): Test if re-entrant?
|
|
94
|
+
async close() {
|
|
95
|
+
if (!this._isOpen) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
log('closing...');
|
|
100
|
+
const [closed, setClosed] = latch({ count: this._streams.size });
|
|
101
|
+
Array.from(this._streams.values()).forEach((stream) => {
|
|
102
|
+
stream.once('end', () => {
|
|
103
|
+
log('stream closed');
|
|
104
|
+
setClosed();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
stream.finalize();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await closed();
|
|
111
|
+
|
|
112
|
+
// NOTE: Depending on environment, pipeline may close before or after the stream above.
|
|
113
|
+
if (this._isOpen) {
|
|
114
|
+
await this._closedTrigger.wait();
|
|
115
|
+
assert(!this._isOpen);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this._feeds.clear();
|
|
119
|
+
this._streams.clear();
|
|
120
|
+
log('closed');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2019 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
import faker from 'faker';
|
|
7
|
+
|
|
8
|
+
import { latch } from '@dxos/async';
|
|
9
|
+
import { createKeyPair } from '@dxos/crypto';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
|
|
12
|
+
import { HypercoreFactory } from './hypercore-factory';
|
|
13
|
+
import { createReadable } from './iterator';
|
|
14
|
+
import { batch, createDataItem, TestDataItem } from './testing';
|
|
15
|
+
|
|
16
|
+
const noop = () => {};
|
|
17
|
+
|
|
18
|
+
describe('Replication', function () {
|
|
19
|
+
const factory1 = new HypercoreFactory();
|
|
20
|
+
const factory2 = new HypercoreFactory();
|
|
21
|
+
|
|
22
|
+
it('replicates feeds', async function () {
|
|
23
|
+
const { publicKey, secretKey } = createKeyPair();
|
|
24
|
+
const core1 = factory1.createFeed(publicKey, { secretKey });
|
|
25
|
+
const core2 = factory2.createFeed(publicKey);
|
|
26
|
+
|
|
27
|
+
// Open.
|
|
28
|
+
{
|
|
29
|
+
const [ready, done] = latch({ count: 2 });
|
|
30
|
+
core1.open(done);
|
|
31
|
+
core2.open(done);
|
|
32
|
+
await ready();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const numBlocks = 10;
|
|
36
|
+
|
|
37
|
+
// Sync.
|
|
38
|
+
{
|
|
39
|
+
const stream1 = core1.replicate(true);
|
|
40
|
+
const stream2 = core2.replicate(false);
|
|
41
|
+
|
|
42
|
+
const [streamsClosed, onClose] = latch({ count: 2 });
|
|
43
|
+
stream1.pipe(stream2, onClose).pipe(stream1, onClose);
|
|
44
|
+
|
|
45
|
+
expect(core1.stats.peers).to.have.lengthOf(1);
|
|
46
|
+
expect(core2.stats.peers).to.have.lengthOf(1);
|
|
47
|
+
|
|
48
|
+
// Wait for complete sync.
|
|
49
|
+
core2.on('sync', () => {
|
|
50
|
+
stream1.end();
|
|
51
|
+
stream2.end();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Write.
|
|
55
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
56
|
+
core1.append(`test-${i}`, noop);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await streamsClosed();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Close.
|
|
63
|
+
{
|
|
64
|
+
const [closed, close] = latch({ count: 2 });
|
|
65
|
+
core1.close(close);
|
|
66
|
+
core1.close(close);
|
|
67
|
+
|
|
68
|
+
await closed();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
expect(core1.stats.totals.uploadedBlocks).to.eq(numBlocks);
|
|
72
|
+
expect(core2.stats.totals.downloadedBlocks).to.eq(numBlocks);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('replicates feeds in batches', async function () {
|
|
76
|
+
const numBlocks = 100;
|
|
77
|
+
|
|
78
|
+
// Replicating feeds must have the same public key.
|
|
79
|
+
const { publicKey, secretKey } = createKeyPair();
|
|
80
|
+
const core1 = factory1.createFeed(publicKey, { secretKey });
|
|
81
|
+
const core2 = factory2.createFeed(publicKey);
|
|
82
|
+
|
|
83
|
+
// Open.
|
|
84
|
+
{
|
|
85
|
+
const [ready, done] = latch({ count: 2 });
|
|
86
|
+
core1.open(done);
|
|
87
|
+
core2.open(done);
|
|
88
|
+
await ready();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const stream1 = core1.replicate(true);
|
|
92
|
+
const stream2 = core2.replicate(false);
|
|
93
|
+
|
|
94
|
+
// Start replication.
|
|
95
|
+
// NOTE: Replication won't start unless the public keys on both sides are the same.
|
|
96
|
+
const [streamsClosed, onClose] = latch({ count: 2 });
|
|
97
|
+
stream1.pipe(stream2, onClose).pipe(stream1, onClose);
|
|
98
|
+
|
|
99
|
+
expect(core1.stats.peers).to.have.lengthOf(1);
|
|
100
|
+
expect(core2.stats.peers).to.have.lengthOf(1);
|
|
101
|
+
|
|
102
|
+
// Replicate messages.
|
|
103
|
+
{
|
|
104
|
+
// Wait until all uploaded.
|
|
105
|
+
const [waitForDownloaded, incDownload] = latch({ count: numBlocks });
|
|
106
|
+
|
|
107
|
+
// Monitor uploads.
|
|
108
|
+
let uploaded = 0;
|
|
109
|
+
core1.on('upload', () => {
|
|
110
|
+
uploaded++;
|
|
111
|
+
log('up', { uploaded, length: core1.length });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Monitor downloads.
|
|
115
|
+
let downloaded = 0;
|
|
116
|
+
core2.on('download', (seq: number, data: Buffer) => {
|
|
117
|
+
downloaded++;
|
|
118
|
+
const { id } = JSON.parse(data.toString()) as TestDataItem;
|
|
119
|
+
expect(id).to.eq(seq);
|
|
120
|
+
log('down', { downloaded, length: core2.length });
|
|
121
|
+
expect(downloaded).to.be.lessThanOrEqual(uploaded);
|
|
122
|
+
incDownload();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Monitor sync points (multiple since delayed writes).
|
|
126
|
+
const sync = { started: 0, complete: 0 };
|
|
127
|
+
core2.on('sync', () => {
|
|
128
|
+
log('sync', { events: ++sync.started });
|
|
129
|
+
core2.download();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Write batch of messages with delay.
|
|
133
|
+
batch((next, i, remaining) => {
|
|
134
|
+
const size = faker.datatype.number({
|
|
135
|
+
min: 1,
|
|
136
|
+
max: Math.min(10, remaining)
|
|
137
|
+
});
|
|
138
|
+
for (let j = 0; j < size; j++) {
|
|
139
|
+
core1.append(JSON.stringify(createDataItem(i + j)), noop);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Random delay.
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
next(size);
|
|
145
|
+
}, faker.datatype.number({ min: 0, max: 100 }));
|
|
146
|
+
}, numBlocks);
|
|
147
|
+
|
|
148
|
+
// Done.
|
|
149
|
+
await waitForDownloaded();
|
|
150
|
+
expect(uploaded).to.eq(downloaded);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Close streams.
|
|
154
|
+
{
|
|
155
|
+
stream1.end();
|
|
156
|
+
stream2.end();
|
|
157
|
+
|
|
158
|
+
await streamsClosed();
|
|
159
|
+
expect(stream1.destroyed).to.be.true;
|
|
160
|
+
expect(stream2.destroyed).to.be.true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Close feeds.
|
|
164
|
+
{
|
|
165
|
+
await core1.close();
|
|
166
|
+
await core2.close();
|
|
167
|
+
|
|
168
|
+
expect(core1.writable).to.be.false;
|
|
169
|
+
expect(core1.stats.totals.uploadedBlocks).to.eq(numBlocks);
|
|
170
|
+
expect(core2.stats.totals.downloadedBlocks).to.eq(numBlocks);
|
|
171
|
+
}
|
|
172
|
+
}).timeout(5_000);
|
|
173
|
+
|
|
174
|
+
it('replicates feeds with read stream', async function () {
|
|
175
|
+
const numBlocks = 10;
|
|
176
|
+
|
|
177
|
+
const { publicKey, secretKey } = createKeyPair();
|
|
178
|
+
const core1 = factory1.createFeed(publicKey, { secretKey });
|
|
179
|
+
const core2 = factory2.createFeed(publicKey);
|
|
180
|
+
|
|
181
|
+
// Open.
|
|
182
|
+
{
|
|
183
|
+
const [ready, done] = latch({ count: 2 });
|
|
184
|
+
core1.open(done);
|
|
185
|
+
core2.open(done);
|
|
186
|
+
await ready();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
expect(core1.opened).to.be.true;
|
|
190
|
+
expect(core2.opened).to.be.true;
|
|
191
|
+
|
|
192
|
+
const stream1 = core1.replicate(true, { live: true });
|
|
193
|
+
const stream2 = core2.replicate(false, { live: true });
|
|
194
|
+
|
|
195
|
+
const [done, onClose] = latch({ count: 2 });
|
|
196
|
+
|
|
197
|
+
// Start replication.
|
|
198
|
+
{
|
|
199
|
+
stream1.pipe(stream2, onClose).pipe(stream1, onClose);
|
|
200
|
+
|
|
201
|
+
expect(core1.stats.peers).to.have.lengthOf(1);
|
|
202
|
+
expect(core2.stats.peers).to.have.lengthOf(1);
|
|
203
|
+
|
|
204
|
+
let sync = 0;
|
|
205
|
+
core2.on('sync', () => {
|
|
206
|
+
log('sync', { events: ++sync });
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Writer.
|
|
211
|
+
{
|
|
212
|
+
setTimeout(async () => {
|
|
213
|
+
for (const _ of Array.from(Array(numBlocks))) {
|
|
214
|
+
const block = {
|
|
215
|
+
text: faker.lorem.sentence()
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
core1.append(JSON.stringify(block), noop);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Reader.
|
|
224
|
+
{
|
|
225
|
+
const [done, inc] = latch({ count: numBlocks });
|
|
226
|
+
|
|
227
|
+
setTimeout(async () => {
|
|
228
|
+
for await (const _ of createReadable(core2.createReadStream({ live: true }))) {
|
|
229
|
+
inc();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await done();
|
|
234
|
+
|
|
235
|
+
stream1.end();
|
|
236
|
+
stream2.end();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await done();
|
|
240
|
+
}).timeout(5_000);
|
|
241
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2019 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import util from 'node:util';
|
|
6
|
+
import { Writable } from 'streamx';
|
|
7
|
+
|
|
8
|
+
import { latch } from '@dxos/async';
|
|
9
|
+
import { createKeyPair } from '@dxos/crypto';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
|
|
12
|
+
import { HypercoreFactory } from './hypercore-factory';
|
|
13
|
+
|
|
14
|
+
describe('Streams', function () {
|
|
15
|
+
it('reads from stream', async function () {
|
|
16
|
+
const factory = new HypercoreFactory<string>();
|
|
17
|
+
const { publicKey, secretKey } = createKeyPair();
|
|
18
|
+
const core = factory.createFeed(publicKey, { secretKey });
|
|
19
|
+
|
|
20
|
+
const numBlocks = 10;
|
|
21
|
+
const [closed, setClosed] = latch();
|
|
22
|
+
const [processed, incProcessed] = latch({ count: numBlocks });
|
|
23
|
+
|
|
24
|
+
const stream = core.createReadStream({ live: true });
|
|
25
|
+
const consumer: Writable = stream.pipe(
|
|
26
|
+
new Writable({
|
|
27
|
+
write: (data: any, next: () => void) => {
|
|
28
|
+
log('received', { data: String(data) });
|
|
29
|
+
incProcessed();
|
|
30
|
+
next();
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
const append = util.promisify(core.append.bind(core));
|
|
37
|
+
for (const i of Array.from(Array(numBlocks)).keys()) {
|
|
38
|
+
await append(`message-${i}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await processed();
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
consumer.once('close', () => {
|
|
46
|
+
log('closed');
|
|
47
|
+
setClosed();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
consumer.destroy();
|
|
51
|
+
await closed();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('feed closed while stream is open', async function () {
|
|
56
|
+
const factory = new HypercoreFactory<string>();
|
|
57
|
+
const { publicKey, secretKey } = createKeyPair();
|
|
58
|
+
const core = factory.createFeed(publicKey, { secretKey });
|
|
59
|
+
|
|
60
|
+
const [closed, setClosed] = latch({ count: 1 });
|
|
61
|
+
{
|
|
62
|
+
const stream = core.createReadStream({ live: true });
|
|
63
|
+
stream.once('close', () => {
|
|
64
|
+
log('closed');
|
|
65
|
+
setClosed();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
stream.destroy();
|
|
69
|
+
}
|
|
70
|
+
await closed();
|
|
71
|
+
|
|
72
|
+
const close = util.promisify(core.close.bind(core));
|
|
73
|
+
await close();
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2019 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import faker from 'faker';
|
|
6
|
+
import util from 'node:util';
|
|
7
|
+
|
|
8
|
+
export const noop = () => {};
|
|
9
|
+
|
|
10
|
+
export const py = (obj: any, fn: Function) => util.promisify(fn.bind(obj));
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Replace with proto def.
|
|
13
|
+
export type TestDataItem = { id: number; text: string };
|
|
14
|
+
|
|
15
|
+
export const createDataItem = (n: number): TestDataItem => ({
|
|
16
|
+
id: n,
|
|
17
|
+
text: faker.lorem.sentence()
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
type BatchCallback = (next: (num: number) => void, index: number, remaining: number) => void;
|
|
21
|
+
|
|
22
|
+
// TODO(burdon): Factor out.
|
|
23
|
+
export const batch = (cb: BatchCallback, total: number) => {
|
|
24
|
+
let i = 0;
|
|
25
|
+
const f = () => {
|
|
26
|
+
const remaining = total - i;
|
|
27
|
+
cb(
|
|
28
|
+
(num = 1) => {
|
|
29
|
+
i += num;
|
|
30
|
+
if (i < total) {
|
|
31
|
+
f();
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
i,
|
|
35
|
+
remaining
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
f();
|
|
40
|
+
};
|