Skip to content

Commit

Permalink
feat: add Server-Sent Events mock support
Browse files Browse the repository at this point in the history
  • Loading branch information
pengzhanbo committed Dec 12, 2024
1 parent 2c77b1c commit 161cafa
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 2 deletions.
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Implement a mock-dev-server in `rspack` and `rsbuild` that is fully consistent w
- ⚓️ Support `alias` in the mock file.
- 📤 Support `multipart` content-type, mock upload file.
- 📥 Support mock download file.
- ⚜️ Support `WebSocket Mock`
- ⚜️ Support `WebSocket Mock` and `Server-Sent Events Mock`
- 🗂 Support building small independent deployable mock services.

## Install
Expand Down Expand Up @@ -187,6 +187,23 @@ export default definePostMock({
})
```

### createSSEStream(req, res)

Create a `Server-sent events` write stream to support mocking `EventSource`.

``` ts
import { createSSEStream, defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
url: '/api/sse',
response: (req, res) => {
const sse = createSSEStream(req, res)
sse.write({ event: 'message', data: { message: 'hello world' } })
sse.end()
}
})
```

## Plugin Options

### options.prefix
Expand Down Expand Up @@ -860,6 +877,40 @@ ws.addEventListener('message', (raw) => {
})
```

**exp:** EventSource Mock

```ts
// sse.mock.ts
import { createSSEStream, defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
url: '/api/sse',
response(req, res) {
const sse = createSSEStream(req, res)
let count = 0
const timer = setInterval(() => {
sse.write({
event: 'count',
data: { count: ++count },
})
if (count >= 10) {
sse.end()
clearInterval(timer)
}
}, 1000)
},
})
```

```ts
// app.js
const es = new EventSource('/api/sse')

es.addEventListener('count', (e) => {
console.log(e.data)
})
```

## Mock Services

In some scenarios, it may be necessary to use the data provided by mock services for display purposes, but the project may have already been packaged, built and deployed without support from `rspack/rsbuild` and this plugin's mock service. Since this plugin supports importing various node modules in mock files at the design stage, the mock file cannot be inline into client build code.
Expand Down
53 changes: 52 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
- ⚓️ 支持在 mock文件中使用 `resolve.alias` 路径别名
- 📤 支持 multipart 类型,模拟文件上传
- 📥 支持模拟文件下载
- ⚜️ 支持模拟 `WebSocket`
- ⚜️ 支持模拟 `WebSocket``Server-Sent Events`
- 🗂 支持构建可独立部署的小型mock服务

## 安装
Expand Down Expand Up @@ -187,6 +187,23 @@ export default definePostMock({
})
```

### createSSEStream(req, res)

创建一个 `Server-sent events` 写入流,用于支持模拟 `EventSource`

``` ts
import { createSSEStream, defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
url: '/api/sse',
response: (req, res) => {
const sse = createSSEStream(req, res)
sse.write({ event: 'message', data: { message: 'hello world' } })
sse.end()
}
})
```

## 插件配置

### options.prefix
Expand Down Expand Up @@ -858,6 +875,40 @@ ws.addEventListener('message', (raw) => {
})
```

**示例:** EventSource Mock

```ts
// sse.mock.ts
import { createSSEStream, defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
url: '/api/sse',
response(req, res) {
const sse = createSSEStream(req, res)
let count = 0
const timer = setInterval(() => {
sse.write({
event: 'count',
data: { count: ++count },
})
if (count >= 10) {
sse.end()
clearInterval(timer)
}
}, 1000)
},
})
```

```ts
// app.js
const es = new EventSource('/api/sse')

es.addEventListener('count', (e) => {
console.log(e.data)
})
```

## 独立部署的小型mock服务

在一些场景中,可能会需要使用mock服务提供的数据支持,用于展示,但可能项目已完成打包构建部署,已脱离 `rspack/rsbuild` 和本插件提供的 mock服务支持。由于本插件在设计之初,支持在mock文件中引入各种 `node` 模块,所以不能将 mock文件打包内联到客户端构建代码中。
Expand Down
20 changes: 20 additions & 0 deletions examples/rsbuild-starter/mock/sse.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createSSEStream, defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
url: '/api/sse',
response(req, res) {
const sse = createSSEStream(req, res)
let count = 0
const timer = setInterval(() => {
sse.write({
event: 'count',
data: { count: ++count },
})
if (count >= 10) {
sse.write({ event: 'close' })
sse.end()
clearInterval(timer)
}
}, 1000)
},
})
14 changes: 14 additions & 0 deletions examples/rsbuild-starter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,19 @@ function webSocketMock() {
}, 3000)
}

function eventSourceMock() {
const es = new EventSource('/api/sse')
es.addEventListener('count', (e) => {
// eslint-disable-next-line no-console
console.log('see count ->', e.data)
})
es.addEventListener('close', () => {
es.close()
// eslint-disable-next-line no-console
console.log('sse closed')
})
}

bootstrap()
webSocketMock()
eventSourceMock()
20 changes: 20 additions & 0 deletions examples/rspack-starter/mock/sse.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createSSEStream, defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
url: '/api/sse',
response(req, res) {
const sse = createSSEStream(req, res)
let count = 0
const timer = setInterval(() => {
sse.write({
event: 'count',
data: { count: ++count },
})
if (count >= 10) {
sse.write({ event: 'close' })
sse.end()
clearInterval(timer)
}
}, 1000)
},
})
14 changes: 14 additions & 0 deletions examples/rspack-starter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,19 @@ function webSocketMock() {
}, 3000)
}

function eventSourceMock() {
const es = new EventSource('/api/sse')
es.addEventListener('count', (e) => {
// eslint-disable-next-line no-console
console.log('sse count ->', e.data)
})
es.addEventListener('close', () => {
es.close()
// eslint-disable-next-line no-console
console.log('sse closed')
})
}

bootstrap()
webSocketMock()
eventSourceMock()
87 changes: 87 additions & 0 deletions src/core/sse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http'
import { Transform } from 'node:stream'

export interface SSEMessage {
data?: string | object
comment?: string
event?: string
id?: string
retry?: number
}

interface WriteHeaders {
writeHead?: (statusCode: number, headers?: OutgoingHttpHeaders) => WriteHeaders
flushHeaders?: () => void
}

export type HeaderStream = NodeJS.WritableStream & WriteHeaders

/**
* Transforms "messages" to W3C event stream content.
* See https://html.spec.whatwg.org/multipage/server-sent-events.html
* A message is an object with one or more of the following properties:
* - data (String or object, which gets turned into JSON)
* - event
* - id
* - retry
* - comment
*
* If constructed with a HTTP Request, it will optimise the socket for streaming.
* If this stream is piped to an HTTP Response, it will set appropriate headers.
*/
class SSEStream extends Transform {
constructor(req: IncomingMessage) {
super({ objectMode: true })
req.socket.setKeepAlive(true)
req.socket.setNoDelay(true)
req.socket.setTimeout(0)
}

pipe<T extends HeaderStream>(destination: T, options?: { end?: boolean }): T {
if (destination.writeHead) {
destination.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Transfer-Encoding': 'identity',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
})
destination.flushHeaders?.()
}
// Some clients (Safari) don't trigger onopen until the first frame is received.
destination.write(':ok\n\n')
return super.pipe(destination, options)
}

_transform(message: SSEMessage, encoding: string, callback: (error?: (Error | null), data?: any) => void) {
if (message.comment)
this.push(`: ${message.comment}\n`)
if (message.event)
this.push(`event: ${message.event}\n`)
if (message.id)
this.push(`id: ${message.id}\n`)
if (message.retry)
this.push(`retry: ${message.retry}\n`)
if (message.data)
this.push(dataString(message.data))
this.push('\n')
callback()
}

write(message: SSEMessage, encoding?: BufferEncoding, cb?: (error: Error | null | undefined) => void): boolean
write(message: SSEMessage, cb?: (error: Error | null | undefined) => void): boolean
write(message: SSEMessage, ...args: any[]): boolean {
return super.write(message, ...args)
}
}

function dataString(data: string | object): string {
if (typeof data === 'object')
return dataString(JSON.stringify(data))
return data.split(/\r\n|\r|\n/).map(line => `data: ${line}\n`).join('')
}

export function createSSEStream(req: IncomingMessage, res: ServerResponse): SSEStream {
const sse = new SSEStream(req)
sse.pipe(res)
return sse
}
1 change: 1 addition & 0 deletions src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './core/defineMock'
export * from './core/defineMockData'
export * from './core/sse'
export type * from './types'

0 comments on commit 161cafa

Please sign in to comment.