Skip to content

Commit

Permalink
Add iterableGetReadable (#15)
Browse files Browse the repository at this point in the history
* Add `iterableGetReadable`

* Add `iterableGetReadable`

* Add entry
  • Loading branch information
compulim authored Jun 9, 2024
1 parent f9dfd27 commit cfcce86
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `PushAsyncIterableIterator` in PR [#11](https://github.com/compulim/iter-fest/pull/11)
- Added `readerValues` in PR [#12](https://github.com/compulim/iter-fest/pull/12) and [#14](https://github.com/compulim/iter-fest/pull/14)
- Added `observableSubscribeAsReadable` in PR [#13](https://github.com/compulim/iter-fest/pull/13)
- Added `iterableGetReadable` in PR [#15](https://github.com/compulim/iter-fest/pull/15)

### Changed

Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ List of ported functions: [`at`](https://tc39.es/ecma262/#sec-array.prototype.at

## Conversions

| From | To | Function signature |
| ----------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `Iterator` | `IterableIterator` | [`iteratorToIterable<T>(iterator: Iterator<T>): IterableIterator<T>`](#converting-an-iterator-to-iterable) |
| `Observable` | `ReadableStream` | [`observableSubscribeAsReadable<T>(observable: Observable<T>): ReadableStream<T>`](#converting-an-observable-to-readablestream) |
| `ReadableStreamDefaultReader` | `AsyncIterableIterator` | [`readerValues`<T>(reader: ReadableStreamDefaultReader<T>): AsyncIterableIterator<T>`](#iterating-readablestreamdefaultreader) |
| `AsyncIterable` | `Observable` | [`observableFromAsync<T>(iterator: AsyncIterableIterator<T>): Observable<T>`](#converting-an-asynciterable-to-observable) |
| From | To | Function signature |
| ----------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `Iterator` | `IterableIterator` | [`iteratorToIterable<T>(iterator: Iterator<T>): IterableIterator<T>`](#converting-an-iterator-to-iterable) |
| `Observable` | `ReadableStream` | [`observableSubscribeAsReadable<T>(observable: Observable<T>): ReadableStream<T>`](#converting-an-observable-to-readablestream) |
| `ReadableStreamDefaultReader` | `AsyncIterableIterator` | [`readerValues`<T>(reader: ReadableStreamDefaultReader<T>): AsyncIterableIterator<T>`](#iterating-readablestreamdefaultreader) |
| `AsyncIterable` | `Observable` | [`observableFromAsync<T>(iterable: AsyncIterable<T>): Observable<T>`](#converting-an-asynciterable-to-observable) |
| `AsyncIterable`/`Iterable` | `ReadableStream` | [`iterableGetReadable<T>(iterable: AsyncIterable<T> | Iterable<T>): ReadableStream<T>`](#converting-an-asynciterableiterable-to-readablestream) |

To convert `Observable` to `AsyncIterableIterator`, [use `ReadableStream` as intermediate format](#converting-an-observable-to-asynciterableiterator).

Expand Down Expand Up @@ -137,6 +138,15 @@ for await (const value of readerValues(readableStream.getReader())) {
}
```

## Converting an `AsyncIterable`/`Iterable` to `ReadableStream`

```ts
const iterable = [1, 2, 3].values();
const readable = iterableGetReadable(iterable);

readable.pipeTo(stream.writable); // Will write 1, 2, 3.
```

## Others

### Typed `Observable`
Expand Down
12 changes: 12 additions & 0 deletions packages/integration-test/importDefault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
iterableFindLast,
iterableFindLastIndex,
iterableForEach,
iterableGetReadable,
iterableIncludes,
iterableIndexOf,
iterableJoin,
Expand Down Expand Up @@ -62,6 +63,17 @@ test('iterableForEach should work', () => {
expect(callbackfn).toHaveBeenCalledTimes(3);
});

test('iterableGetReadable should work', async () => {
const iterable = [1, 2, 3].values();

const reader = iterableGetReadable(iterable).getReader();

await expect(reader.read()).resolves.toEqual({ done: false, value: 1 });
await expect(reader.read()).resolves.toEqual({ done: false, value: 2 });
await expect(reader.read()).resolves.toEqual({ done: false, value: 3 });
await expect(reader.read()).resolves.toEqual({ done: true, value: undefined });
});

test('iterableIncludes should work', () => expect(iterableIncludes([1, 2, 3], 2)).toBe(true));

test('iterableIndexOf should work', () => expect(iterableIndexOf([1, 2, 3], 2)).toBe(1));
Expand Down
12 changes: 12 additions & 0 deletions packages/integration-test/importNamed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { iterableFindIndex } from 'iter-fest/iterableFindIndex';
import { iterableFindLast } from 'iter-fest/iterableFindLast';
import { iterableFindLastIndex } from 'iter-fest/iterableFindLastIndex';
import { iterableForEach } from 'iter-fest/iterableForEach';
import { iterableGetReadable } from 'iter-fest/iterableGetReadable';
import { iterableIncludes } from 'iter-fest/iterableIncludes';
import { iterableIndexOf } from 'iter-fest/iterableIndexOf';
import { iterableJoin } from 'iter-fest/iterableJoin';
Expand Down Expand Up @@ -60,6 +61,17 @@ test('iterableForEach should work', () => {
expect(callbackfn).toHaveBeenCalledTimes(3);
});

test('iterableGetReadable should work', async () => {
const iterable = [1, 2, 3].values();

const reader = iterableGetReadable(iterable).getReader();

await expect(reader.read()).resolves.toEqual({ done: false, value: 1 });
await expect(reader.read()).resolves.toEqual({ done: false, value: 2 });
await expect(reader.read()).resolves.toEqual({ done: false, value: 3 });
await expect(reader.read()).resolves.toEqual({ done: true, value: undefined });
});

test('iterableIncludes should work', () => expect(iterableIncludes([1, 2, 3], 2)).toBe(true));

test('iterableIndexOf should work', () => expect(iterableIndexOf([1, 2, 3], 2)).toBe(1));
Expand Down
12 changes: 12 additions & 0 deletions packages/integration-test/requireNamed.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const { iterableFindIndex } = require('iter-fest/iterableFindIndex');
const { iterableFindLast } = require('iter-fest/iterableFindLast');
const { iterableFindLastIndex } = require('iter-fest/iterableFindLastIndex');
const { iterableForEach } = require('iter-fest/iterableForEach');
const { iterableGetReadable } = require('iter-fest/iterableGetReadable');
const { iterableIncludes } = require('iter-fest/iterableIncludes');
const { iterableIndexOf } = require('iter-fest/iterableIndexOf');
const { iterableJoin } = require('iter-fest/iterableJoin');
Expand Down Expand Up @@ -61,6 +62,17 @@ test('iterableForEach should work', () => {
expect(callbackfn).toHaveBeenCalledTimes(3);
});

test('iterableGetReadable should work', async () => {
const iterable = [1, 2, 3].values();

const reader = iterableGetReadable(iterable).getReader();

await expect(reader.read()).resolves.toEqual({ done: false, value: 1 });
await expect(reader.read()).resolves.toEqual({ done: false, value: 2 });
await expect(reader.read()).resolves.toEqual({ done: false, value: 3 });
await expect(reader.read()).resolves.toEqual({ done: true, value: undefined });
});

test('iterableIncludes should work', () => expect(iterableIncludes([1, 2, 3], 2)).toBe(true));

test('iterableIndexOf should work', () => expect(iterableIndexOf([1, 2, 3], 2)).toBe(1));
Expand Down
12 changes: 12 additions & 0 deletions packages/integration-test/requiredDefault.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
iterableFindLast,
iterableFindLastIndex,
iterableForEach,
iterableGetReadable,
iterableIncludes,
iterableIndexOf,
iterableJoin,
Expand Down Expand Up @@ -63,6 +64,17 @@ test('iterableForEach should work', () => {
expect(callbackfn).toHaveBeenCalledTimes(3);
});

test('iterableGetReadable should work', async () => {
const iterable = [1, 2, 3].values();

const reader = iterableGetReadable(iterable).getReader();

await expect(reader.read()).resolves.toEqual({ done: false, value: 1 });
await expect(reader.read()).resolves.toEqual({ done: false, value: 2 });
await expect(reader.read()).resolves.toEqual({ done: false, value: 3 });
await expect(reader.read()).resolves.toEqual({ done: true, value: undefined });
});

test('iterableIncludes should work', () => expect(iterableIncludes([1, 2, 3], 2)).toBe(true));

test('iterableIndexOf should work', () => expect(iterableIndexOf([1, 2, 3], 2)).toBe(1));
Expand Down
10 changes: 10 additions & 0 deletions packages/iter-fest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@
"default": "./dist/iter-fest.iterableForEach.js"
}
},
"./iterableGetReadable": {
"import": {
"types": "./dist/iter-fest.iterableGetReadable.d.mts",
"default": "./dist/iter-fest.iterableGetReadable.mjs"
},
"require": {
"types": "./dist/iter-fest.iterableGetReadable.d.ts",
"default": "./dist/iter-fest.iterableGetReadable.js"
}
},
"./iterableIncludes": {
"import": {
"types": "./dist/iter-fest.iterableIncludes.d.mts",
Expand Down
1 change: 1 addition & 0 deletions packages/iter-fest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './iterableFindIndex';
export * from './iterableFindLast';
export * from './iterableFindLastIndex';
export * from './iterableForEach';
export * from './iterableGetReadable';
export * from './iterableIncludes';
export * from './iterableIndexOf';
export * from './iterableJoin';
Expand Down
139 changes: 139 additions & 0 deletions packages/iter-fest/src/iterableGetReadable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { iterableGetReadable } from './iterableGetReadable';
import type { JestMockOf } from './private/JestMockOf';
import hasResolved from './private/hasResolved';
import withResolvers from './private/withResolvers';

describe.each(['AsyncIterator' as const, 'Iterator' as const])('with %s', type => {
let next: JestMockOf<() => unknown>;
let readable: ReadableStream;
let reader: ReadableStreamDefaultReader;

beforeEach(() => {
if (type === 'AsyncIterator') {
const iterator: AsyncIterator<number> = {
next: jest
.fn()
.mockImplementationOnce(() => Promise.resolve({ value: 1 }))
.mockImplementationOnce(() => Promise.resolve({ done: true, value: undefined }))
};

next = iterator.next as JestMockOf<() => unknown>;

readable = iterableGetReadable({
[Symbol.asyncIterator]() {
return iterator;
}
});
} else {
const iterator: Iterator<number> = {
next: jest
.fn()
.mockImplementationOnce(() => ({ value: 1 }))
.mockImplementationOnce(() => ({ done: true, value: undefined }))
};

next = iterator.next as JestMockOf<() => unknown>;

readable = iterableGetReadable({
[Symbol.iterator]() {
return iterator;
}
});
}

reader = readable.getReader();
});

test('should have been called next() once', () => expect(next).toHaveBeenCalledTimes(1));

describe('when call read()', () => {
let readPromise: Promise<ReadableStreamReadResult<number>>;

beforeEach(() => {
readPromise = reader.read();
});

test('should read value 1', () => expect(readPromise).resolves.toEqual({ done: false, value: 1 }));
test('should have been called next() twice', () => expect(next).toHaveBeenCalledTimes(2));

describe('when call read() again', () => {
let readPromise: Promise<ReadableStreamReadResult<number>>;

beforeEach(() => {
readPromise = reader.read();
});

test('should read value done', () => expect(readPromise).resolves.toEqual({ done: true, value: undefined }));
test('should have been called next() twice', () => expect(next).toHaveBeenCalledTimes(2));
});
});
});

describe('comprehensive', () => {
let next: JestMockOf<() => Promise<IteratorResult<number>>>;
let readable: ReadableStream;
let reader: ReadableStreamDefaultReader;
let deferreds: PromiseWithResolvers<IteratorResult<number>>[];

beforeEach(() => {
deferreds = [];

const iterator: AsyncIterator<number> = {
next: jest.fn().mockImplementation(() => {
const deferred = withResolvers<IteratorResult<number>>();

deferreds.push(deferred);

return deferred.promise;
})
};

next = iterator.next as JestMockOf<() => Promise<IteratorResult<number>>>;

readable = iterableGetReadable({
[Symbol.asyncIterator]() {
return iterator;
}
});

reader = readable.getReader();
});

describe('when read() is called', () => {
let readPromise: Promise<ReadableStreamReadResult<number>>;

beforeEach(() => {
readPromise = reader.read();
});

test('next() should have been called once', () => expect(next).toHaveBeenCalledTimes(1));
test('read() should not have been resolved', () => expect(hasResolved(readPromise)).resolves.toBe(false));

describe('when next() is resolved with 1', () => {
beforeEach(() => deferreds[0]?.resolve({ value: 1 }));

test('read() should have been resolved to 1', () =>
expect(readPromise).resolves.toEqual({ done: false, value: 1 }));
test('next() should have been called twice', () => expect(next).toHaveBeenCalledTimes(2));

describe('when read() is called again', () => {
let readPromise: Promise<ReadableStreamReadResult<number>>;

beforeEach(() => {
readPromise = reader.read();
});

test('next() should have been called twice', () => expect(next).toHaveBeenCalledTimes(2));
test('read() should not have been resolved', () => expect(hasResolved(readPromise)).resolves.toBe(false));

describe('when next() is resolved with done', () => {
beforeEach(() => deferreds[1]?.resolve({ done: true, value: undefined }));

test('read() should have been resolved to done', () =>
expect(readPromise).resolves.toEqual({ done: true, value: undefined }));
test('next() should have been called twice', () => expect(next).toHaveBeenCalledTimes(2));
});
});
});
});
});
19 changes: 19 additions & 0 deletions packages/iter-fest/src/iterableGetReadable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function isIterable(iterable: unknown): iterable is Iterable<unknown> {
return !!(iterable && typeof iterable === 'object' && Symbol.iterator in iterable);
}

export function iterableGetReadable<T>(iterable: AsyncIterable<T> | Iterable<T>): ReadableStream<T> {
const iterator = isIterable(iterable) ? iterable[Symbol.iterator]() : iterable[Symbol.asyncIterator]();

return new ReadableStream({
async pull(controller) {
const result = await iterator.next();

if (result.done) {
controller.close();
} else {
controller.enqueue(result.value);
}
}
});
}
1 change: 1 addition & 0 deletions packages/iter-fest/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default defineConfig([
'iter-fest.iterableFindLast': './src/iterableFindLast.ts',
'iter-fest.iterableFindLastIndex': './src/iterableFindLastIndex.ts',
'iter-fest.iterableForEach': './src/iterableForEach.ts',
'iter-fest.iterableGetReadable': './src/iterableGetReadable.ts',
'iter-fest.iterableIncludes': './src/iterableIncludes.ts',
'iter-fest.iterableIndexOf': './src/iterableIndexOf.ts',
'iter-fest.iterableJoin': './src/iterableJoin.ts',
Expand Down

0 comments on commit cfcce86

Please sign in to comment.