diff --git a/CHANGELOG.md b/CHANGELOG.md index 506abd2..60b98a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index ac7d4df..e449a18 100644 --- a/README.md +++ b/README.md @@ -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(iterator: Iterator): IterableIterator`](#converting-an-iterator-to-iterable) | -| `Observable` | `ReadableStream` | [`observableSubscribeAsReadable(observable: Observable): ReadableStream`](#converting-an-observable-to-readablestream) | -| `ReadableStreamDefaultReader` | `AsyncIterableIterator` | [`readerValues`(reader: ReadableStreamDefaultReader): AsyncIterableIterator`](#iterating-readablestreamdefaultreader) | -| `AsyncIterable` | `Observable` | [`observableFromAsync(iterator: AsyncIterableIterator): Observable`](#converting-an-asynciterable-to-observable) | +| From | To | Function signature | +| ----------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `Iterator` | `IterableIterator` | [`iteratorToIterable(iterator: Iterator): IterableIterator`](#converting-an-iterator-to-iterable) | +| `Observable` | `ReadableStream` | [`observableSubscribeAsReadable(observable: Observable): ReadableStream`](#converting-an-observable-to-readablestream) | +| `ReadableStreamDefaultReader` | `AsyncIterableIterator` | [`readerValues`(reader: ReadableStreamDefaultReader): AsyncIterableIterator`](#iterating-readablestreamdefaultreader) | +| `AsyncIterable` | `Observable` | [`observableFromAsync(iterable: AsyncIterable): Observable`](#converting-an-asynciterable-to-observable) | +| `AsyncIterable`/`Iterable` | `ReadableStream` | [`iterableGetReadable(iterable: AsyncIterable | Iterable): ReadableStream`](#converting-an-asynciterableiterable-to-readablestream) | To convert `Observable` to `AsyncIterableIterator`, [use `ReadableStream` as intermediate format](#converting-an-observable-to-asynciterableiterator). @@ -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` diff --git a/packages/integration-test/importDefault.test.ts b/packages/integration-test/importDefault.test.ts index 294606b..0547f5e 100644 --- a/packages/integration-test/importDefault.test.ts +++ b/packages/integration-test/importDefault.test.ts @@ -13,6 +13,7 @@ import { iterableFindLast, iterableFindLastIndex, iterableForEach, + iterableGetReadable, iterableIncludes, iterableIndexOf, iterableJoin, @@ -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)); diff --git a/packages/integration-test/importNamed.test.ts b/packages/integration-test/importNamed.test.ts index 0ff5c10..7ae1654 100644 --- a/packages/integration-test/importNamed.test.ts +++ b/packages/integration-test/importNamed.test.ts @@ -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'; @@ -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)); diff --git a/packages/integration-test/requireNamed.test.cjs b/packages/integration-test/requireNamed.test.cjs index 761e37c..7242edd 100644 --- a/packages/integration-test/requireNamed.test.cjs +++ b/packages/integration-test/requireNamed.test.cjs @@ -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'); @@ -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)); diff --git a/packages/integration-test/requiredDefault.test.cjs b/packages/integration-test/requiredDefault.test.cjs index 45793cd..82a88ca 100644 --- a/packages/integration-test/requiredDefault.test.cjs +++ b/packages/integration-test/requiredDefault.test.cjs @@ -11,6 +11,7 @@ const { iterableFindLast, iterableFindLastIndex, iterableForEach, + iterableGetReadable, iterableIncludes, iterableIndexOf, iterableJoin, @@ -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)); diff --git a/packages/iter-fest/package.json b/packages/iter-fest/package.json index 9ca9700..1399f91 100644 --- a/packages/iter-fest/package.json +++ b/packages/iter-fest/package.json @@ -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", diff --git a/packages/iter-fest/src/index.ts b/packages/iter-fest/src/index.ts index 70ee2ac..cef4dfb 100644 --- a/packages/iter-fest/src/index.ts +++ b/packages/iter-fest/src/index.ts @@ -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'; diff --git a/packages/iter-fest/src/iterableGetReadable.spec.ts b/packages/iter-fest/src/iterableGetReadable.spec.ts new file mode 100644 index 0000000..a6a6ae4 --- /dev/null +++ b/packages/iter-fest/src/iterableGetReadable.spec.ts @@ -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 = { + 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 = { + 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>; + + 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>; + + 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>>; + let readable: ReadableStream; + let reader: ReadableStreamDefaultReader; + let deferreds: PromiseWithResolvers>[]; + + beforeEach(() => { + deferreds = []; + + const iterator: AsyncIterator = { + next: jest.fn().mockImplementation(() => { + const deferred = withResolvers>(); + + deferreds.push(deferred); + + return deferred.promise; + }) + }; + + next = iterator.next as JestMockOf<() => Promise>>; + + readable = iterableGetReadable({ + [Symbol.asyncIterator]() { + return iterator; + } + }); + + reader = readable.getReader(); + }); + + describe('when read() is called', () => { + let readPromise: Promise>; + + 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>; + + 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)); + }); + }); + }); + }); +}); diff --git a/packages/iter-fest/src/iterableGetReadable.ts b/packages/iter-fest/src/iterableGetReadable.ts new file mode 100644 index 0000000..fc37fba --- /dev/null +++ b/packages/iter-fest/src/iterableGetReadable.ts @@ -0,0 +1,19 @@ +function isIterable(iterable: unknown): iterable is Iterable { + return !!(iterable && typeof iterable === 'object' && Symbol.iterator in iterable); +} + +export function iterableGetReadable(iterable: AsyncIterable | Iterable): ReadableStream { + 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); + } + } + }); +} diff --git a/packages/iter-fest/tsup.config.ts b/packages/iter-fest/tsup.config.ts index cc4cdf6..536629d 100644 --- a/packages/iter-fest/tsup.config.ts +++ b/packages/iter-fest/tsup.config.ts @@ -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',