diff --git a/apps/content/.vitepress/config.ts b/apps/content/.vitepress/config.ts index c7688fe2a..4cb53ead4 100644 --- a/apps/content/.vitepress/config.ts +++ b/apps/content/.vitepress/config.ts @@ -88,7 +88,7 @@ export default withMermaid(defineConfig({ { text: 'Context', link: '/docs/context' }, { text: 'Error Handling', link: '/docs/error-handling' }, { text: 'Binary Data', link: '/docs/binary-data' }, - { text: 'Event Iterator (SSE)', link: '/docs/event-iterator' }, + { text: 'AsyncIteratorObject (SSE)', link: '/docs/async-iterator-object' }, { text: 'Metadata', link: '/docs/metadata' }, { text: 'Playgrounds', link: '/docs/playgrounds' }, { @@ -131,7 +131,7 @@ export default withMermaid(defineConfig({ { text: 'Server-Side', link: '/docs/client/server-side' }, { text: 'Client-Side', link: '/docs/client/client-side' }, { text: 'Error Handling', link: '/docs/client/error-handling' }, - { text: 'Event Iterator', link: '/docs/client/event-iterator' }, + { text: 'AsyncIteratorObject (SSE)', link: '/docs/client/async-iterator-object' }, { text: 'Dynamic Link', link: '/docs/client/dynamic-link' }, ], }, diff --git a/apps/content/docs/adapters/fetch-api.md b/apps/content/docs/adapters/fetch-api.md index 8d8df96c9..3b9be5883 100644 --- a/apps/content/docs/adapters/fetch-api.md +++ b/apps/content/docs/adapters/fetch-api.md @@ -155,7 +155,7 @@ The examples above only show how to configure the link. For examples of creating ## Event Stream Options -You can configure how [event iterators](/docs/event-iterator) are streamed to the client using the `toFetchResponse.eventStream` options when creating the handler. +You can configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `toFetchResponse.eventStream` options when creating the handler. ```ts const handler = new OpenAPIHandler(router, { @@ -209,5 +209,5 @@ const handler = new OpenAPIHandler(router, { ``` ::: info -You can also configure how [event iterators](/docs/event-iterator) are streamed from client to server using `toFetchRequest.eventStream` options when creating the link. However, this is rarely used because streaming requests are not widely supported in browsers and may require manually overriding the `fetch` function with `duplex`. +You can also configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed from client to server using `toFetchRequest.eventStream` options when creating the link. However, this is rarely used because streaming requests are not widely supported in browsers and may require manually overriding the `fetch` function with `duplex`. ::: diff --git a/apps/content/docs/adapters/node-http.md b/apps/content/docs/adapters/node-http.md index a1c125703..52a8f7296 100644 --- a/apps/content/docs/adapters/node-http.md +++ b/apps/content/docs/adapters/node-http.md @@ -80,7 +80,7 @@ server.listen(3000, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000' ## Event Stream Options -You can configure how [event iterators](/docs/event-iterator) are streamed to the client using the `sendStandardResponse.eventStream` options when creating the handler. +You can configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `sendStandardResponse.eventStream` options when creating the handler. ```ts const handler = new OpenAPIHandler(router, { diff --git a/apps/content/docs/event-iterator.md b/apps/content/docs/async-iterator-object.md similarity index 76% rename from apps/content/docs/event-iterator.md rename to apps/content/docs/async-iterator-object.md index a83635ca0..662df4a16 100644 --- a/apps/content/docs/event-iterator.md +++ b/apps/content/docs/async-iterator-object.md @@ -1,10 +1,10 @@ -# Event Iterator (SSE) +# AsyncIteratorObject (SSE) -Event Iterator enables **typesafe**, **realtime data streaming**. It is the recommended approach for building features like live notifications, chat messages, progress updates, and data feeds. +AsyncIteratorObject enables **typesafe**, **realtime data streaming**. It is the recommended approach for building features like live notifications, chat messages, progress updates, and data feeds. ## Overview -An event iterator is implemented as an [asynchronous generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*) (or a compatible implementation). In the example below, the handler emits a new event every second: +An `AsyncIteratorObject` is implemented as an [asynchronous generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*) (or a compatible implementation). In the example below, the handler emits a new event every second: ```ts const example = os @@ -18,18 +18,18 @@ const example = os ``` ::: info -Learn how to consume event iterators from the client in the [client guide](/docs/client/event-iterator). +Learn how to consume an `AsyncIteratorObject` from the client in the [client guide](/docs/client/async-iterator-object). ::: ## Validating Events -Use the built‑in `eventIterator` helper that works with any [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library to validate events. +Use the built‑in `asyncIteratorObject` schema that works with any [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library to validate events. ```ts -import { eventIterator } from '@orpc/server' +import { asyncIteratorObject } from '@orpc/server' const example = os - .output(eventIterator(z.object({ message: z.string() }))) + .output(asyncIteratorObject(z.object({ message: z.string() }))) .handler(async function* ({ input, signal, lastEventId }) { while (true) { signal?.throwIfAborted() @@ -68,7 +68,7 @@ const example = os }) ``` -## Stop Event Iterator +## Stop AsyncIteratorObject To end the stream, use either a `return` or `throw` statement. oRPC marks the stream as completed when the handler returns. @@ -114,7 +114,7 @@ const example = os ## Publisher Helper -You can combine the event iterator with the [Publisher Helper](/docs/helpers/publisher) to build real-time features like chat, notifications, or live updates with resume support. +You can combine the [AsyncIteratorObject](/docs/async-iterator-object) with the [Publisher Helper](/docs/helpers/publisher) to build real-time features like chat, notifications, or live updates with resume support. ```ts const publisher = new MemoryPublisher<{ diff --git a/apps/content/docs/binary-data.md b/apps/content/docs/binary-data.md index 036b75b01..62e2787e5 100644 --- a/apps/content/docs/binary-data.md +++ b/apps/content/docs/binary-data.md @@ -30,7 +30,7 @@ const example = os }) ``` -## `ReadableStream` +## ReadableStream\ Procedures can return `ReadableStream` to stream binary responses. The example below uses the [Response Headers Plugin](/docs/plugins/response-headers) to set the appropriate `Content-Type` header. diff --git a/apps/content/docs/client/event-iterator.md b/apps/content/docs/client/async-iterator-object.md similarity index 66% rename from apps/content/docs/client/event-iterator.md rename to apps/content/docs/client/async-iterator-object.md index b4cde24ac..94b91e11f 100644 --- a/apps/content/docs/client/event-iterator.md +++ b/apps/content/docs/client/async-iterator-object.md @@ -1,15 +1,15 @@ -# Event Iterator in Client +# AsyncIteratorObject in Client -Consume an [Event Iterator](/docs/event-iterator) like an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator). Await the call, then iterate over events as they arrive. +Consume an [AsyncIteratorObject](/docs/async-iterator-object) like an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator). Await the call, then iterate over events as they arrive. ## Basic Usage ```ts twoslash -import { eventIterator, oc, RouterContractClient } from '@orpc/contract' +import { asyncIteratorObject, oc, RouterContractClient } from '@orpc/contract' import { z } from 'zod' const contract = { - streaming: oc.output(eventIterator(z.object({ message: z.string() }))) + streaming: oc.output(asyncIteratorObject(z.object({ message: z.string() }))) } declare const client: RouterContractClient @@ -44,7 +44,7 @@ for await (const event of iterator) { ## Error Handling ::: info -Unlike traditional SSE, Event Iterators do not retry automatically after an error. To add retries, use the [Retry Plugin](/docs/plugins/retry#event-source-simulation). +Unlike traditional SSE, AsyncIteratorObjects do not retry automatically after an error. To add retries, use the [Retry Plugin](/docs/plugins/retry#event-source-simulation). ::: ```ts @@ -77,14 +77,14 @@ for await (const event of iterator) { } ``` -## Using `consumeEventIterator` +## Using `consumeAsyncIterator` -Use `consumeEventIterator` to consume an event iterator with lifecycle callbacks. It accepts either an event iterator or a promise that resolves to one. +Use `consumeAsyncIterator` to consume an `AsyncIterator` with lifecycle callbacks. It accepts either an iterator or a promise that resolves to one. ```ts -import { consumeEventIterator } from '@orpc/client' +import { consumeAsyncIterator } from '@orpc/client' -const cancel = consumeEventIterator(client.streaming(), { +const cancel = consumeAsyncIterator(client.streaming(), { onEvent: (event) => { console.log(event.message) }, diff --git a/apps/content/docs/integrations/nest.md b/apps/content/docs/integrations/nest.md index 15e3603b1..4d7012e00 100644 --- a/apps/content/docs/integrations/nest.md +++ b/apps/content/docs/integrations/nest.md @@ -221,7 +221,7 @@ Procedures run only when a matching NestJS controller method is called. If no ro ### Event Stream Options -Configure how [event iterators](/docs/event-iterator) are streamed to the client using the `toNestResponse.eventStream` options. +Configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `toNestResponse.eventStream` options. ```ts @Module({ diff --git a/apps/content/docs/integrations/opentelemetry.md b/apps/content/docs/integrations/opentelemetry.md index 3328b62e4..e343983b0 100644 --- a/apps/content/docs/integrations/opentelemetry.md +++ b/apps/content/docs/integrations/opentelemetry.md @@ -120,7 +120,7 @@ Define the `name` property on your middleware to improve span naming and make tr ## Capture Abort Signals -If your application heavily uses [Event Iterator](/docs/event-iterator) or similar streaming patterns, we recommend capturing an event when the `signal` is aborted to properly track and detach unexpected long-running operations: +If your application heavily uses [AsyncIteratorObject](/docs/async-iterator-object) or similar streaming patterns, we recommend capturing an event when the `signal` is aborted to properly track and detach unexpected long-running operations: ```ts import { trace } from '@opentelemetry/api' diff --git a/apps/content/docs/integrations/tanstack-query.md b/apps/content/docs/integrations/tanstack-query.md index 6ce11ec0c..acc8096d6 100644 --- a/apps/content/docs/integrations/tanstack-query.md +++ b/apps/content/docs/integrations/tanstack-query.md @@ -89,7 +89,7 @@ const query = useQuery(orpc.planet.find.queryOptions({ ## Streamed Query Options -Use `.streamedOptions` to build streamed query options for [Event Iterator](/docs/event-iterator). The resulting data is an array of events, and each new event is appended as it arrives. +Use `.streamedOptions` to build streamed query options for an [AsyncIteratorObject](/docs/async-iterator-object). The resulting data is an array of events, and each new event is appended as it arrives. It works with `useQuery`, `useSuspenseQuery`, and `prefetchQuery`, and any other API that accepts query options. @@ -117,7 +117,7 @@ const query = useQuery(orpc.streamed.streamedOptions({ ## Live Query Options -Use `.liveOptions` to build live query options for [Event Iterator](/docs/event-iterator). The data always reflects the latest event, replacing the previous value whenever a new one arrives. +Use `.liveOptions` to build live query options for an [AsyncIteratorObject](/docs/async-iterator-object). The data always reflects the latest event, replacing the previous value whenever a new one arrives. It works with `useQuery`, `useSuspenseQuery`, and `prefetchQuery`, and any other API that accepts query options. diff --git a/apps/content/docs/openapi/handler.md b/apps/content/docs/openapi/handler.md index 5e3309e69..b5a93d80c 100644 --- a/apps/content/docs/openapi/handler.md +++ b/apps/content/docs/openapi/handler.md @@ -223,7 +223,7 @@ If you use `OpenAPILink` with a custom server-side error format, make sure to co ## Event Stream Options -Configure how [event iterators](/docs/event-iterator) are streamed to the client. Available options depend on the adapter. For example, the fetch adapter supports: +Configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client. Available options depend on the adapter. For example, the fetch adapter supports: ```ts const handler = new OpenAPIHandler(router, { diff --git a/apps/content/docs/openapi/input-and-output-mapping.md b/apps/content/docs/openapi/input-and-output-mapping.md index beee88841..755f83703 100644 --- a/apps/content/docs/openapi/input-and-output-mapping.md +++ b/apps/content/docs/openapi/input-and-output-mapping.md @@ -218,15 +218,15 @@ const uploadLargeFile = os Supported body hints: -| Hint | Parsed Result | -| ------------------- | --------------------------------------------------------------------------------- | -| `json` | JSON value | -| `form-data` | `FormData` decoded with [bracket notation](/docs/openapi/bracket-notation) | -| `url-search-params` | `URLSearchParams` decoded with [bracket notation](/docs/openapi/bracket-notation) | -| `event-stream` | [Event Iterator](/docs/event-iterator) | -| `octet-stream` | `ReadableStream` for streamed binary data | -| `file` | `File` for binary data | -| `none` | `undefined` | +| Hint | Parsed Result | +| ------------------- | --------------------------------------------------------------------------------------------------- | +| `json` | JSON value | +| `form-data` | `FormData` decoded with [bracket notation](/docs/openapi/bracket-notation) | +| `url-search-params` | `URLSearchParams` decoded with [bracket notation](/docs/openapi/bracket-notation) | +| `event-stream` | [AsyncIteratorObject](/docs/async-iterator-object) | +| `octet-stream` | [ReadableStream\](/docs/binary-data#readablestreamuint8array) for streamed binary data | +| `file` | `File` for binary data | +| `none` | `undefined` | ::: info Learn more about body hints in the [Standard Server documentation](https://github.com/middleapi/standardserver#standard-body) diff --git a/apps/content/docs/openapi/link.md b/apps/content/docs/openapi/link.md index 4f800fdc0..11f7c27e0 100644 --- a/apps/content/docs/openapi/link.md +++ b/apps/content/docs/openapi/link.md @@ -258,7 +258,7 @@ const link = new OpenAPILink(contract, { ## Event Stream Options -Configure how [event iterators](/docs/event-iterator) are streamed to the server. Available options depend on the adapter. For example, the fetch adapter supports: +Configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the server. Available options depend on the adapter. For example, the fetch adapter supports: ```ts const link = new OpenAPILink(contract, { diff --git a/apps/content/docs/openapi/serializer.md b/apps/content/docs/openapi/serializer.md index 8e0fa337b..405650b36 100644 --- a/apps/content/docs/openapi/serializer.md +++ b/apps/content/docs/openapi/serializer.md @@ -6,26 +6,26 @@ OpenAPI Serializers handle one-way serialization to JSON-friendly formats. They `OpenAPISerializer` supports the following types by default: -| Type | Handler key | Serialized | Notes | -| ---------------------------------------- | ----------- | ------------------ | ----------------------------- | -| **string** | | | | -| **number** | | | | -| **NaN** | `nan` | `null` | | -| **boolean** | | | | -| **null** | | | | -| **undefined** | `undefined` | `null` | Ignore `undefined` properties | -| **Date** | `date` | ISO String, `null` | | -| **BigInt** | `bigint` | string | | -| **RegExp** | `regexp` | string | | -| **URL** | `url` | string | | -| **Record (object)** | | | `toJSON` methods are ignored | -| **Array** | | | | -| **Set** | `set` | array | | -| **Map** | `map` | array | | -| **Blob** | | | Unsupported in Event Iterator | -| **File** | | | Unsupported in Event Iterator | -| **Event Iterator (AsyncIteratorObject)** | | | Only at the root level | -| **ReadableStream\** | | | Only at the root level | +| Type | Handler key | Serialized | Notes | +| -------------------------------- | ----------- | ------------------ | ------------------------------------ | +| **string** | | | | +| **number** | | | | +| **NaN** | `nan` | `null` | | +| **boolean** | | | | +| **null** | | | | +| **undefined** | `undefined` | `null` | Ignore `undefined` properties | +| **Date** | `date` | ISO String, `null` | | +| **BigInt** | `bigint` | string | | +| **RegExp** | `regexp` | string | | +| **URL** | `url` | string | | +| **Record (object)** | | | `toJSON` methods are ignored | +| **Array** | | | | +| **Set** | `set` | array | | +| **Map** | `map` | array | | +| **Blob** | | | Unsupported in `AsyncIteratorObject` | +| **File** | | | Unsupported in `AsyncIteratorObject` | +| **AsyncIteratorObject** | | | Only at the root level | +| **ReadableStream\** | | | Only at the root level | @@ -128,9 +128,9 @@ Standard-Server: file If the receiver mistakenly handles this payload as a regular (non-file) body, set the `standard-server` header to help the receiver detect the actual data type and handle it correctly. Learn more about this header in the [Standard Server Documentation](https://github.com/middleapi/standardserver#resolving-body). ::: -### Event Iterator (AsyncIteratorObject) +### AsyncIteratorObject -When the output is an event iterator (`AsyncIteratorObject`), it is sent as a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. Each event contains one serialized chunk of data. +When the output is an `AsyncIteratorObject`, it is sent as a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. Each event contains one serialized chunk of data. ```http HTTP/1.1 200 OK diff --git a/apps/content/docs/plugins/retry.md b/apps/content/docs/plugins/retry.md index e46cbf0e6..b284628ad 100644 --- a/apps/content/docs/plugins/retry.md +++ b/apps/content/docs/plugins/retry.md @@ -76,7 +76,7 @@ const link = new RPCLink({ ## Event Source Simulation -To replicate the behavior of [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) for [Event Iterator](/docs/event-iterator), use the following configuration: +To replicate the behavior of [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) for an [AsyncIteratorObject](/docs/async-iterator-object), use the following configuration: ```ts const streaming = await client.streaming('the input', { diff --git a/apps/content/docs/rpc/handler.md b/apps/content/docs/rpc/handler.md index d2b72e680..81d12e1e4 100644 --- a/apps/content/docs/rpc/handler.md +++ b/apps/content/docs/rpc/handler.md @@ -221,7 +221,7 @@ const handler = new RPCHandler(router, { ## Event Stream Options -Configure how [event iterators](/docs/event-iterator) are streamed to the client. Available options depend on the adapter. For example, the fetch adapter supports: +Configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client. Available options depend on the adapter. For example, the fetch adapter supports: ```ts const handler = new RPCHandler(router, { diff --git a/apps/content/docs/rpc/link.md b/apps/content/docs/rpc/link.md index b4a6dc028..3ef11a215 100644 --- a/apps/content/docs/rpc/link.md +++ b/apps/content/docs/rpc/link.md @@ -278,7 +278,7 @@ const link = new RPCLink({ ## Event Stream Options -Configure how [event iterators](/docs/event-iterator) are streamed to the server. Available options depend on the adapter. For example, the fetch adapter supports: +Configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the server. Available options depend on the adapter. For example, the fetch adapter supports: ```ts const link = new RPCLink({ diff --git a/apps/content/docs/rpc/serializer.md b/apps/content/docs/rpc/serializer.md index cbd2c2851..ae3d78e58 100644 --- a/apps/content/docs/rpc/serializer.md +++ b/apps/content/docs/rpc/serializer.md @@ -6,26 +6,26 @@ RPC Serializers handle the serialization and deserialization of data sent betwee `RPCSerializer` supports the following types by default: -| Type | Handler key | Notes | -| ---------------------------------------- | ----------- | ----------------------------- | -| **string** | | | -| **number** | | | -| **NaN** | `nan` | | -| **boolean** | | | -| **null** | | | -| **undefined** | `undefined` | Ignore `undefined` properties | -| **Date** | `date` | Includes `Invalid Date`. | -| **BigInt** | `bigint` | | -| **RegExp** | `regexp` | | -| **URL** | `url` | | -| **Record (object)** | | `toJSON` methods are ignored | -| **Array** | | | -| **Set** | `set` | | -| **Map** | `map` | | -| **Blob** | | Unsupported in Event Iterator | -| **File** | | Unsupported in Event Iterator | -| **Event Iterator (AsyncIteratorObject)** | | Only at the root level | -| **ReadableStream\** | | Only at the root level | +| Type | Handler key | Notes | +| -------------------------------- | ----------- | ------------------------------------ | +| **string** | | | +| **number** | | | +| **NaN** | `nan` | | +| **boolean** | | | +| **null** | | | +| **undefined** | `undefined` | Ignore `undefined` properties | +| **Date** | `date` | Includes `Invalid Date`. | +| **BigInt** | `bigint` | | +| **RegExp** | `regexp` | | +| **URL** | `url` | | +| **Record (object)** | | `toJSON` methods are ignored | +| **Array** | | | +| **Set** | `set` | | +| **Map** | `map` | | +| **Blob** | | Unsupported in `AsyncIteratorObject` | +| **File** | | Unsupported in `AsyncIteratorObject` | +| **AsyncIteratorObject** | | Only at the root level | +| **ReadableStream\** | | Only at the root level | @@ -144,9 +144,9 @@ Standard-Server: file If the receiver mistakenly handles this payload as a regular (non-file) body, set the `standard-server` header to help the receiver detect the actual data type and handle it correctly. Learn more about this header in the [Standard Server Documentation](https://github.com/middleapi/standardserver#resolving-body). ::: -### Event Iterator (AsyncIteratorObject) +### AsyncIteratorObject -When the output is an event iterator (`AsyncIteratorObject`), it is sent as a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. Each event contains one serialized chunk of data. +When the output is an `AsyncIteratorObject`, it is sent as a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. Each event contains one serialized chunk of data. ```http HTTP/1.1 200 OK diff --git a/apps/content/learn-and-contribute/mini-orpc/beyond-the-basics.md b/apps/content/learn-and-contribute/mini-orpc/beyond-the-basics.md index 029a6852f..e6c125b3a 100644 --- a/apps/content/learn-and-contribute/mini-orpc/beyond-the-basics.md +++ b/apps/content/learn-and-contribute/mini-orpc/beyond-the-basics.md @@ -29,7 +29,7 @@ You can implement these features in any order. Pick the ones you find interestin - [ ] [RPC Protocol](/docs/advanced/rpc-protocol) Implementation ([reference](https://github.com/middleapi/orpc/blob/main/packages/client/src/adapters/standard/rpc-serializer.ts)) - [ ] Support native types like `Date`, `Map`, `Set`, etc. - [ ] Support `File`/`Blob` types - - [ ] Support [Event Iterator](/docs/event-iterator) types + - [ ] Support [AsyncIteratorObject](/docs/async-iterator-object) types - [ ] Multi-runtime support - [ ] Standard Server Concept ([reference](https://github.com/middleapi/orpc/tree/main/packages/standard-server)) diff --git a/package.json b/package.json index 56f251fa4..795968565 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "2.0.0-beta.14", "private": true, - "packageManager": "pnpm@11.9.0", + "packageManager": "pnpm@11.10.0", "scripts": { "prepare": "simple-git-hooks", "type:check": "pnpm run -r type:check && tsc", @@ -36,10 +36,10 @@ "@orpc/tanstack-query": "workspace:*", "@orpc/valibot": "workspace:*", "@orpc/zod": "workspace:*", - "@standardserver/core": "^0.0.25", - "@standardserver/fetch": "^0.0.25", - "@standardserver/peer": "^0.0.25", - "@standardserver/shared": "^0.0.25", + "@standardserver/core": "^0.0.32", + "@standardserver/fetch": "^0.0.32", + "@standardserver/peer": "^0.0.32", + "@standardserver/shared": "^0.0.32", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@types/node": "^26.0.1", diff --git a/packages/bun/package.json b/packages/bun/package.json index f3ee6bda8..c4f8faf45 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -39,7 +39,7 @@ "@orpc/ratelimit": "workspace:*", "@orpc/server": "workspace:*", "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25" + "@standardserver/core": "^0.0.32" }, "devDependencies": { "@types/bun": "^1.3.14", diff --git a/packages/bun/tests/rpc/cancel-and-abort.test.ts b/packages/bun/tests/rpc/cancel-and-abort.test.ts index c33201317..a127c5d58 100644 --- a/packages/bun/tests/rpc/cancel-and-abort.test.ts +++ b/packages/bun/tests/rpc/cancel-and-abort.test.ts @@ -35,7 +35,7 @@ describe.each([ }) // TODO: https://github.com/oven-sh/bun/issues/33227 - it.skipIf(adapter === 'bun-fetch')('server should cancel event iterator response and abort request when client cancels', async () => { + it.skipIf(adapter === 'bun-fetch')('server should cancel AsyncIteratorObject response and abort request when client cancels', async () => { const cancel = vi.fn() handler.mockResolvedValueOnce(new AsyncIteratorClass( async () => { diff --git a/packages/bun/tests/rpc/data-transfer.test.ts b/packages/bun/tests/rpc/data-transfer.test.ts index a9728352b..2041a8b15 100644 --- a/packages/bun/tests/rpc/data-transfer.test.ts +++ b/packages/bun/tests/rpc/data-transfer.test.ts @@ -88,7 +88,7 @@ describe.each([ // TODO: There an issues with Bun Websocket Server, when multiple messages sent simultaneously // We might need to report this issue - it.skipIf(adapter === 'bun-websocket')('support event iterator and transfer event iterator in parallel', async () => { + it.skipIf(adapter === 'bun-websocket')('support AsyncIteratorObject and transfer AsyncIteratorObject in parallel', async () => { const stream = (async function* () { yield 'order 1' await sleep(200) diff --git a/packages/client/package.json b/packages/client/package.json index 8c6488348..dd61ff0f0 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -66,9 +66,9 @@ }, "dependencies": { "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25", - "@standardserver/fetch": "^0.0.25", - "@standardserver/peer": "^0.0.25" + "@standardserver/core": "^0.0.32", + "@standardserver/fetch": "^0.0.32", + "@standardserver/peer": "^0.0.32" }, "devDependencies": { "zod": "^4.4.3" diff --git a/packages/client/src/adapters/fetch/transport.ts b/packages/client/src/adapters/fetch/transport.ts index ca07f7fca..a5eb490a4 100644 --- a/packages/client/src/adapters/fetch/transport.ts +++ b/packages/client/src/adapters/fetch/transport.ts @@ -25,7 +25,7 @@ export interface FetchLinkTransportOptions { origin?: Value, [options: ClientOptions, path: string[]]> /** - * Options for how to convert the Standard Request to a Fetch Request, like event iterator options, etc. + * Options for how to convert the Standard Request to a Fetch Request, like event stream options, etc. */ toFetchRequest?: undefined | ToFetchBodyOptions diff --git a/packages/client/src/adapters/standard/link.test.ts b/packages/client/src/adapters/standard/link.test.ts index 626909d03..445079ebb 100644 --- a/packages/client/src/adapters/standard/link.test.ts +++ b/packages/client/src/adapters/standard/link.test.ts @@ -128,7 +128,7 @@ describe('standardLink', () => { await expect(link.call(['test'], 'input', { context: {} })).rejects.toThrow(error) }) - it('traces input & output event iterator', async () => { + it('traces input & output AsyncIteratorObject', async () => { const codec = makeCodec() const transport = makeTransport() const link = new StandardLink(codec, transport) diff --git a/packages/client/src/adapters/standard/link.ts b/packages/client/src/adapters/standard/link.ts index 73906a56b..cf1a9e7ea 100644 --- a/packages/client/src/adapters/standard/link.ts +++ b/packages/client/src/adapters/standard/link.ts @@ -64,9 +64,9 @@ export class StandardLink implements ClientLink { if (isAsyncIteratorObject(input)) { /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for AsyncIteratorObject to remain other special properties */ - input = override(input, traceAsyncIterator('consume_event_iterator_input', input)) + input = override(input, traceAsyncIterator('consume_async_iterator_object_input', input)) } return intercept(this.interceptors, { ...options, path, input }, async ({ path, input, ...options }) => { @@ -129,9 +129,9 @@ export class StandardLink implements ClientLink { * Do not use otelContext here, as it is a lazy span. * * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for AsyncIteratorObject to remain other special properties */ - return override(output, traceAsyncIterator('consume_event_iterator_output', output)) + return override(output, traceAsyncIterator('consume_async_iterator_object_output', output)) } return output diff --git a/packages/client/src/adapters/standard/rpc-link-codec.test.ts b/packages/client/src/adapters/standard/rpc-link-codec.test.ts index 17a580105..0dbfe3972 100644 --- a/packages/client/src/adapters/standard/rpc-link-codec.test.ts +++ b/packages/client/src/adapters/standard/rpc-link-codec.test.ts @@ -96,7 +96,7 @@ describe('rpcLinkCodec', () => { }], ['Blob', () => new Blob(['data'])], ['ReadableStream', () => new ReadableStream()], - ['async iterator', () => (async function* () { yield 1 })()], + ['AsyncIteratorObject', () => (async function* () { yield 1 })()], ] as const)('falls back to POST when GET with %s', async (_, factory) => { const codec = new RPCLinkCodec({ url: '/api', method: 'GET', serializer }) const value = factory() diff --git a/packages/client/src/event-iterator.test.ts b/packages/client/src/async-iterator-object.test.ts similarity index 87% rename from packages/client/src/event-iterator.test.ts rename to packages/client/src/async-iterator-object.test.ts index 0f1eaf820..c25b2f194 100644 --- a/packages/client/src/event-iterator.test.ts +++ b/packages/client/src/async-iterator-object.test.ts @@ -1,7 +1,7 @@ import { getEventMeta, withEventMeta } from '@standardserver/core' -import { wrapEventIteratorPreservingMeta } from './event-iterator' +import { wrapAsyncIteratorPreservingEventMeta } from './async-iterator-object' -describe('wrapEventIteratorPreservingMeta', () => { +describe('wrapAsyncIteratorPreservingEventMeta', () => { it('preserves metadata when mapping yielded and returned values', async () => { const event = withEventMeta({ order: 2 }, { id: 'id-2' }) const returned = withEventMeta({ order: 3 }, { retry: 4000 }) @@ -19,7 +19,7 @@ describe('wrapEventIteratorPreservingMeta', () => { } }) - const mapped = wrapEventIteratorPreservingMeta(iterator, { + const mapped = wrapAsyncIteratorPreservingEventMeta(iterator, { mapResult, }) @@ -51,7 +51,7 @@ describe('wrapEventIteratorPreservingMeta', () => { const mapResult = vi.fn(async result => result) - const mapped = wrapEventIteratorPreservingMeta(iterator, { + const mapped = wrapAsyncIteratorPreservingEventMeta(iterator, { mapResult, }) @@ -75,7 +75,7 @@ describe('wrapEventIteratorPreservingMeta', () => { throw error })() - const mapped = wrapEventIteratorPreservingMeta(iterator, { + const mapped = wrapAsyncIteratorPreservingEventMeta(iterator, { onError, mapError, }) @@ -96,7 +96,7 @@ describe('wrapEventIteratorPreservingMeta', () => { it('does not reattach metadata when mapped errors stay the same or become non-objects', async () => { const error = withEventMeta(new Error('TEST'), { id: 'error-1' }) - const sameError = wrapEventIteratorPreservingMeta((async function* () { + const sameError = wrapAsyncIteratorPreservingEventMeta((async function* () { throw error })(), { mapError: async cause => cause, @@ -104,7 +104,7 @@ describe('wrapEventIteratorPreservingMeta', () => { await expect(sameError.next()).rejects.toBe(error) - const primitiveError = wrapEventIteratorPreservingMeta((async function* () { + const primitiveError = wrapAsyncIteratorPreservingEventMeta((async function* () { throw error })(), { mapError: async () => 'mapped-error', diff --git a/packages/client/src/event-iterator.ts b/packages/client/src/async-iterator-object.ts similarity index 91% rename from packages/client/src/event-iterator.ts rename to packages/client/src/async-iterator-object.ts index b066e1859..38e316976 100644 --- a/packages/client/src/event-iterator.ts +++ b/packages/client/src/async-iterator-object.ts @@ -2,7 +2,7 @@ import type { AsyncIteratorClass, WrapAsyncIteratorOptions } from '@orpc/shared' import { isTypescriptObject, wrapAsyncIterator } from '@orpc/shared' import { getEventMeta, withEventMeta } from '@standardserver/core' -export function wrapEventIteratorPreservingMeta( +export function wrapAsyncIteratorPreservingEventMeta( iterator: AsyncIterator, { mapResult, mapError, ...rest }: WrapAsyncIteratorOptions, ): AsyncIteratorClass { diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index dc88b8012..3b4631e6c 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,12 +1,16 @@ -import { isInferableError } from './error-utils' - +export * from './async-iterator-object' export * from './client' export * from './client-safe' export * from './consts' export * from './dynamic-link' export * from './error' +export { + /** + * @deprecated Use `isInferableError` instead. + */ + isInferableError as isDefinedError, +} from './error-utils' export * from './error-utils' -export * from './event-iterator' export * from './rpc-json-serializer' export * from './rpc-serializer' export * from './types' @@ -23,13 +27,30 @@ export type { export { AsyncIteratorClass, + asyncIteratorToStream, + asyncIteratorToUnproxiedDataStream, + consumeAsyncIterator, + /** + * @deprecated Use `consumeAsyncIterator` instead. + */ + consumeAsyncIterator as consumeEventIterator, + /** + * @deprecated Use `asyncIteratorToStream` instead. + */ asyncIteratorToStream as eventIteratorToStream, + /** + * @deprecated Use `asyncIteratorToUnproxiedDataStream` instead. + */ asyncIteratorToUnproxiedDataStream as eventIteratorToUnproxiedDataStream, onError, onFinish, onStart, onSuccess, - streamToAsyncIteratorClass as streamToEventIterator, + streamToAsyncIteratorObject, + /** + * @deprecated Use `streamToAsyncIteratorObject` instead. + */ + streamToAsyncIteratorObject as streamToEventIterator, } from '@orpc/shared' export type { @@ -42,8 +63,3 @@ export { unwrapEvent, withEventMeta, } from '@standardserver/core' - -/** - * @deprecated Use `isInferableError` instead. - */ -export const isDefinedError = isInferableError diff --git a/packages/client/src/plugins/batch.test.ts b/packages/client/src/plugins/batch.test.ts index d50b42266..2e0e8b75a 100644 --- a/packages/client/src/plugins/batch.test.ts +++ b/packages/client/src/plugins/batch.test.ts @@ -216,7 +216,7 @@ describe('batchLinkPlugin', () => { expect(transport.send).toHaveBeenCalledTimes(2) }) - it('skips batching for requests with async iterator body', async () => { + it('skips batching for requests with AsyncIteratorObject body', async () => { const codec = makeCodec() const transport = makeTransport() diff --git a/packages/client/src/plugins/dedupe.test.ts b/packages/client/src/plugins/dedupe.test.ts index a8b36e4d6..b334e0886 100644 --- a/packages/client/src/plugins/dedupe.test.ts +++ b/packages/client/src/plugins/dedupe.test.ts @@ -180,7 +180,7 @@ describe('dedupeLinkPlugin', () => { expect((callOptions as any).next).toEqual(expect.any(Function)) }) - it('replicates async iterator response bodies for deduped requests', async () => { + it('replicates AsyncIteratorObject response bodies for deduped requests', async () => { const codec = makeCodec() const iteratorFactory = vi.fn(async function* () { yield 'first' diff --git a/packages/client/src/plugins/retry.test.ts b/packages/client/src/plugins/retry.test.ts index 676e52447..1f9ed282e 100644 --- a/packages/client/src/plugins/retry.test.ts +++ b/packages/client/src/plugins/retry.test.ts @@ -3,7 +3,7 @@ import type { StandardLinkCodec, StandardLinkTransport } from '../adapters/stand import type { RetryLinkPluginContext } from './retry' import { withEventMeta } from '@standardserver/core' import { StandardLink } from '../adapters/standard' -import { RetryLinkPlugin, RetryLinkPluginInvalidEventIteratorRetryResponse } from './retry' +import { RetryLinkPlugin } from './retry' interface TestContext extends RetryLinkPluginContext { tag?: string @@ -210,8 +210,8 @@ describe('retryLinkPlugin', () => { expect(clean).toHaveBeenCalledWith(false) }) - describe('event iterator', () => { - it('retries event iterator and forwards lastEventId from metadata', async () => { + describe('asyncIteratorObject', () => { + it('retries AsyncIteratorObject and forwards lastEventId from metadata', async () => { const codec = makeCodec() const transport = makeTransport() @@ -255,7 +255,7 @@ describe('retryLinkPlugin', () => { expect(shouldRetry).toHaveBeenCalledWith(expect.objectContaining({ lastEventRetry: 0 })) }) - it('throws when retry response is not an event iterator', async () => { + it('throws when retry response is not an AsyncIteratorObject', async () => { const codec = makeCodec() const transport = makeTransport() @@ -281,7 +281,7 @@ describe('retryLinkPlugin', () => { const iterator = await link.call(['planet', 'create'], { name: 'Earth' }, { context: { retry: 1, retryDelay: 0 } }) as AsyncIterator - await expect(iterator.next()).rejects.toBeInstanceOf(RetryLinkPluginInvalidEventIteratorRetryResponse) + await expect(iterator.next()).rejects.toBeInstanceOf(TypeError) }) it('support manually cleanup', async () => { @@ -379,7 +379,7 @@ describe('retryLinkPlugin', () => { expect(retriedReturn).toHaveBeenCalledTimes(1) }) - it('reset special event iterator properties after retry', async () => { + it('reset special AsyncIteratorObject properties after retry', async () => { const codec = makeCodec() const transport = makeTransport() diff --git a/packages/client/src/plugins/retry.ts b/packages/client/src/plugins/retry.ts index 8360a4ab4..9b40792aa 100644 --- a/packages/client/src/plugins/retry.ts +++ b/packages/client/src/plugins/retry.ts @@ -24,7 +24,7 @@ export interface RetryLinkPluginAttemptOptions export interface RetryLinkPluginContext { /** * Maximum retry attempts before throwing. - * Use `Number.POSITIVE_INFINITY` for infinite retries (e.g. for event iterators). + * Use `Number.POSITIVE_INFINITY` for infinite retries (e.g. for AsyncIteratorObject). * * @default 0 */ @@ -58,8 +58,6 @@ export interface RetryLinkPluginOptions<_T extends RetryLinkPluginContext> { default?: RetryLinkPluginContext | undefined } -export class RetryLinkPluginInvalidEventIteratorRetryResponse extends Error { } - export class RetryLinkPlugin implements StandardLinkPlugin { private readonly defaultRetry: Exclude private readonly defaultRetryDelay: Exclude @@ -185,14 +183,14 @@ export class RetryLinkPlugin i lastEventId = meta?.id ?? lastEventId lastEventRetry = meta?.retry ?? lastEventRetry - const maybeEventIterator = await callNext({ error }) - if (!isAsyncIteratorObject(maybeEventIterator)) { - throw new RetryLinkPluginInvalidEventIteratorRetryResponse( - 'RetryLinkPlugin: Expected an Event Iterator, got a non-Event Iterator', + const asyncIteratorObject = await callNext({ error }) + if (!isAsyncIteratorObject(asyncIteratorObject)) { + throw new TypeError( + 'RetryLinkPlugin: Expected an AsyncIteratorObject, got a different type.', ) } - current = maybeEventIterator + current = asyncIteratorObject if (isIteratorAborted) { await current.return?.() diff --git a/packages/client/src/rpc-serializer.test.ts b/packages/client/src/rpc-serializer.test.ts index 30be2538f..b52d23e9e 100644 --- a/packages/client/src/rpc-serializer.test.ts +++ b/packages/client/src/rpc-serializer.test.ts @@ -61,7 +61,7 @@ describe('rpcSerializer', () => { }) }) - describe('event iterator', async () => { + describe('asyncIteratorObject', async () => { const serializer = new RPCSerializer() function serializeAndDeserialize(value: unknown): unknown { diff --git a/packages/client/src/rpc-serializer.ts b/packages/client/src/rpc-serializer.ts index 4b6ea015a..4dd8c30b4 100644 --- a/packages/client/src/rpc-serializer.ts +++ b/packages/client/src/rpc-serializer.ts @@ -2,8 +2,8 @@ import type { StandardBody } from '@standardserver/core' import type { RPCJsonSerializerOptions } from './rpc-json-serializer' import { isAsyncIteratorObject, stringifyJSON } from '@orpc/shared' import { ErrorEvent } from '@standardserver/core' +import { wrapAsyncIteratorPreservingEventMeta } from './async-iterator-object' import { createORPCErrorFromJson, isORPCErrorJson, toORPCError } from './error-utils' -import { wrapEventIteratorPreservingMeta } from './event-iterator' import { RPCJsonSerializer } from './rpc-json-serializer' export interface RPCSerializerSerializeOptions { @@ -41,7 +41,7 @@ export class RPCSerializer { } if (isAsyncIteratorObject(data)) { - return wrapEventIteratorPreservingMeta(data, { + return wrapAsyncIteratorPreservingEventMeta(data, { mapResult: (result) => { // standard event stream data already supports these types without additional serialization. if (result.value === undefined) { @@ -86,7 +86,7 @@ export class RPCSerializer { } if (isAsyncIteratorObject(data)) { - return wrapEventIteratorPreservingMeta(data, { + return wrapAsyncIteratorPreservingEventMeta(data, { mapResult: (result) => { if (result.value === undefined) { return result diff --git a/packages/client/src/utils.test-d.ts b/packages/client/src/utils.test-d.ts index 02c33ec92..f1d805896 100644 --- a/packages/client/src/utils.test-d.ts +++ b/packages/client/src/utils.test-d.ts @@ -1,8 +1,7 @@ -import type { PromiseWithError } from '@orpc/shared' import type { ORPCError } from './error' import type { Client, ClientContext } from './types' import { isInferableError } from './error-utils' -import { consumeEventIterator, safe } from './utils' +import { safe } from './utils' describe('safe', async () => { const client = {} as Client> @@ -72,53 +71,3 @@ describe('safe', async () => { expectTypeOf(data).toEqualTypeOf() }) }) - -describe('consumeEventIterator', () => { - it('can infer types from PromiseWithError + AsyncGenerator', () => { - void consumeEventIterator({} as PromiseWithError, 'error-value'>, { - onEvent: (message) => { - expectTypeOf(message).toEqualTypeOf<'message-value'>() - }, - onError: (error) => { - expectTypeOf(error).toEqualTypeOf<'error-value'>() - }, - onSuccess: (value) => { - expectTypeOf(value).toEqualTypeOf<'done-value' | undefined>() - }, - onFinish: ([error, data, isSuccess]) => { - if (!error || isSuccess) { - expectTypeOf(error).toEqualTypeOf() - expectTypeOf(data).toEqualTypeOf<'done-value' | undefined>() - } - else { - expectTypeOf(error).toEqualTypeOf<'error-value'>() - expectTypeOf(data).toEqualTypeOf() - } - }, - }) - }) - - it('can infer types from AsyncIterator', () => { - void consumeEventIterator({} as AsyncIterator<'message-value', 'done-value'>, { - onEvent: (message) => { - expectTypeOf(message).toEqualTypeOf<'message-value'>() - }, - onError: (error) => { - expectTypeOf(error).toEqualTypeOf() - }, - onSuccess: (value) => { - expectTypeOf(value).toEqualTypeOf<'done-value' | undefined>() - }, - onFinish: ([error, data, isSuccess]) => { - if (!error || isSuccess) { - expectTypeOf(error).toEqualTypeOf() - expectTypeOf(data).toEqualTypeOf<'done-value' | undefined>() - } - else { - expectTypeOf(error).toEqualTypeOf() - expectTypeOf(data).toEqualTypeOf() - } - }, - }) - }) -}) diff --git a/packages/client/src/utils.test.ts b/packages/client/src/utils.test.ts index ecca9a61c..52ca95036 100644 --- a/packages/client/src/utils.test.ts +++ b/packages/client/src/utils.test.ts @@ -1,5 +1,5 @@ import { ORPCError } from './error' -import { consumeEventIterator, resolveClientRest, resolveFriendlyClientOptions, safe } from './utils' +import { resolveClientRest, resolveFriendlyClientOptions, safe } from './utils' describe('resolveFriendlyClientOptions', () => { it('works', () => { @@ -39,217 +39,3 @@ it('safe', async () => { expect([...r4]).toEqual([e4, undefined, null, false]) expect({ ...r4 }).toEqual(expect.objectContaining({ error: e4, data: undefined, inferableError: null, isSuccess: false })) }) - -describe('consumeEventIterator', () => { - it('on success', async () => { - const iterator = (async function* () { - yield 1 - yield 2 - return 3 - }()) - - const onEvent = vi.fn() - const onError = vi.fn() - const onSuccess = vi.fn() - const onFinish = vi.fn() - - void consumeEventIterator(iterator, { - onEvent, - onError, - onSuccess, - onFinish, - }) - - await vi.waitFor(() => { - expect(onEvent).toHaveBeenCalledTimes(2) - expect(onEvent).toHaveBeenNthCalledWith(1, 1) - expect(onEvent).toHaveBeenNthCalledWith(2, 2) - - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSuccess).toHaveBeenNthCalledWith(1, 3) - expect(onFinish).toHaveBeenCalledTimes(1) - expect(onFinish).toHaveBeenNthCalledWith(1, [null, 3, true]) - - expect(onError).toHaveBeenCalledTimes(0) - }) - }) - - it('on error', async () => { - const error = new Error('TEST') - const iterator = (async function* () { - yield 1 - yield 2 - throw error - }()) - - const onEvent = vi.fn() - const onError = vi.fn() - const onSuccess = vi.fn() - const onFinish = vi.fn() - - void consumeEventIterator(iterator, { - onEvent, - onError, - onSuccess, - onFinish, - }) - - await vi.waitFor(() => { - expect(onEvent).toHaveBeenCalledTimes(2) - expect(onEvent).toHaveBeenNthCalledWith(1, 1) - expect(onEvent).toHaveBeenNthCalledWith(2, 2) - - expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenNthCalledWith(1, error) - - expect(onFinish).toHaveBeenCalledTimes(1) - expect(onFinish).toHaveBeenNthCalledWith(1, [error, undefined, false]) - - expect(onSuccess).toHaveBeenCalledTimes(0) - }) - }) - - it('on error without onError and onFinish', async ({ onTestFinished }) => { - const unhandledRejectionHandler = vi.fn() - process.on('unhandledRejection', unhandledRejectionHandler) - onTestFinished(() => { - process.off('unhandledRejection', unhandledRejectionHandler) - }) - - const error = new Error('TEST') - const iterator = (async function* () { - yield 1 - yield 2 - throw error - }()) - - const onEvent = vi.fn() - - void consumeEventIterator(iterator, { - onEvent, - }) - - await vi.waitFor(() => { - expect(onEvent).toHaveBeenCalledTimes(2) - expect(onEvent).toHaveBeenNthCalledWith(1, 1) - expect(onEvent).toHaveBeenNthCalledWith(2, 2) - }) - - expect(unhandledRejectionHandler).toHaveBeenCalledTimes(1) - expect(unhandledRejectionHandler).toHaveBeenNthCalledWith(1, error, expect.anything()) - }) - - it('unsubscribe', async () => { - let cleanup = false - const iterator = (async function* () { - try { - await new Promise(resolve => setTimeout(resolve, 10)) - yield 1 - yield 2 - return 3 - } - finally { - cleanup = true - } - }()) - - const onEvent = vi.fn() - const onSuccess = vi.fn() - const onFinish = vi.fn() - const onError = vi.fn() - - const unsubscribe = consumeEventIterator(iterator, { - onEvent, - onError, - onSuccess, - onFinish, - }) - - await new Promise(resolve => setTimeout(resolve, 1)) - await unsubscribe() - expect(cleanup).toBe(true) - // side-effect of async generator - waiting for .next resolve before .return effect - expect(onEvent).toHaveBeenCalledTimes(1) - expect(onEvent).toHaveBeenNthCalledWith(1, 1) - - expect(onError).toHaveBeenCalledTimes(0) - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFinish).toHaveBeenCalledTimes(1) - // undefined can be passed on success because iterator can be canceled - expect(onSuccess).toHaveBeenNthCalledWith(1, undefined) - expect(onFinish).toHaveBeenNthCalledWith(1, [null, undefined, true]) - }) - - it('error on unsubscribe', async () => { - const error = new Error('TEST') - let cleanup = false - const iterator = (async function* () { - try { - await new Promise(resolve => setTimeout(resolve, 10)) - yield 1 - yield 2 - return 3 - } - finally { - cleanup = true - // eslint-disable-next-line no-unsafe-finally - throw error - } - }()) - - const onEvent = vi.fn() - const onError = vi.fn() - const onSuccess = vi.fn() - const onFinish = vi.fn() - - const unsubscribe = consumeEventIterator(iterator, { - onEvent, - onError, - onSuccess, - onFinish, - }) - - await new Promise(resolve => setTimeout(resolve, 1)) - await expect(unsubscribe()).rejects.toThrow(error) - expect(cleanup).toBe(true) - // side-effect of async generator - waiting for .next resolve before .return effect - expect(onEvent).toHaveBeenCalledTimes(1) - expect(onEvent).toHaveBeenNthCalledWith(1, 1) - - expect(onError).toHaveBeenCalledTimes(0) - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFinish).toHaveBeenCalledTimes(1) - // undefined can be passed on success because iterator can be canceled - expect(onSuccess).toHaveBeenNthCalledWith(1, undefined) - expect(onFinish).toHaveBeenNthCalledWith(1, [null, undefined, true]) - }) - - it('on iterator promise rejection', async () => { - const error = new Error('TEST') - const iterator = Promise.reject(error) - - const onEvent = vi.fn() - const onError = vi.fn() - const onSuccess = vi.fn() - const onFinish = vi.fn() - - void consumeEventIterator(iterator, { - onEvent, - onError, - onSuccess, - onFinish, - }) - - await vi.waitFor(() => { - expect(onEvent).toHaveBeenCalledTimes(0) - - expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenNthCalledWith(1, error) - - expect(onFinish).toHaveBeenCalledTimes(1) - expect(onFinish).toHaveBeenNthCalledWith(1, [error, undefined, false]) - - expect(onSuccess).toHaveBeenCalledTimes(0) - }) - }) -}) diff --git a/packages/client/src/utils.ts b/packages/client/src/utils.ts index dd3ca6ee6..828cd5938 100644 --- a/packages/client/src/utils.ts +++ b/packages/client/src/utils.ts @@ -61,79 +61,3 @@ export async function safe(promise: PromiseWit ) } } - -export interface ConsumeEventIteratorOptions { - /** - * Called on each event - */ - onEvent: (event: T) => void - /** - * Called once error happens - */ - onError?: (error: TError) => void - /** - * Called once event iterator is done - * - * @info If iterator is canceled, `undefined` can be passed on success - */ - onSuccess?: (value: TReturn | undefined) => void - /** - * Called once after onError or onSuccess - * - * @info If iterator is canceled, `undefined` can be passed on success - */ - onFinish?: (state: [error: TError, data: undefined, isSuccess: false] | [error: null, data: TReturn | undefined, isSuccess: true]) => void -} - -/** - * Consumes an event iterator with lifecycle callbacks - * - * @warning If no `onError` or `onFinish` is provided, error will be thrown into unhandled rejection channel. - * @return unsubscribe callback - */ -export function consumeEventIterator( - iterator: AsyncIterator | PromiseWithError, TError>, - options: ConsumeEventIteratorOptions, -): () => Promise { - void (async () => { - let onFinishState: [error: TError, data: undefined, isSuccess: false] | [error: null, data: TReturn | undefined, isSuccess: true] - - try { - const resolvedIterator = await iterator - - while (true) { - const { done, value } = await resolvedIterator.next() - - if (done) { - // if iterator is canceled, value can be undefined - const realValue = value as typeof value | undefined - onFinishState = [null, realValue, true] - options.onSuccess?.(realValue) - break - } - - options.onEvent(value) - } - } - catch (error) { - onFinishState = [error as TError, undefined, false] - - /** - * If no `onError` or `onFinish` is provided, unhandled rejections will be thrown - * This is best practice for error handling - error should not be silently ignored - */ - if (!options.onError && !options.onFinish) { - throw error - } - - options.onError?.(error as TError) - } - finally { - options.onFinish?.(onFinishState!) - } - })() - - return async () => { - await (await iterator)?.return?.() - } -} diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 30c146315..cde201f63 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -42,7 +42,7 @@ "@orpc/publisher": "workspace:*", "@orpc/ratelimit": "workspace:*", "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25" + "@standardserver/core": "^0.0.32" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.16.20", diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index 2b79a179a..da070b7aa 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -3,7 +3,6 @@ export * from './builder-variants' export * from './caller' export * from './error' export * from './error-utils' -export * from './event-iterator' export * from './meta' export * from './meta-built-in' export * from './meta-utils' @@ -13,6 +12,13 @@ export * from './router' export * from './router-client' export * from './router-utils' export * from './schema' +export * from './schema-built-in' +export { + /** + * @deprecated Use `asyncIteratorObject` instead. + */ + asyncIteratorObject as eventIterator, +} from './schema-built-in' export * from './schema-utils' export type { diff --git a/packages/contract/src/event-iterator.test.ts b/packages/contract/src/schema-built-in.test.ts similarity index 72% rename from packages/contract/src/event-iterator.test.ts rename to packages/contract/src/schema-built-in.test.ts index 953b7ae6d..9dee0b4ba 100644 --- a/packages/contract/src/event-iterator.test.ts +++ b/packages/contract/src/schema-built-in.test.ts @@ -3,7 +3,7 @@ import { ORPCError } from '@orpc/client' import { getEventMeta, withEventMeta } from '@standardserver/core' import * as z from 'zod' import { ValidationError } from './error' -import { eventIterator, getEventIteratorSchemaDetails } from './event-iterator' +import { asyncIteratorObject, getAsyncIteratorObjectSchemaDetails } from './schema-built-in' const ORDER_SCHEMA = z.object({ order: z.number() }) @@ -13,15 +13,15 @@ function assertValidationSuccess(result: T): ass } } -describe('eventIterator', () => { - it('expect a async iterator object', async () => { - const schema = eventIterator(ORDER_SCHEMA) +describe('asyncIteratorObject', () => { + it('expect a AsyncIteratorObject', async () => { + const schema = asyncIteratorObject(ORDER_SCHEMA) const result = await schema['~standard'].validate(123) expect(result.issues).toHaveLength(1) }) it('can validate yields and preserve meta', async () => { - const schema = eventIterator(ORDER_SCHEMA) + const schema = asyncIteratorObject(ORDER_SCHEMA) const result = await schema['~standard'].validate((async function* () { yield { order: 1 } @@ -43,11 +43,11 @@ describe('eventIterator', () => { try { await result.value.next() - throw new Error('Expected event iterator validation to fail') + throw new Error('Expected AsyncIteratorObject validation to fail') } catch (error) { expect(error).toBeInstanceOf(ORPCError) - expect((error as AnyORPCError).code).toEqual('EVENT_ITERATOR_VALIDATION_FAILED') + expect((error as AnyORPCError).code).toEqual('ASYNC_ITERATOR_OBJECT_VALIDATION_FAILED') expect((error as AnyORPCError).cause).toBeInstanceOf(ValidationError) expect(((error as AnyORPCError).cause as ValidationError).issues).toHaveLength(1) expect(((error as AnyORPCError).cause as ValidationError).invalidData).toEqual({ order: '3' }) @@ -55,7 +55,7 @@ describe('eventIterator', () => { }) it('can validate returns and preserve meta', async () => { - const schema = eventIterator(ORDER_SCHEMA, ORDER_SCHEMA) + const schema = asyncIteratorObject(ORDER_SCHEMA, ORDER_SCHEMA) const result = await schema['~standard'].validate((async function* () { return { order: 1 } @@ -70,7 +70,7 @@ describe('eventIterator', () => { }) it('not required returns schema', async () => { - const schema = eventIterator(ORDER_SCHEMA) + const schema = asyncIteratorObject(ORDER_SCHEMA) const result = await schema['~standard'].validate((async function* () { return 'anything' @@ -83,7 +83,7 @@ describe('eventIterator', () => { it('cleanup origin when validation fails', async () => { let cleanupCalled = false - const schema = eventIterator(ORDER_SCHEMA) + const schema = asyncIteratorObject(ORDER_SCHEMA) const result = await schema['~standard'].validate((async function* () { try { @@ -99,17 +99,17 @@ describe('eventIterator', () => { assertValidationSuccess(result) await expect(result.value.next()).resolves.toEqual({ done: false, value: { order: 1 } }) - await expect(result.value.next()).rejects.toThrow('Event iterator validation failed') + await expect(result.value.next()).rejects.toThrow('AsyncIteratorObject validation failed') expect(cleanupCalled).toBe(true) }) }) -it('getEventIteratorSchemaDetails', async () => { +it('getAsyncIteratorObjectSchemaDetails', async () => { const yieldSchema = ORDER_SCHEMA const returnSchema = ORDER_SCHEMA - const schema = eventIterator(yieldSchema, returnSchema) + const schema = asyncIteratorObject(yieldSchema, returnSchema) - expect(getEventIteratorSchemaDetails(schema)).toEqual({ yieldSchema, returnSchema }) - expect(getEventIteratorSchemaDetails(undefined)).toBeUndefined() - expect(getEventIteratorSchemaDetails(z.object({}))).toBeUndefined() + expect(getAsyncIteratorObjectSchemaDetails(schema)).toEqual({ yieldSchema, returnSchema }) + expect(getAsyncIteratorObjectSchemaDetails(undefined)).toBeUndefined() + expect(getAsyncIteratorObjectSchemaDetails(z.object({}))).toBeUndefined() }) diff --git a/packages/contract/src/event-iterator.ts b/packages/contract/src/schema-built-in.ts similarity index 54% rename from packages/contract/src/event-iterator.ts rename to packages/contract/src/schema-built-in.ts index 0641c9c72..5a641a259 100644 --- a/packages/contract/src/event-iterator.ts +++ b/packages/contract/src/schema-built-in.ts @@ -1,36 +1,34 @@ import type { AsyncIteratorClass } from '@orpc/shared' import type { AnySchema, Schema } from './schema' -import { ORPCError, wrapEventIteratorPreservingMeta } from '@orpc/client' +import { ORPCError, wrapAsyncIteratorPreservingEventMeta } from '@orpc/client' import { isAsyncIteratorObject, ORPC_NAME } from '@orpc/shared' import { ValidationError } from './error' -const EVENT_ITERATOR_SCHEMA_DETAILS_SYMBOL = Symbol.for('ORPC_EVENT_ITERATOR_SCHEMA_DETAILS') +const ASYNC_ITERATOR_OBJECT_SCHEMA_DETAILS_SYMBOL = Symbol.for('ORPC_ASYNC_ITERATOR_OBJECT_SCHEMA_DETAILS') -export interface EventIteratorSchemaDetails { +export interface AsyncIteratorObjectSchemaDetails { yieldSchema: AnySchema returnSchema?: AnySchema } /** - * Define schema for an event iterator. - * - * @see {@link https://orpc.dev/docs/event-iterator#validate-event-iterator Validate Event Iterator Docs} + * Define schema for an AsyncIteratorObject. */ -export function eventIterator( +export function asyncIteratorObject( yieldSchema: Schema, returnSchema?: Schema, ): Schema, AsyncIteratorClass> { return { '~standard': { - [EVENT_ITERATOR_SCHEMA_DETAILS_SYMBOL as any]: { yieldSchema, returnSchema } satisfies EventIteratorSchemaDetails, + [ASYNC_ITERATOR_OBJECT_SCHEMA_DETAILS_SYMBOL as any]: { yieldSchema, returnSchema } satisfies AsyncIteratorObjectSchemaDetails, vendor: ORPC_NAME, version: 1, validate(iterator) { if (!isAsyncIteratorObject(iterator)) { - return { issues: [{ message: 'Expect event iterator', path: [] }] } + return { issues: [{ message: 'Expect AsyncIteratorObject', path: [] }] } } - const mapped = wrapEventIteratorPreservingMeta(iterator, { + const mapped = wrapAsyncIteratorPreservingEventMeta(iterator, { async mapResult(result) { const schema = result.done ? returnSchema : yieldSchema @@ -41,11 +39,11 @@ export function eventIterator { expect(finish).toHaveBeenCalledWith() }) - it('wraps matched async iterators and finishes after successful consumption', async () => { + it('wraps matched AsyncIteratorObject and finishes after successful consumption', async () => { const plugin = new EvlogHandlerPlugin() const { routing } = getPluginHooks(plugin) const { finish } = mockIntegration() @@ -270,7 +270,7 @@ describe('evlogHandlerPlugin', () => { expect(finish).toHaveBeenCalledWith({ status: 200 }) }) - it('logs async iterator wrapper errors as internal failures', async () => { + it('logs AsyncIteratorObject wrapper errors as internal failures', async () => { const plugin = new EvlogHandlerPlugin() const { routing } = getPluginHooks(plugin) const { finish, logger } = mockIntegration() @@ -449,7 +449,7 @@ describe('evlogHandlerPlugin', () => { })).resolves.toBe('pong') }) - it('logs client async iterator errors', async () => { + it('logs client AsyncIteratorObject errors', async () => { const logger = createLogger() const plugin = new EvlogHandlerPlugin() const { client } = getPluginHooks(plugin) diff --git a/packages/evlog/src/handler-plugin.ts b/packages/evlog/src/handler-plugin.ts index 5a1e51170..0303633cd 100644 --- a/packages/evlog/src/handler-plugin.ts +++ b/packages/evlog/src/handler-plugin.ts @@ -3,7 +3,7 @@ import type { StandardHandlerInterceptor, StandardHandlerOptions, StandardHandle import type { StandardRequest } from '@standardserver/core' import type { RequestLogger } from 'evlog' import type { BaseEvlogOptions, FrameworkIntegrationHelpers, FrameworkIntegrationSpec } from 'evlog/toolkit' -import { wrapEventIteratorPreservingMeta } from '@orpc/client' +import { wrapAsyncIteratorPreservingEventMeta } from '@orpc/client' import { isAbortError, isAsyncIteratorObject, ORPC_NAME, override, sleep, toArray, wrapReadableStream } from '@orpc/shared' import { flattenStandardHeader, parseStandardUrl } from '@standardserver/core' import { defineFrameworkIntegration } from 'evlog/toolkit' @@ -87,9 +87,9 @@ export class EvlogHandlerPlugin implements StandardHandlerPlu ...result.response, /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for AsyncIteratorObject to remain other special properties */ - body: override(result.response.body, wrapEventIteratorPreservingMeta(result.response.body, { + body: override(result.response.body, wrapAsyncIteratorPreservingEventMeta(result.response.body, { runWith, onError: (error) => { /** @@ -114,7 +114,7 @@ export class EvlogHandlerPlugin implements StandardHandlerPlu ...result.response, /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for ReadableStream to remain other special properties */ body: override(result.response.body, wrapReadableStream(result.response.body, { runWith, @@ -196,9 +196,9 @@ export class EvlogHandlerPlugin implements StandardHandlerPlu if (isAsyncIteratorObject(output)) { /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for AsyncIteratorObject to remain other special properties */ - return override(output, wrapEventIteratorPreservingMeta(output, { + return override(output, wrapAsyncIteratorPreservingEventMeta(output, { onError: (error) => { logBusinessLogicError(logger, error) }, @@ -208,7 +208,7 @@ export class EvlogHandlerPlugin implements StandardHandlerPlu if (output instanceof ReadableStream) { /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for ReadableStream to remain other special properties */ return override(output, wrapReadableStream(output, { onError: (error) => { diff --git a/packages/nest/package.json b/packages/nest/package.json index 4f3f5a36d..d0e5d6782 100644 --- a/packages/nest/package.json +++ b/packages/nest/package.json @@ -52,8 +52,8 @@ "@orpc/openapi": "workspace:*", "@orpc/server": "workspace:*", "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25", - "@standardserver/node": "^0.0.25" + "@standardserver/core": "^0.0.32", + "@standardserver/node": "^0.0.32" }, "devDependencies": { "@fastify/cookie": "^11.0.2", diff --git a/packages/nest/src/module.ts b/packages/nest/src/module.ts index fe2e25d11..2c41eb7c9 100644 --- a/packages/nest/src/module.ts +++ b/packages/nest/src/module.ts @@ -28,7 +28,7 @@ export type ORPCModuleConfig toNestStandardLazyRequest?: undefined | ((req: any, res: any) => NestStandardLazyRequest) /** - * Options for how to convert the Standard Response to a Nest Response (returning value), like event iterator options, etc. + * Options for how to convert the Standard Response to a Nest Response (returning value), like event stream options, etc. */ toNestResponse?: undefined | { /** diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 17a3695f7..129edd052 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -90,8 +90,8 @@ "@orpc/json-schema": "workspace:*", "@orpc/server": "workspace:*", "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25", - "@standardserver/fetch": "^0.0.25", + "@standardserver/core": "^0.0.32", + "@standardserver/fetch": "^0.0.32", "rou3": "^0.8.1" }, "devDependencies": { diff --git a/packages/openapi/src/adapters/standard/openapi-handler-codec.ts b/packages/openapi/src/adapters/standard/openapi-handler-codec.ts index 110cf1e7b..ff146c718 100644 --- a/packages/openapi/src/adapters/standard/openapi-handler-codec.ts +++ b/packages/openapi/src/adapters/standard/openapi-handler-codec.ts @@ -85,7 +85,7 @@ export class OpenAPIHandlerCodecCore { } } - // data can be Blob, Event Iterator, ReadableStream, ... + // data can be Blob, AsyncIteratorObject, ReadableStream, ... return data } diff --git a/packages/openapi/src/openapi-generator.test.ts b/packages/openapi/src/openapi-generator.test.ts index 247470ac9..57cdf44dd 100644 --- a/packages/openapi/src/openapi-generator.test.ts +++ b/packages/openapi/src/openapi-generator.test.ts @@ -1,5 +1,5 @@ import type { JsonSchemaConverter } from '@orpc/json-schema' -import { eventIterator, oc } from '@orpc/contract' +import { asyncIteratorObject, oc } from '@orpc/contract' import * as arktype from 'arktype' import z from 'zod' import { openapi } from './meta' @@ -1241,11 +1241,11 @@ describe('openAPIGenerator', () => { }) }) - it('maps event iterator inputs to an SSE request body', async () => { + it('maps AsyncIteratorObject inputs to an SSE request body', async () => { const doc = await generator.generate({ subscribe: oc .meta(openapi({})) - .input(eventIterator(z.string(), z.boolean())), + .input(asyncIteratorObject(z.string(), z.boolean())), }) expect(doc.paths?.['/subscribe']).toEqual({ @@ -1555,11 +1555,11 @@ describe('openAPIGenerator', () => { }) }) - it('maps event iterator outputs to an SSE success response', async () => { + it('maps AsyncIteratorObject outputs to an SSE success response', async () => { const doc = await generator.generate({ subscribe: oc .meta(openapi({})) - .output(eventIterator(z.string(), z.boolean())), + .output(asyncIteratorObject(z.string(), z.boolean())), }) expect(doc.paths?.['/subscribe']).toEqual({ @@ -1884,13 +1884,13 @@ describe('openAPIGenerator', () => { }) }) - it('merges repeated event iterator schemas', async () => { + it('merges repeated AsyncIteratorObject schemas', async () => { const doc = await generator.generate({ procedure: oc - .input(eventIterator(z.looseObject({ yield1: z.string() }), z.looseObject({ return1: z.string() }))) - .input(eventIterator(z.looseObject({ yield2: z.string() }), z.looseObject({ return2: z.string() }))) - .output(eventIterator(z.looseObject({ yield3: z.string() }), z.looseObject({ return3: z.string() }))) - .output(eventIterator(z.looseObject({ yield4: z.string() }), z.looseObject({ return4: z.string() }))), + .input(asyncIteratorObject(z.looseObject({ yield1: z.string() }), z.looseObject({ return1: z.string() }))) + .input(asyncIteratorObject(z.looseObject({ yield2: z.string() }), z.looseObject({ return2: z.string() }))) + .output(asyncIteratorObject(z.looseObject({ yield3: z.string() }), z.looseObject({ return3: z.string() }))) + .output(asyncIteratorObject(z.looseObject({ yield4: z.string() }), z.looseObject({ return4: z.string() }))), }) expect(doc.paths?.['/procedure']?.post).toMatchObject({ diff --git a/packages/openapi/src/openapi-generator.ts b/packages/openapi/src/openapi-generator.ts index bf278ed50..63cef215b 100644 --- a/packages/openapi/src/openapi-generator.ts +++ b/packages/openapi/src/openapi-generator.ts @@ -7,7 +7,7 @@ import type { Value } from '@orpc/shared' import type { OpenAPIMeta } from './meta' import type { OpenAPIDocument, OpenAPIOperationObject } from './types' import { COMMON_ERROR_STATUS_MAP } from '@orpc/client' -import { getEventIteratorSchemaDetails } from '@orpc/contract' +import { getAsyncIteratorObjectSchemaDetails } from '@orpc/contract' import { combineJsonObjectSchemaEntries, combineJsonSchemasWithComposition, @@ -210,16 +210,16 @@ export class OpenAPIGenerator { const inputSchemas = def.inputSchemas if (inputStructure === 'compact') { - const eventIteratorDetails = getEventIteratorDetails(inputSchemas) + const asyncIteratorObjectDetails = getAsyncIteratorObjectDetails(inputSchemas) - if (eventIteratorDetails) { - const [yieldSchemas, returnSchemas] = eventIteratorDetails + if (asyncIteratorObjectDetails) { + const [yieldSchemas, returnSchemas] = asyncIteratorObjectDetails const yieldResult = await this.convertSchemas(yieldSchemas, 'input') const returnResult = await this.convertSchemas(returnSchemas, 'input') ref.requestBody = { required: true, - content: toEventIteratorContent(yieldResult, returnResult, doc, options), + content: toAsyncIteratorObjectContent(yieldResult, returnResult, doc, options), } return @@ -388,17 +388,17 @@ export class OpenAPIGenerator { const outputStructure = meta?.outputStructure ?? DEFAULT_OPENAPI_OUTPUT_STRUCTURE if (outputStructure === 'compact') { - const eventDetails = getEventIteratorDetails(outputSchemas) + const iteratorDetails = getAsyncIteratorObjectDetails(outputSchemas) - if (eventDetails) { - const [yieldSchemas, returnSchemas] = eventDetails + if (iteratorDetails) { + const [yieldSchemas, returnSchemas] = iteratorDetails const yieldResult = await this.convertSchemas(yieldSchemas, 'output') const returnResult = await this.convertSchemas(returnSchemas, 'output') ref.responses ??= {} ref.responses[status] = { description, - content: toEventIteratorContent(yieldResult, returnResult, doc, options), + content: toAsyncIteratorObjectContent(yieldResult, returnResult, doc, options), } return @@ -604,7 +604,7 @@ function strip$schemaField(schema: JsonSchema): JsonSchema { return rest } -function getEventIteratorDetails(schemas: AnySchema[] | undefined): [yieldSchemas: AnySchema[], returnSchemas: AnySchema[]] | undefined { +function getAsyncIteratorObjectDetails(schemas: AnySchema[] | undefined): [yieldSchemas: AnySchema[], returnSchemas: AnySchema[]] | undefined { if (!schemas || schemas.length === 0) { return undefined } @@ -613,7 +613,7 @@ function getEventIteratorDetails(schemas: AnySchema[] | undefined): [yieldSchema const returnSchemas: AnySchema[] = [] for (const s of schemas) { - const details = getEventIteratorSchemaDetails(s) + const details = getAsyncIteratorObjectSchemaDetails(s) if (!details) { return undefined } @@ -627,7 +627,7 @@ function getEventIteratorDetails(schemas: AnySchema[] | undefined): [yieldSchema return yieldSchemas.length || returnSchemas.length ? [yieldSchemas, returnSchemas] : undefined } -function toEventIteratorContent( +function toAsyncIteratorObjectContent( [yieldSchema, yieldOptional]: [JsonSchema, optional: boolean], [returnSchema, returnOptional]: [JsonSchema, optional: boolean], doc: OpenAPIDocument, diff --git a/packages/openapi/src/openapi-serializer.test.ts b/packages/openapi/src/openapi-serializer.test.ts index b9c513685..6d5374885 100644 --- a/packages/openapi/src/openapi-serializer.test.ts +++ b/packages/openapi/src/openapi-serializer.test.ts @@ -87,12 +87,12 @@ describe('openAPISerializer', () => { expect(s.serialize({ file: blob }, { asFormData: false, useFormDataForBlobFields: true })).toBeInstanceOf(FormData) }) - describe('async iterator object', () => { + describe('asyncIteratorObject', () => { async function* toAsyncIter(values: T[]) { for (const v of values) yield v } - it('returns an async iterator and serializes yielded values', async () => { + it('returns an AsyncIteratorObject and serializes yielded values', async () => { const result = serializer.serialize(toAsyncIter([new Date('2023-01-01'), 42n])) as AsyncIteratorObject expect(result).toSatisfy(isAsyncIteratorObject) @@ -221,12 +221,12 @@ describe('openAPISerializer', () => { expect(serializer.deserialize(stream)).toBe(stream) }) - describe('async iterator object', () => { + describe('asyncIteratorObject', () => { async function* toAsyncIter(values: T[]) { for (const v of values) yield v } - it('returns an async iterator and passes through yielded values', async () => { + it('returns an AsyncIteratorObject and passes through yielded values', async () => { const result = serializer.deserialize(toAsyncIter([{ a: 1 }, { b: 2 }])) as AsyncIterable expect(result).toSatisfy(isAsyncIteratorObject) @@ -271,7 +271,7 @@ describe('openAPISerializer', () => { await expect(result.next()).rejects.toBe(error) }) - it('passes through non-ErrorEvent iterator errors', async () => { + it('passes through non-ErrorEvent errors', async () => { const error = new Error('unexpected') const result = serializer.deserialize((async function* () { throw error diff --git a/packages/openapi/src/openapi-serializer.ts b/packages/openapi/src/openapi-serializer.ts index 5d6e5c249..91cc06a33 100644 --- a/packages/openapi/src/openapi-serializer.ts +++ b/packages/openapi/src/openapi-serializer.ts @@ -1,7 +1,7 @@ import type { StandardBody } from '@standardserver/core' import type { BracketNotationSerializerOptions } from './bracket-notation' import type { OpenAPIJsonSerializerOptions } from './openapi-json-serializer' -import { createORPCErrorFromJson, isORPCErrorJson, toORPCError, wrapEventIteratorPreservingMeta } from '@orpc/client' +import { createORPCErrorFromJson, isORPCErrorJson, toORPCError, wrapAsyncIteratorPreservingEventMeta } from '@orpc/client' import { isAsyncIteratorObject } from '@orpc/shared' import { ErrorEvent } from '@standardserver/core' import { BracketNotationSerializer } from './bracket-notation' @@ -60,7 +60,7 @@ export class OpenAPISerializer { } if (isAsyncIteratorObject(data)) { - return wrapEventIteratorPreservingMeta(data, { + return wrapAsyncIteratorPreservingEventMeta(data, { mapResult: (result) => { // standard event stream data already supports these types without additional serialization. if (result.value === undefined) { @@ -109,7 +109,7 @@ export class OpenAPISerializer { } if (isAsyncIteratorObject(data)) { - return wrapEventIteratorPreservingMeta(data, { + return wrapAsyncIteratorPreservingEventMeta(data, { mapResult: (result) => { if (result.value === undefined) { return result diff --git a/packages/pino/package.json b/packages/pino/package.json index b93dc5ce2..0bccb8a0b 100644 --- a/packages/pino/package.json +++ b/packages/pino/package.json @@ -42,7 +42,7 @@ "@orpc/client": "workspace:*", "@orpc/server": "workspace:*", "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25" + "@standardserver/core": "^0.0.32" }, "devDependencies": { "pino": "^10.3.1" diff --git a/packages/pino/src/handler-plugin.test.ts b/packages/pino/src/handler-plugin.test.ts index 51e28da33..17a107744 100644 --- a/packages/pino/src/handler-plugin.test.ts +++ b/packages/pino/src/handler-plugin.test.ts @@ -205,7 +205,7 @@ describe('pinoHandlerPlugin', () => { expect(capturedBindings.rpc.method).toBe('ping') }) - it('logs event iterator errors as error and aborted stream errors as info', async () => { + it('logs AsyncIteratorObject errors as error and aborted stream errors as info', async () => { const baseLogger = new FakeLogger({ rpc: {} }) const streamError = new Error('stream-error') const handler1 = new StandardHandler(createCodec(os.handler(async function* () { diff --git a/packages/pino/src/handler-plugin.ts b/packages/pino/src/handler-plugin.ts index ed9aa75a4..90c064a23 100644 --- a/packages/pino/src/handler-plugin.ts +++ b/packages/pino/src/handler-plugin.ts @@ -2,7 +2,7 @@ import type { Context, ErrorMap, ProcedureClientInterceptor, Schema } from '@orp import type { StandardHandlerInterceptor, StandardHandlerOptions, StandardHandlerPlugin, StandardHandlerRoutingInterceptor, StandardHandlerRoutingInterceptorOptions } from '@orpc/server/standard' import type { Logger } from 'pino' import type { LoggerContext } from './context' -import { wrapEventIteratorPreservingMeta } from '@orpc/client' +import { wrapAsyncIteratorPreservingEventMeta } from '@orpc/client' import { isAbortError, isAsyncIteratorObject, ORPC_NAME, override, toArray, wrapReadableStream } from '@orpc/shared' import { flattenStandardHeader } from '@standardserver/core' import pino from 'pino' @@ -164,9 +164,9 @@ export class PinoHandlerPlugin implements StandardHandlerPlug if (isAsyncIteratorObject(output)) { /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for AsyncIteratorObject to remain other special properties */ - return override(output, wrapEventIteratorPreservingMeta(output, { + return override(output, wrapAsyncIteratorPreservingEventMeta(output, { onError: (error) => { logBusinessLogicError(getLogger(context), error) }, @@ -176,7 +176,7 @@ export class PinoHandlerPlugin implements StandardHandlerPlug if (output instanceof ReadableStream) { /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for ReadableStream to remain other special properties */ return override(output, wrapReadableStream(output, { onError: (error) => { diff --git a/packages/publisher/package.json b/packages/publisher/package.json index 428ae8dea..d9a6c93e3 100644 --- a/packages/publisher/package.json +++ b/packages/publisher/package.json @@ -67,7 +67,7 @@ "dependencies": { "@orpc/client": "workspace:*", "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25" + "@standardserver/core": "^0.0.32" }, "devDependencies": { "@upstash/redis": "^1.38.0", diff --git a/packages/publisher/src/adapters/memory.test.ts b/packages/publisher/src/adapters/memory.test.ts index 319d45909..7955d0185 100644 --- a/packages/publisher/src/adapters/memory.test.ts +++ b/packages/publisher/src/adapters/memory.test.ts @@ -110,7 +110,7 @@ describe('memoryPublisher', () => { await unsubscribeAfterTail() }) - it('expires old events lazily for async iterator subscribers', async () => { + it('expires old events lazily for AsyncIteratorObject subscribers', async () => { const publisher = new MemoryPublisher({ replay: { enabled: true, diff --git a/packages/publisher/src/publisher.test.ts b/packages/publisher/src/publisher.test.ts index c526c3255..387a9c2d9 100644 --- a/packages/publisher/src/publisher.test.ts +++ b/packages/publisher/src/publisher.test.ts @@ -76,7 +76,7 @@ describe('publisher', () => { await iterator.return() }) - describe('async iterator subscriptions', () => { + describe('asyncIteratorObject subscriptions', () => { it('streams messages in the order they are published', async () => { const events: string[] = [] const iterator = publisher.subscribe('message') diff --git a/packages/publisher/src/publisher.ts b/packages/publisher/src/publisher.ts index c494ab259..ff22d4d43 100644 --- a/packages/publisher/src/publisher.ts +++ b/packages/publisher/src/publisher.ts @@ -3,7 +3,7 @@ import { AsyncIteratorClass } from '@orpc/shared' export interface PublisherOptions { /** - * Maximum number of events to buffer for async iterator subscribers. + * Maximum number of events to buffer for AsyncIteratorObject subscribers. * * If the buffer exceeds this limit, the oldest event is dropped. * This prevents unbounded memory growth if consumers process events slowly. @@ -86,7 +86,7 @@ export abstract class Publisher> { */ subscribe(event: K, listener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise> /** - * Subscribes to a specific event using an async iterator. + * Subscribes to a specific event using an AsyncIteratorObject. * Useful for `for await...of` loops with optional buffering and abort support. * * @example diff --git a/packages/ratelimit/package.json b/packages/ratelimit/package.json index 53ab7e86d..56c133f5b 100644 --- a/packages/ratelimit/package.json +++ b/packages/ratelimit/package.json @@ -68,7 +68,7 @@ "dependencies": { "@orpc/server": "workspace:*", "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25" + "@standardserver/core": "^0.0.32" }, "devDependencies": { "@upstash/ratelimit": "^2.0.8", diff --git a/packages/server/package.json b/packages/server/package.json index eb940dbec..5c64ac21b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -109,10 +109,10 @@ "@orpc/contract": "workspace:*", "@orpc/interop": "workspace:*", "@orpc/shared": "workspace:*", - "@standardserver/core": "^0.0.25", - "@standardserver/fetch": "^0.0.25", - "@standardserver/node": "^0.0.25", - "@standardserver/peer": "^0.0.25", + "@standardserver/core": "^0.0.32", + "@standardserver/fetch": "^0.0.32", + "@standardserver/node": "^0.0.32", + "@standardserver/peer": "^0.0.32", "cookie": "^2.0.1" }, "devDependencies": { diff --git a/packages/server/src/adapters/fetch/handler.ts b/packages/server/src/adapters/fetch/handler.ts index c9740a14e..0fff09c6d 100644 --- a/packages/server/src/adapters/fetch/handler.ts +++ b/packages/server/src/adapters/fetch/handler.ts @@ -18,7 +18,7 @@ export type FetchHandlerFetchInterceptor = Interceptor { /** - * Options for how to convert the Fetch Response to a Standard Response, like event iterator options, etc. + * Options for how to convert the Fetch Response to a Standard Response, like event stream options, etc. */ toFetchResponse?: undefined | ToFetchResponseOptions diff --git a/packages/server/src/adapters/node/body-compression-plugin.test.ts b/packages/server/src/adapters/node/body-compression-plugin.test.ts index ad014430f..e28058918 100644 --- a/packages/server/src/adapters/node/body-compression-plugin.test.ts +++ b/packages/server/src/adapters/node/body-compression-plugin.test.ts @@ -80,7 +80,7 @@ describe('bodyCompressionHandlerPlugin', () => { expect(res.headers['content-encoding']).toBe('gzip') }) - it('does not compress event iterator responses', async () => { + it('does not compress AsyncIteratorObject responses', async () => { const handler = new RPCHandler( { ping: os.handler(async function* () { diff --git a/packages/server/src/adapters/standard/handler.test.ts b/packages/server/src/adapters/standard/handler.test.ts index c35dbf7f8..ba64409f4 100644 --- a/packages/server/src/adapters/standard/handler.test.ts +++ b/packages/server/src/adapters/standard/handler.test.ts @@ -223,7 +223,7 @@ describe('standardHandler', () => { expect(client.mock.calls[0]?.[1]?.lastEventId).toBeUndefined() }) - it('safely traces async iterator input', async () => { + it('safely traces AsyncIteratorObject input', async () => { async function* input() { yield 'e1' yield 'e2' diff --git a/packages/server/src/adapters/standard/handler.ts b/packages/server/src/adapters/standard/handler.ts index 5ad89a5fa..1717ec4f7 100644 --- a/packages/server/src/adapters/standard/handler.ts +++ b/packages/server/src/adapters/standard/handler.ts @@ -122,9 +122,9 @@ export class StandardHandler { if (isAsyncIteratorObject(input)) { /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for AsyncIteratorObject to remain other special properties */ - input = override(input, traceAsyncIterator('consume_event_iterator_input', input)) + input = override(input, traceAsyncIterator('consume_async_iterator_object_input', input)) } const client = createProcedureClient(procedure, { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 7837f0f41..ff2d3b734 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -69,6 +69,7 @@ export type { } from '@orpc/contract' export { + asyncIteratorObject, defineMeta, eventIterator, reconcileORPCError, @@ -87,13 +88,25 @@ export type { export { AsyncIteratorClass, + asyncIteratorToStream, + asyncIteratorToUnproxiedDataStream, + /** + * @deprecated Use `asyncIteratorToStream` instead. + */ asyncIteratorToStream as eventIteratorToStream, + /** + * @deprecated Use `asyncIteratorToUnproxiedDataStream` instead. + */ asyncIteratorToUnproxiedDataStream as eventIteratorToUnproxiedDataStream, onError, onFinish, onStart, onSuccess, - streamToAsyncIteratorClass as streamToEventIterator, + streamToAsyncIteratorObject, + /** + * @deprecated Use `streamToAsyncIteratorObject` instead. + */ + streamToAsyncIteratorObject as streamToEventIterator, } from '@orpc/shared' export type { diff --git a/packages/server/src/procedure-client.test.ts b/packages/server/src/procedure-client.test.ts index 66908b844..ffa275b42 100644 --- a/packages/server/src/procedure-client.test.ts +++ b/packages/server/src/procedure-client.test.ts @@ -682,7 +682,7 @@ describe('createProcedureClient', () => { expect(handler).toHaveBeenCalledWith(expect.objectContaining({ context: { auth: true } }), undefined) }) - it('trace output event iterator and use override for proxy', async () => { + it('trace output AsyncIteratorObject and use override for proxy', async () => { const handlerIterator = (async function* () { yield 'event' return 'result' @@ -842,7 +842,7 @@ describe('createProcedureClient', () => { expect(reconcileErrorSpy).toHaveBeenCalledWith(errorMap, error) }) - describe('event iterator', () => { + describe('asyncIteratorObject', () => { it('reconcile error event before throw', async () => { const error = new ORPCError('BAD_REQUEST', { data: 'data' }) handler.mockResolvedValueOnce((async function* () { diff --git a/packages/server/src/procedure-client.ts b/packages/server/src/procedure-client.ts index 9fab3b434..28cc177b9 100644 --- a/packages/server/src/procedure-client.ts +++ b/packages/server/src/procedure-client.ts @@ -6,7 +6,7 @@ import type { ORPCErrorConstructorMap } from './error' import type { Lazyable } from './lazy' import type { MiddlewareDone } from './middleware' import type { AnyProcedure, Procedure, ProcedureHandlerOptions } from './procedure' -import { cloneORPCError, ORPCError, wrapEventIteratorPreservingMeta } from '@orpc/client' +import { cloneORPCError, ORPCError, wrapAsyncIteratorPreservingEventMeta } from '@orpc/client' import { reconcileORPCError, ValidationError } from '@orpc/contract' import { intercept, isAsyncIteratorObject, override, resolveMaybeOptionalOptions, runWithSpan, toArray, traceAsyncIterator, traceReadableStream, value } from '@orpc/shared' import { createORPCErrorConstructorMap } from './error' @@ -110,18 +110,18 @@ export function createProcedureClient< if (isAsyncIteratorObject(output)) { /** - * traceAsyncIterator/wrapEventIteratorPreservingMeta return AsyncIteratorClass - * which is backwards compatible with Event Iterator & almost async iterator. + * traceAsyncIterator/wrapAsyncIteratorPreservingEventMeta return AsyncIteratorClass + * which is backwards compatible with AsyncIteratorObject. * * @warning * If remove this return, can be breaking change * because AsyncIteratorClass convert `.throw` to `.return` (rarely used) * * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for AsyncIteratorObject to remain other special properties */ - return override(output, wrapEventIteratorPreservingMeta( - traceAsyncIterator('consume_event_iterator_output', output), + return override(output, wrapAsyncIteratorPreservingEventMeta( + traceAsyncIterator('consume_async_iterator_object_output', output), { mapError: reconcileError }, )) as typeof output } @@ -129,7 +129,7 @@ export function createProcedureClient< if ((output as any) instanceof ReadableStream) { /** * @warning - * Remember use `override` for event iterator to remain other special properties + * Remember use `override` for ReadableStream to remain other special properties */ return override(output, traceReadableStream('consume_octet_stream_output', output)) as typeof output } diff --git a/packages/shared/package.json b/packages/shared/package.json index 5ebb0be00..bee01cba8 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -43,7 +43,7 @@ } }, "dependencies": { - "@standardserver/shared": "^0.0.25", + "@standardserver/shared": "^0.0.32", "radash": "^12.1.1", "type-fest": "^5.3.1" }, diff --git a/packages/shared/src/iterator.test-d.ts b/packages/shared/src/iterator.test-d.ts new file mode 100644 index 000000000..48bb6c9a8 --- /dev/null +++ b/packages/shared/src/iterator.test-d.ts @@ -0,0 +1,52 @@ +import type { PromiseWithError } from './types' +import { consumeAsyncIterator } from './iterator' + +describe('consumeAsyncIterator', () => { + it('can infer types from PromiseWithError + AsyncGenerator', () => { + void consumeAsyncIterator({} as PromiseWithError, 'error-value'>, { + onEvent: (message) => { + expectTypeOf(message).toEqualTypeOf<'message-value'>() + }, + onError: (error) => { + expectTypeOf(error).toEqualTypeOf<'error-value' | Error>() + }, + onSuccess: (value) => { + expectTypeOf(value).toEqualTypeOf<'done-value' | undefined>() + }, + onFinish: ([error, data, isSuccess]) => { + if (!error || isSuccess) { + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf<'done-value' | undefined>() + } + else { + expectTypeOf(error).toEqualTypeOf<'error-value' | Error>() + expectTypeOf(data).toEqualTypeOf() + } + }, + }) + }) + + it('can infer types from AsyncIterator', () => { + void consumeAsyncIterator({} as AsyncIterator<'message-value', 'done-value'>, { + onEvent: (message) => { + expectTypeOf(message).toEqualTypeOf<'message-value'>() + }, + onError: (error) => { + expectTypeOf(error).toEqualTypeOf() + }, + onSuccess: (value) => { + expectTypeOf(value).toEqualTypeOf<'done-value' | undefined>() + }, + onFinish: ([error, data, isSuccess]) => { + if (!error || isSuccess) { + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf<'done-value' | undefined>() + } + else { + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf() + } + }, + }) + }) +}) diff --git a/packages/shared/src/iterator.test.ts b/packages/shared/src/iterator.test.ts index 8d76dbcda..ee71172b3 100644 --- a/packages/shared/src/iterator.test.ts +++ b/packages/shared/src/iterator.test.ts @@ -1,6 +1,6 @@ import { AsyncLocalStorage } from 'node:async_hooks' import { AsyncIteratorClass, sleep } from '@standardserver/shared' -import { replicateAsyncIterator, traceAsyncIterator, wrapAsyncIterator } from './iterator' +import { consumeAsyncIterator, replicateAsyncIterator, traceAsyncIterator, wrapAsyncIterator } from './iterator' import * as OpenTelemetry from './opentelemetry' const runInSpanContextSpy = vi.spyOn(OpenTelemetry, 'runInSpanContext') @@ -395,3 +395,217 @@ describe('replicateAsyncIterator', async () => { expect(cleanup).toBe(true) }) }) + +describe('consumeAsyncIterator', () => { + it('on success', async () => { + const iterator = (async function* () { + yield 1 + yield 2 + return 3 + }()) + + const onEvent = vi.fn() + const onError = vi.fn() + const onSuccess = vi.fn() + const onFinish = vi.fn() + + void consumeAsyncIterator(iterator, { + onEvent, + onError, + onSuccess, + onFinish, + }) + + await vi.waitFor(() => { + expect(onEvent).toHaveBeenCalledTimes(2) + expect(onEvent).toHaveBeenNthCalledWith(1, 1) + expect(onEvent).toHaveBeenNthCalledWith(2, 2) + + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onSuccess).toHaveBeenNthCalledWith(1, 3) + expect(onFinish).toHaveBeenCalledTimes(1) + expect(onFinish).toHaveBeenNthCalledWith(1, [null, 3, true]) + + expect(onError).toHaveBeenCalledTimes(0) + }) + }) + + it('on error', async () => { + const error = new Error('TEST') + const iterator = (async function* () { + yield 1 + yield 2 + throw error + }()) + + const onEvent = vi.fn() + const onError = vi.fn() + const onSuccess = vi.fn() + const onFinish = vi.fn() + + void consumeAsyncIterator(iterator, { + onEvent, + onError, + onSuccess, + onFinish, + }) + + await vi.waitFor(() => { + expect(onEvent).toHaveBeenCalledTimes(2) + expect(onEvent).toHaveBeenNthCalledWith(1, 1) + expect(onEvent).toHaveBeenNthCalledWith(2, 2) + + expect(onError).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenNthCalledWith(1, error) + + expect(onFinish).toHaveBeenCalledTimes(1) + expect(onFinish).toHaveBeenNthCalledWith(1, [error, undefined, false]) + + expect(onSuccess).toHaveBeenCalledTimes(0) + }) + }) + + it('on error without onError and onFinish', async ({ onTestFinished }) => { + const unhandledRejectionHandler = vi.fn() + process.on('unhandledRejection', unhandledRejectionHandler) + onTestFinished(() => { + process.off('unhandledRejection', unhandledRejectionHandler) + }) + + const error = new Error('TEST') + const iterator = (async function* () { + yield 1 + yield 2 + throw error + }()) + + const onEvent = vi.fn() + + void consumeAsyncIterator(iterator, { + onEvent, + }) + + await vi.waitFor(() => { + expect(onEvent).toHaveBeenCalledTimes(2) + expect(onEvent).toHaveBeenNthCalledWith(1, 1) + expect(onEvent).toHaveBeenNthCalledWith(2, 2) + }) + + expect(unhandledRejectionHandler).toHaveBeenCalledTimes(1) + expect(unhandledRejectionHandler).toHaveBeenNthCalledWith(1, error, expect.anything()) + }) + + it('unsubscribe', async () => { + let cleanup = false + const iterator = (async function* () { + try { + await new Promise(resolve => setTimeout(resolve, 10)) + yield 1 + yield 2 + return 3 + } + finally { + cleanup = true + } + }()) + + const onEvent = vi.fn() + const onSuccess = vi.fn() + const onFinish = vi.fn() + const onError = vi.fn() + + const unsubscribe = consumeAsyncIterator(iterator, { + onEvent, + onError, + onSuccess, + onFinish, + }) + + await new Promise(resolve => setTimeout(resolve, 1)) + await unsubscribe() + expect(cleanup).toBe(true) + // side-effect of async generator - waiting for .next resolve before .return effect + expect(onEvent).toHaveBeenCalledTimes(1) + expect(onEvent).toHaveBeenNthCalledWith(1, 1) + + expect(onError).toHaveBeenCalledTimes(0) + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onFinish).toHaveBeenCalledTimes(1) + // undefined can be passed on success because iterator can be canceled + expect(onSuccess).toHaveBeenNthCalledWith(1, undefined) + expect(onFinish).toHaveBeenNthCalledWith(1, [null, undefined, true]) + }) + + it('error on unsubscribe', async () => { + const error = new Error('TEST') + let cleanup = false + const iterator = (async function* () { + try { + await new Promise(resolve => setTimeout(resolve, 10)) + yield 1 + yield 2 + return 3 + } + finally { + cleanup = true + // eslint-disable-next-line no-unsafe-finally + throw error + } + }()) + + const onEvent = vi.fn() + const onError = vi.fn() + const onSuccess = vi.fn() + const onFinish = vi.fn() + + const unsubscribe = consumeAsyncIterator(iterator, { + onEvent, + onError, + onSuccess, + onFinish, + }) + + await new Promise(resolve => setTimeout(resolve, 1)) + await expect(unsubscribe()).rejects.toThrow(error) + expect(cleanup).toBe(true) + // side-effect of async generator - waiting for .next resolve before .return effect + expect(onEvent).toHaveBeenCalledTimes(1) + expect(onEvent).toHaveBeenNthCalledWith(1, 1) + + expect(onError).toHaveBeenCalledTimes(0) + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onFinish).toHaveBeenCalledTimes(1) + // undefined can be passed on success because iterator can be canceled + expect(onSuccess).toHaveBeenNthCalledWith(1, undefined) + expect(onFinish).toHaveBeenNthCalledWith(1, [null, undefined, true]) + }) + + it('on iterator promise rejection', async () => { + const error = new Error('TEST') + const iterator = Promise.reject(error) + + const onEvent = vi.fn() + const onError = vi.fn() + const onSuccess = vi.fn() + const onFinish = vi.fn() + + void consumeAsyncIterator(iterator, { + onEvent, + onError, + onSuccess, + onFinish, + }) + + await vi.waitFor(() => { + expect(onEvent).toHaveBeenCalledTimes(0) + + expect(onError).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenNthCalledWith(1, error) + + expect(onFinish).toHaveBeenCalledTimes(1) + expect(onFinish).toHaveBeenNthCalledWith(1, [error, undefined, false]) + + expect(onSuccess).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/packages/shared/src/iterator.ts b/packages/shared/src/iterator.ts index 080caad29..fc2d1c22a 100644 --- a/packages/shared/src/iterator.ts +++ b/packages/shared/src/iterator.ts @@ -1,5 +1,6 @@ import type { Promisable } from 'type-fest' import type { StartSpanOptions } from './opentelemetry' +import type { PromiseWithError, ThrowableError } from './types' import { AsyncIteratorClass } from '@standardserver/shared' import { once } from './function' import { recordSpanError, runInSpanContext, startSpan } from './opentelemetry' @@ -156,3 +157,85 @@ export function replicateAsyncIterator( return replicated } + +export interface ConsumeAsyncIteratorOptions { + /** + * Called on each event + */ + onEvent: (event: T) => void + /** + * Called once error happens + */ + onError?: (error: TError) => void + /** + * Called once AsyncIteratorObject is done + * + * @info If iterator is canceled, `undefined` can be passed on success + */ + onSuccess?: (value: TReturn | undefined) => void + /** + * Called once after onError or onSuccess + * + * @info If iterator is canceled, `undefined` can be passed on success + */ + onFinish?: (state: [error: TError, data: undefined, isSuccess: false] | [error: null, data: TReturn | undefined, isSuccess: true]) => void +} + +/** + * Consumes an AsyncIteratorObject with lifecycle callbacks + * + * @warning If no `onError` or `onFinish` is provided, error will be thrown into unhandled rejection channel. + * @return unsubscribe callback + */ +export function consumeAsyncIterator( + iterator: AsyncIterator | PromiseWithError, TError>, + options: ConsumeAsyncIteratorOptions, +): () => Promise { + // The typed error should always be combined with `ThrowableError` + // because an AsyncIterator can throw any error during iteration, + // while TError only represents errors from the initial promise. + // In oRPC client/server usage, initial and iteration errors are usually the same, + // but this utility is shared, so it needs to support the general case. + + void (async () => { + let onFinishState: [error: TError | ThrowableError, data: undefined, isSuccess: false] | [error: null, data: TReturn | undefined, isSuccess: true] + + try { + const resolvedIterator = await iterator + + while (true) { + const { done, value } = await resolvedIterator.next() + + if (done) { + // if iterator is canceled, value can be undefined + const realValue = value as typeof value | undefined + onFinishState = [null, realValue, true] + options.onSuccess?.(realValue) + break + } + + options.onEvent(value) + } + } + catch (error) { + onFinishState = [error as ThrowableError, undefined, false] + + /** + * If no `onError` or `onFinish` is provided, unhandled rejections will be thrown + * This is best practice for error handling - error should not be silently ignored + */ + if (!options.onError && !options.onFinish) { + throw error + } + + options.onError?.(error as ThrowableError) + } + finally { + options.onFinish?.(onFinishState!) + } + })() + + return async () => { + await (await iterator)?.return?.() + } +} diff --git a/packages/shared/src/stream.test.ts b/packages/shared/src/stream.test.ts index fd03f1835..4d2ccdaf5 100644 --- a/packages/shared/src/stream.test.ts +++ b/packages/shared/src/stream.test.ts @@ -2,7 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks' import { AsyncIteratorClass, sleep } from '@standardserver/shared' import * as OpenTelemetry from './opentelemetry' import { promiseWithResolvers } from './promise' -import { asyncIteratorToStream, asyncIteratorToUnproxiedDataStream, replicateReadableStream, streamToAsyncIteratorClass, traceReadableStream, wrapReadableStream } from './stream' +import { asyncIteratorToStream, asyncIteratorToUnproxiedDataStream, replicateReadableStream, streamToAsyncIteratorObject, traceReadableStream, wrapReadableStream } from './stream' const runInSpanContextSpy = vi.spyOn(OpenTelemetry, 'runInSpanContext') const startSpanSpy = vi.spyOn(OpenTelemetry, 'startSpan') @@ -334,7 +334,7 @@ function createSpan() { } as any } -describe('streamToAsyncIteratorClass', () => { +describe('streamToAsyncIteratorObject', () => { it('should convert a ReadableStream to AsyncIteratorClass', async () => { const values = [1, 2, 3, 4, 5] const stream = new ReadableStream({ @@ -344,7 +344,7 @@ describe('streamToAsyncIteratorClass', () => { }, }) - const iterator = streamToAsyncIteratorClass(stream) + const iterator = streamToAsyncIteratorObject(stream) expect(iterator).toBeInstanceOf(AsyncIteratorClass) const results: number[] = [] @@ -362,7 +362,7 @@ describe('streamToAsyncIteratorClass', () => { }, }) - const iterator = streamToAsyncIteratorClass(stream) + const iterator = streamToAsyncIteratorObject(stream) const results: number[] = [] for await (const value of iterator) { @@ -380,7 +380,7 @@ describe('streamToAsyncIteratorClass', () => { }, }) - const iterator = streamToAsyncIteratorClass(stream) + const iterator = streamToAsyncIteratorObject(stream) try { for await (const value of iterator) { @@ -407,7 +407,7 @@ describe('streamToAsyncIteratorClass', () => { }, }) - const iterator = streamToAsyncIteratorClass(stream) + const iterator = streamToAsyncIteratorObject(stream) await iterator.next() await iterator.return() @@ -430,7 +430,7 @@ describe('streamToAsyncIteratorClass', () => { cancel, }) - const iterator = streamToAsyncIteratorClass(stream, { signal: controller.signal }) + const iterator = streamToAsyncIteratorObject(stream, { signal: controller.signal }) await expect(iterator.next()).rejects.toBe(controller.signal.reason) expect(cancel).toHaveBeenCalledTimes(1) @@ -451,7 +451,7 @@ describe('streamToAsyncIteratorClass', () => { cancel, }) - const iterator = streamToAsyncIteratorClass(stream, { signal: controller.signal }) + const iterator = streamToAsyncIteratorObject(stream, { signal: controller.signal }) await iterator.next() controller.abort() @@ -474,7 +474,7 @@ describe('streamToAsyncIteratorClass', () => { cancel, }) - const iterator = streamToAsyncIteratorClass(stream, { signal: controller.signal }) + const iterator = streamToAsyncIteratorObject(stream, { signal: controller.signal }) const nextPromise = iterator.next() await sleep(0) @@ -499,7 +499,7 @@ describe('streamToAsyncIteratorClass', () => { cancel, }) - const iterator = streamToAsyncIteratorClass(stream, { signal: controller.signal }) + const iterator = streamToAsyncIteratorObject(stream, { signal: controller.signal }) const results: number[] = [] for await (const value of iterator) { @@ -537,7 +537,7 @@ describe('asyncIteratorToStream', () => { expect(results).toEqual([1, 2, 3]) }) - it('should handle empty async iterator', async () => { + it('should handle empty AsyncIteratorObject', async () => { async function* emptyGenerator() { // Empty generator } @@ -552,7 +552,7 @@ describe('asyncIteratorToStream', () => { expect(result.value).toBeUndefined() }) - it('should handle async iterator errors', async () => { + it('should handle AsyncIteratorObject errors', async () => { const error = new Error('Iterator error') async function* errorGenerator() { yield 1 @@ -605,9 +605,9 @@ it('streamToAsyncIteratorClass + asyncIteratorToStream', async () => { }, }) - const iterator = streamToAsyncIteratorClass(stream) + const iterator = streamToAsyncIteratorObject(stream) const newStream = asyncIteratorToStream(iterator) - const newIterator = streamToAsyncIteratorClass(newStream) + const newIterator = streamToAsyncIteratorObject(newStream) const results: number[] = [] for await (const value of newIterator) { @@ -677,7 +677,7 @@ describe('asyncIteratorToUnproxiedDataStream', () => { expect(results.some(isProxied)).toBe(false) }) - it('should handle empty async iterator', async () => { + it('should handle empty AsyncIteratorObject', async () => { async function* emptyGenerator() { // Empty generator } @@ -692,7 +692,7 @@ describe('asyncIteratorToUnproxiedDataStream', () => { expect(result.value).toBeUndefined() }) - it('should handle async iterator errors', async () => { + it('should handle AsyncIteratorObject errors', async () => { const error = new Error('Iterator error') async function* errorGenerator() { yield 1 diff --git a/packages/shared/src/stream.ts b/packages/shared/src/stream.ts index 17849ea27..2ae3b5ee1 100644 --- a/packages/shared/src/stream.ts +++ b/packages/shared/src/stream.ts @@ -123,9 +123,9 @@ export function traceReadableStream( } /** - * Converts a `ReadableStream` into an `AsyncIteratorClass`. + * Converts a {@link ReadableStream} into an {@link AsyncIteratorClass}. */ -export function streamToAsyncIteratorClass( +export function streamToAsyncIteratorObject( stream: ReadableStream, { signal }: { signal?: undefined | AbortSignal } = {}, ): AsyncIteratorClass { diff --git a/packages/tanstack-query/src/procedure-utils.test.ts b/packages/tanstack-query/src/procedure-utils.test.ts index 4ad86bbbe..995a436be 100644 --- a/packages/tanstack-query/src/procedure-utils.test.ts +++ b/packages/tanstack-query/src/procedure-utils.test.ts @@ -161,7 +161,7 @@ describe('procedureUtils', () => { const options = utils.streamedOptions({ input: { search: '__search__' }, context: { batch: '__batch__' } }) client.mockResolvedValueOnce('INVALID') - await expect(options.queryFn!({ signal, client: queryClient } as any)).rejects.toThrow('streamedQuery requires an event iterator output') + await expect(options.queryFn!({ signal, client: queryClient } as any)).rejects.toThrow('streamedQuery requires an AsyncIteratorObject output') expect(client).toHaveBeenCalledTimes(1) expect(client).toHaveBeenCalledWith({ search: '__search__' }, { signal, context: { batch: '__batch__', @@ -255,7 +255,7 @@ describe('procedureUtils', () => { const options = utils.liveOptions({ input: { search: '__search__' }, context: { batch: '__batch__' } }) client.mockResolvedValueOnce('INVALID') - await expect(options.queryFn!({ signal, client: queryClient } as any)).rejects.toThrow('liveQuery requires an event iterator output') + await expect(options.queryFn!({ signal, client: queryClient } as any)).rejects.toThrow('liveQuery requires an AsyncIteratorObject output') expect(client).toHaveBeenCalledTimes(1) expect(client).toHaveBeenCalledWith({ search: '__search__' }, { signal, diff --git a/packages/tanstack-query/src/procedure-utils.ts b/packages/tanstack-query/src/procedure-utils.ts index a07bd03f0..bb624a72f 100644 --- a/packages/tanstack-query/src/procedure-utils.ts +++ b/packages/tanstack-query/src/procedure-utils.ts @@ -281,7 +281,7 @@ export class ProcedureUtils { diff --git a/playgrounds/cloudflare/worker/routers/message.ts b/playgrounds/cloudflare/worker/routers/message.ts index 4c6046183..5df88ffbe 100644 --- a/playgrounds/cloudflare/worker/routers/message.ts +++ b/playgrounds/cloudflare/worker/routers/message.ts @@ -1,6 +1,6 @@ import { publicOS } from '../orpc' import { openapi } from '@orpc/openapi' -import { eventIterator } from '@orpc/server' +import { asyncIteratorObject } from '@orpc/server' import { z } from 'zod' export const publishMessage = publicOS @@ -28,7 +28,7 @@ export const subscribeMessages = publicOS .input(z.object({ channel: z.string().describe('Channel name, use an unguessable unique value for security'), })) - .output(eventIterator(z.object({ + .output(asyncIteratorObject(z.object({ message: z.string(), }))) .handler(async ({ context, signal, lastEventId }, { channel }) => { diff --git a/playgrounds/nest/src/contracts/message.ts b/playgrounds/nest/src/contracts/message.ts index e782d5d80..f02f7dc56 100644 --- a/playgrounds/nest/src/contracts/message.ts +++ b/playgrounds/nest/src/contracts/message.ts @@ -1,6 +1,6 @@ import { oc } from '@orpc/contract' import { openapi } from '@orpc/openapi' -import { eventIterator } from '@orpc/server' +import { asyncIteratorObject } from '@orpc/server' import { z } from 'zod' export const publishMessage = oc @@ -25,6 +25,6 @@ export const subscribeMessages = oc .input(z.object({ channel: z.string().describe('Channel name, use an unguessable unique value for security'), })) - .output(eventIterator(z.object({ + .output(asyncIteratorObject(z.object({ message: z.string(), }))) diff --git a/playgrounds/next/src/routers/message.ts b/playgrounds/next/src/routers/message.ts index 0916a91ce..9d1bbcc2a 100644 --- a/playgrounds/next/src/routers/message.ts +++ b/playgrounds/next/src/routers/message.ts @@ -1,6 +1,6 @@ import { publicOS } from '@/orpc' import { openapi } from '@orpc/openapi' -import { eventIterator } from '@orpc/server' +import { asyncIteratorObject } from '@orpc/server' import { z } from 'zod' export const publishMessage = publicOS @@ -28,7 +28,7 @@ export const subscribeMessages = publicOS .input(z.object({ channel: z.string().describe('Channel name, use an unguessable unique value for security'), })) - .output(eventIterator(z.object({ + .output(asyncIteratorObject(z.object({ message: z.string(), }))) .handler(async ({ context, signal, lastEventId }, { channel }) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f54861586..537ef51f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,17 +72,17 @@ importers: specifier: workspace:* version: link:packages/zod '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/fetch': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/peer': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/shared': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -328,8 +328,8 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 devDependencies: '@types/bun': specifier: ^1.3.14 @@ -344,14 +344,14 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/fetch': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/peer': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 devDependencies: zod: specifier: ^4.4.3 @@ -372,8 +372,8 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 devDependencies: '@cloudflare/vitest-pool-workers': specifier: ^0.16.20 @@ -441,8 +441,8 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 devDependencies: evlog: specifier: ^2.19.2 @@ -500,11 +500,11 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/node': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 devDependencies: '@fastify/cookie': specifier: ^11.0.2 @@ -580,11 +580,11 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/fetch': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 rou3: specifier: ^0.8.1 version: 0.8.1 @@ -630,8 +630,8 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 devDependencies: pino: specifier: ^10.3.1 @@ -646,8 +646,8 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 devDependencies: '@upstash/redis': specifier: ^1.38.0 @@ -665,8 +665,8 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 devDependencies: '@upstash/ratelimit': specifier: ^2.0.8 @@ -696,17 +696,17 @@ importers: specifier: workspace:* version: link:../shared '@standardserver/core': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/fetch': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/node': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 '@standardserver/peer': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 cookie: specifier: ^2.0.1 version: 2.0.1 @@ -724,8 +724,8 @@ importers: packages/shared: dependencies: '@standardserver/shared': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.32 + version: 0.0.32 radash: specifier: ^12.1.1 version: 12.1.1 @@ -4289,20 +4289,20 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@standardserver/core@0.0.25': - resolution: {integrity: sha512-WIp2orfBhFIMfIqejN8O0UEP+dzcD1485J9nIURapD3tSo7jZ+ElgYhaLVc7Ilfmf6N9qIMrzSdLOTD7qjeL8w==} + '@standardserver/core@0.0.32': + resolution: {integrity: sha512-2d1EnjbTqM25A9+ry7uURfjoQfHkDoj4qBx+FulZtqI7im1j4TSKBsAwTAhko6MOzfXAeD+lLXmy6d21wjiR0w==} - '@standardserver/fetch@0.0.25': - resolution: {integrity: sha512-8VpXDUTdhoqngX/pE6vv60rqtvds6N+KdQgo7JlKTYULgJOYToxuU6Imc2tp6nl+QHierhoP0pxRlbrpwL6ATg==} + '@standardserver/fetch@0.0.32': + resolution: {integrity: sha512-ipONyEOmoVfOxo+Mr6a0sT3IA98GRijUH81wA7tJaXBMLlfeV8AvGiRPZjQedZnB6AwNA/Tr3Wx7FHendQPTyw==} - '@standardserver/node@0.0.25': - resolution: {integrity: sha512-pSgTBPw6eKzsHwYOXwjt5jZmWzd7ePiNCnfMS0jUZci5QLWyUvL951dDF+6ERuxqE11BKXX5HjPcw9aaKTH3ig==} + '@standardserver/node@0.0.32': + resolution: {integrity: sha512-/QrX6yvM23odYp6/3lt+BSxXida4Eo8E9Sq1kuDKaJDSg20t0WEt714dBWKsf0s2api+wDq+bmz1x9LcBRRHaw==} - '@standardserver/peer@0.0.25': - resolution: {integrity: sha512-tTe9ZSdFTYdhMH5T7UlujWFCU033jL5e6OJ3CgwSyhJp4iOWGaxNtYPq6CDXD0cQRJJpdOcKfoQ8PkmOwxWU8Q==} + '@standardserver/peer@0.0.32': + resolution: {integrity: sha512-vSPxnNw3rajr/cNXIJrr5LhjyzF2p9uXxm2IAGMBf1BJExr3Gb9F1V5hdK7dVZAhoC2IhEleIlxhRlZ2m2tgtw==} - '@standardserver/shared@0.0.25': - resolution: {integrity: sha512-w7c+dXX9Si2Gi5OJFqe0Yh+hhn2EC2MV3q7AUtTQTUadOb7vZ4Yb/4jgz9O8aTiwjK1UPAk/xWINLC/Ip2j7TA==} + '@standardserver/shared@0.0.32': + resolution: {integrity: sha512-OTZkjAn01IBYrKC6HwcNeh1+R3D3fiXnGCRPEPfG0A0nM/he71iPxlOTfurTQq7XzveeIz2maTpmBJCYSXt8QQ==} '@stylistic/eslint-plugin@5.10.0': resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==} @@ -13604,27 +13604,27 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@standardserver/core@0.0.25': + '@standardserver/core@0.0.32': dependencies: - '@standardserver/shared': 0.0.25 + '@standardserver/shared': 0.0.32 - '@standardserver/fetch@0.0.25': + '@standardserver/fetch@0.0.32': dependencies: - '@standardserver/core': 0.0.25 - '@standardserver/shared': 0.0.25 + '@standardserver/core': 0.0.32 + '@standardserver/shared': 0.0.32 - '@standardserver/node@0.0.25': + '@standardserver/node@0.0.32': dependencies: - '@standardserver/core': 0.0.25 - '@standardserver/fetch': 0.0.25 - '@standardserver/shared': 0.0.25 + '@standardserver/core': 0.0.32 + '@standardserver/fetch': 0.0.32 + '@standardserver/shared': 0.0.32 - '@standardserver/peer@0.0.25': + '@standardserver/peer@0.0.32': dependencies: - '@standardserver/core': 0.0.25 - '@standardserver/shared': 0.0.25 + '@standardserver/core': 0.0.32 + '@standardserver/shared': 0.0.32 - '@standardserver/shared@0.0.25': {} + '@standardserver/shared@0.0.32': {} '@stylistic/eslint-plugin@5.10.0(eslint@10.6.0(jiti@2.7.0))': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4ce1ac8ba..730851cbc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -35,3 +35,10 @@ allowBuilds: tree-sitter-json: false vue-demi: false workerd: false + +minimumReleaseAgeExclude: + - '@standardserver/core@0.0.32' + - '@standardserver/fetch@0.0.32' + - '@standardserver/node@0.0.32' + - '@standardserver/peer@0.0.32' + - '@standardserver/shared@0.0.32' diff --git a/tests/batch/batch-plugin.test.ts b/tests/batch/batch-plugin.test.ts index af895ed02..dffe78acd 100644 --- a/tests/batch/batch-plugin.test.ts +++ b/tests/batch/batch-plugin.test.ts @@ -41,7 +41,7 @@ describe.each([ expect(fetchSpy).toHaveBeenCalledTimes(1) // ensure batch was used }) - it('support readable stream, blob, json, and event iterator responses in buffered mode', async () => { + it('support readable stream, blob, json, and AsyncIteratorObject responses in buffered mode', async () => { const streamProcedure = vi.fn(async () => new ReadableStream({ async start(controller) { controller.enqueue(new TextEncoder().encode('part 1')) @@ -110,7 +110,7 @@ describe.each([ expect(fetchSpy).toHaveBeenCalledTimes(1) // ensure batch was used }) - it('supports readable stream, blob, json, and event iterator responses in streaming mode', async () => { + it('supports readable stream, blob, json, and AsyncIteratorObject responses in streaming mode', async () => { const streamProcedure = vi.fn(async () => new ReadableStream({ async start(controller) { controller.enqueue(new TextEncoder().encode('part 1')) diff --git a/tests/openapi/cancel-and-abort.test.ts b/tests/openapi/cancel-and-abort.test.ts index d2b5fb29f..3475b7d1b 100644 --- a/tests/openapi/cancel-and-abort.test.ts +++ b/tests/openapi/cancel-and-abort.test.ts @@ -42,7 +42,7 @@ describe.each([ expect(handler.mock.calls[0]![0].signal.aborted).toBe(true) }) - it('server should cancel event iterator response and abort request when client cancels', async () => { + it('server should cancel AsyncIteratorObject response and abort request when client cancels', async () => { const cancel = vi.fn() handler.mockResolvedValueOnce(new AsyncIteratorClass( async () => { diff --git a/tests/openapi/data-transfer.test.ts b/tests/openapi/data-transfer.test.ts index f28e99eb0..d45749b9e 100644 --- a/tests/openapi/data-transfer.test.ts +++ b/tests/openapi/data-transfer.test.ts @@ -172,7 +172,7 @@ describe.each([ }) }) - it('supports event iterator and transfers events in parallel', async () => { + it('supports AsyncIteratorObject and transfers events in parallel', async () => { const stream = (async function* () { yield 'order 1' await sleep(200) diff --git a/tests/rpc/cancel-and-abort.test.ts b/tests/rpc/cancel-and-abort.test.ts index 1f41b6941..a66e932f0 100644 --- a/tests/rpc/cancel-and-abort.test.ts +++ b/tests/rpc/cancel-and-abort.test.ts @@ -39,7 +39,7 @@ describe.each([ expect(handler.mock.calls[0]![0].signal.aborted).toBe(true) }) - it('server should cancel event iterator response and abort request when client cancels', async () => { + it('server should cancel AsyncIteratorObject response and abort request when client cancels', async () => { const cancel = vi.fn() handler.mockResolvedValueOnce(new AsyncIteratorClass( async () => { diff --git a/tests/rpc/data-transfer.test.ts b/tests/rpc/data-transfer.test.ts index 4456c73db..0d96ea8a7 100644 --- a/tests/rpc/data-transfer.test.ts +++ b/tests/rpc/data-transfer.test.ts @@ -82,7 +82,7 @@ describe.each([ expect(Date.now() - startTime).toBeLessThan(100) }) - it('support event iterator and transfer event iterator in parallel', async () => { + it('support AsyncIteratorObject and transfer AsyncIteratorObject in parallel', async () => { const stream = (async function* () { yield 'order 1' await sleep(200)