From 267ad71d7d794a429272e4fa0c7b926ced2fc499 Mon Sep 17 00:00:00 2001 From: saltyaom Date: Tue, 4 Jun 2024 14:26:45 +0700 Subject: [PATCH] :tada: feat: generator stream --- .eslintrc.json | 3 +- bun.lockb | Bin 99331 -> 100934 bytes example/a.ts | 77 ++++++++++++++++------------ example/b.ts | 64 ++++++++++------------- package.json | 1 + src/handler.ts | 80 ++++++++++++++++++++++++++--- test/response/stream.test.ts | 96 +++++++++++++++++++++++++++++++++++ 7 files changed, 244 insertions(+), 77 deletions(-) create mode 100644 test/response/stream.test.ts diff --git a/.eslintrc.json b/.eslintrc.json index 62667f63..3c009c11 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,7 +24,8 @@ "@typescript-eslint/no-namespace": "off", "no-case-declarations": "off", "no-extra-semi": "off", - "sonarjs/cognitive-complexity": "off" + "sonarjs/cognitive-complexity": "off", + "sonarjs/no-all-duplicated-branches": "off" }, "ignorePatterns": ["example/*", "test/**/*"] } diff --git a/bun.lockb b/bun.lockb index 36f18b45fe40582d55fd50b46bc9f73d3c2af51f..dcb0e4a455208f8b0bc020fce5cf0ad2e01a3afc 100755 GIT binary patch delta 14350 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 delta 13443 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 b-g`4$bw1}V@j)gld$BN!&6 { - await onBeforeHandle(async ({ name, start, stop, children }) => { - for (const onStart of children) - onStart(async ({ name, start, stop }) => { - console.log(name, start, '->', await stop, 'ms') - }) - - console.log('beforeHandle took', (await stop) - start, 'ms') - }) - - await onAfterHandle(async ({ name, start, stop, children }) => { - for (const onStart of children) - onStart(async ({ name, start, stop }) => { - console.log(name, start, '->', await stop, 'ms') - }) - - console.log('afterHandle took', (await stop) - start, 'ms') - }) - }) - .get('/', 'okkkk XD', { - beforeHandle: [ - async function a() { - await Bun.sleep(100) - }, - async function b() { - await Bun.sleep(150) - } - ] - }) - import { req } from '../test/utils' -app.handle(req('/')).then((x) => x.text()) + +const app = new Elysia().get('/', async function* () { + yield 'a' + await Bun.sleep(10) + + yield 'b' + await Bun.sleep(10) + + yield 'c' +}) + +// console.log(app.routes[0].composed?.toString()) + +const controller = new AbortController() + +setTimeout(() => { + controller.abort() +}, 15) + +app.handle( + new Request('http://e.ly', { + signal: controller.signal + }) +) + .then((x) => x.body) + .then((x) => { + if (!x) return + + const reader = x?.getReader() + + let acc = '' + const { promise, resolve } = Promise.withResolvers() + + reader.read().then(function pump({ done, value }): unknown { + if (done) return resolve(acc) + + acc += value.toString() + console.log(value.toString()) + return reader.read().then(pump) + }) + + return promise + }) + .then((x) => console.log('Result:', x)) diff --git a/example/b.ts b/example/b.ts index 972065b0..6264b132 100644 --- a/example/b.ts +++ b/example/b.ts @@ -1,43 +1,35 @@ -import { Elysia, t } from '../src' -import { req, post } from '../test/utils' +import { Elysia } from '../src' -const app = new Elysia({ precompile: true }) - .trace(async ({ parse, transform, beforeHandle, context }) => { - { - const { end, time } = await parse - console.log('Parse', (await end) - time) - } +const app = new Elysia() + .trace(async ({ onBeforeHandle, onAfterHandle }) => { + await onBeforeHandle(async ({ name, start, stop, children }) => { + for (const onStart of children) + onStart(async ({ name, start, stop }) => { + console.log(name, start, '->', await stop, 'ms') + }) - { - const { end, time } = await transform - console.log('Transform', (await end) - time) - } + console.log('beforeHandle took', (await stop) - start, 'ms') + }) - { - const { end, time } = await beforeHandle - console.log('Before Handle', (await end) - time) - } + await onAfterHandle(async ({ name, start, stop, children }) => { + for (const onStart of children) + onStart(async ({ name, start, stop }) => { + console.log(name, start, '->', await stop, 'ms') + }) + + console.log('afterHandle took', (await stop) - start, 'ms') + }) }) - .get( - '/', - (context) => { - console.log("A") - - return 'a' - }, - { - beforeHandle() { - // await new Promise((r) => setTimeout(r, 10)) + .get('/', 'okkkk XD', { + beforeHandle: [ + async function a() { + await Bun.sleep(100) }, - error({ error }) { - console.log(error) + async function b() { + await Bun.sleep(150) } - } - ) + ] + }) -// console.log(app.routes[0].composed.toString()) - -await app.handle(req('/')).then((x) => x.text()) -// .then(console.log) - -// console.log(app.event.trace[0].fn) +import { req } from '../test/utils' +app.handle(req('/')).then((x) => x.text()) diff --git a/package.json b/package.json index 01f64ed3..4214b6e4 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "openapi-types": "^12.1.3" }, "devDependencies": { + "@elysiajs/stream": "^1.0.2", "@types/benchmark": "^2.1.5", "@types/bun": "^1.1.2", "@types/cookie": "^0.6.0", diff --git a/src/handler.ts b/src/handler.ts index 0fe9c3f0..c4066cd3 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -111,6 +111,50 @@ export const serializeCookie = (cookies: Context['set']['cookie']) => { return set } +// const concatUint8Array = (a: Uint8Array, b: Uint8Array) => { +// const arr = new Uint8Array(a.length + b.length) +// arr.set(a, 0) +// arr.set(b, a.length) + +// return arr +// } + +const handleStream = ( + generator: Generator | AsyncGenerator, + request?: Request +) => { + let end = false + + return new Response( + new ReadableStream({ + async start(controller) { + request?.signal.addEventListener('abort', () => { + end = true + + try { + controller.close() + } catch { + // nothing + } + }) + + for await (const chunk of generator) { + if (end) break + if (!chunk) continue + + controller.enqueue(Buffer.from(chunk.toString())) + } + + try { + controller.close() + } catch { + // nothing + } + } + }) + ) +} + export const mapResponse = ( response: unknown, set: Context['set'], @@ -324,6 +368,10 @@ export const mapResponse = ( if (response instanceof Error) return errorToResponse(response as Error, set) + // @ts-expect-error + if (typeof response?.next === 'function') + return handleStream(response as any, request) + if ('charCodeAt' in (response as any)) { const code = (response as any).charCodeAt(0) @@ -408,7 +456,7 @@ export const mapResponse = ( case 'Promise': // @ts-ignore return (response as any as Promise).then((x) => { - const r = mapCompactResponse(x) + const r = mapCompactResponse(x, request) if (r !== undefined) return r @@ -417,7 +465,7 @@ export const mapResponse = ( // ? Maybe response or Blob case 'Function': - return mapCompactResponse((response as Function)()) + return mapCompactResponse((response as Function)(), request) case 'Number': case 'Boolean': @@ -446,6 +494,10 @@ export const mapResponse = ( if (response instanceof Error) return errorToResponse(response as Error, set) + // @ts-expect-error + if (typeof response?.next === 'function') + return handleStream(response as any, request) + if ('charCodeAt' in (response as any)) { const code = (response as any).charCodeAt(0) @@ -677,6 +729,10 @@ export const mapEarlyResponse = ( if (response instanceof Error) return errorToResponse(response as Error, set) + // @ts-expect-error + if (typeof response?.next === 'function') + return handleStream(response as any, request) + if ('charCodeAt' in (response as any)) { const code = (response as any).charCodeAt(0) @@ -766,7 +822,7 @@ export const mapEarlyResponse = ( return errorToResponse(response as Error, set) case 'Function': - return mapCompactResponse((response as Function)()) + return mapCompactResponse((response as Function)(), request) case 'Number': case 'Boolean': @@ -795,6 +851,10 @@ export const mapEarlyResponse = ( if (response instanceof Error) return errorToResponse(response as Error, set) + // @ts-expect-error + if (typeof response?.next === 'function') + return handleStream(response as any, request) + if ('charCodeAt' in (response as any)) { const code = (response as any).charCodeAt(0) @@ -896,13 +956,13 @@ export const mapCompactResponse = ( case 'Promise': // @ts-ignore - return (response as any as Promise).then( - mapCompactResponse + return (response as any as Promise).then((x) => + mapCompactResponse(x, request) ) // ? Maybe response or Blob case 'Function': - return mapCompactResponse((response as Function)()) + return mapCompactResponse((response as Function)(), request) case 'Number': case 'Boolean': @@ -920,11 +980,17 @@ export const mapCompactResponse = ( }) if (response instanceof Promise) - return response.then(mapCompactResponse) as any + return response.then((x) => + mapCompactResponse(x, request) + ) as any if (response instanceof Error) return errorToResponse(response as Error) + // @ts-expect-error + if (typeof response?.next === 'function') + return handleStream(response as any, request) + if ('charCodeAt' in (response as any)) { const code = (response as any).charCodeAt(0) diff --git a/test/response/stream.test.ts b/test/response/stream.test.ts new file mode 100644 index 00000000..08002327 --- /dev/null +++ b/test/response/stream.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from 'bun:test' +import { req } from '../utils' + +import { Elysia } from '../../src' + +describe('Stream', () => { + it('handle stream', async () => { + const expected = ['a', 'b', 'c'] + + const app = new Elysia().get('/', async function* () { + yield 'a' + await Bun.sleep(10) + + yield 'b' + await Bun.sleep(10) + + yield 'c' + }) + + const response = await app + .handle(req('/')) + .then((x) => x.body) + .then((x) => { + if (!x) return + + const reader = x?.getReader() + + let acc = '' + const { promise, resolve } = Promise.withResolvers() + + reader.read().then(function pump({ done, value }): unknown { + if (done) return resolve(acc) + + expect(value.toString()).toBe(expected.shift()!) + + acc += value.toString() + return reader.read().then(pump) + }) + + return promise + }) + + expect(expected).toHaveLength(0) + expect(response).toBe('abc') + }) + + it('stop stream on canceled request', async () => { + const expected = ['a', 'b'] + + const app = new Elysia().get('/', async function* () { + yield 'a' + await Bun.sleep(10) + + yield 'b' + await Bun.sleep(10) + + yield 'c' + }) + + const controller = new AbortController() + + setTimeout(() => { + controller.abort() + }, 15) + + const response = await app + .handle( + new Request('http://e.ly', { + signal: controller.signal + }) + ) + .then((x) => x.body) + .then((x) => { + if (!x) return + + const reader = x?.getReader() + + let acc = '' + const { promise, resolve } = Promise.withResolvers() + + reader.read().then(function pump({ done, value }): unknown { + if (done) return resolve(acc) + + expect(value.toString()).toBe(expected.shift()!) + + acc += value.toString() + return reader.read().then(pump) + }) + + return promise + }) + + expect(expected).toHaveLength(0) + expect(response).toBe('ab') + }) +})