Skip to content

Commit

Permalink
feat!: provide onError, onComment, and onRetry callbacks
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The parser now takes an object of callbacks instead of an `onParse` callback. This means you do not have to check the type of the event in the `onEvent` callback, but instead provide separate callbacks for each event type.

BREAKING CHANGE: The `ParsedEvent` type has been renamed to `EventSourceMessage` and the `type` attribute has been removed.

BREAKING CHANGE: The `EventSourceCallback` type has been removed in favor of the `ParserCallbacks` interface.

BREAKING CHNAGE: The `ReconnectInterval` type has been removed in favor of providing the interval directly to the `onRetry` callback.

BREAKING CHANGE: The `ParseEvent` type has been removed in favor of providing separate callbacks for each event type.

BREAKING CHANGE: The parser has been rewritten to be more specification compliant. Certain _rare_ edge cases may now be handled differently. Mixed CRLF and LF line endings will now be handled correctly. `retry` fields now have to be completely valid integers to be parsed.
  • Loading branch information
rexxars committed Oct 19, 2024
1 parent fe2896f commit d76aeb9
Show file tree
Hide file tree
Showing 15 changed files with 4,626 additions and 11,232 deletions.
92 changes: 92 additions & 0 deletions MIGRATE-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# How to migrate from v2 to v3

## If importing from `eventsource-parser`

The parser now takes an object of callbacks instead of only an `onParse` callback. This means you do not have to check the type of the event in the `onEvent` callback, but instead provide separate callbacks for each event type:

```diff
-import {createParser, type ParsedEvent, type ReconnectInterval} from 'eventsource-parser'
+import {createParser, type EventSourceMessage} from 'eventsource-parser'

-const parser = createParser((event: ParsedEvent | ReconnectInterval) => {
- if (event.type === 'event') {
- // …handle event…
- } else if (event.type === 'reconnect-interval') {
- // …handle retry interval change…
- }
-})
+const parser = createParser({
+ onEvent: (event: EventSourceMessage) => {
+ // …handle event…
+ },
+ onRetry: (interval: number) => {
+ // …handle retry interval change…
+ }
+})
```

The parser also now has a `onError` callback that you can use to handle parse errors, as well as an `onComment` callback that you can use to handle comments:

```ts
const parser = createParser({
onEvent: (event: EventSourceMessage) => {
// …handle event…
},
onRetry: (interval: number) => {
// …handle retry interval change…
},
onError: (error: Error) => {
// …handle parse error…
},
onComment: (comment: string) => {
// …handle comment…
},
})
```

Renamed types:

- `ParsedEvent` => `EventSourceMessage` (and `type` property is removed)

Removed types:

- `EventSourceParseCallback` - replaced with `ParserCallbacks` interface (`onEvent` property)
- `ReconnectInterval` - no longer needed, as the `onRetry` callback now provides the interval directly
- `ParseEvent` - no longer needed - the different event types are now handled by separate callbacks

## If using the `TransformStream` variant

No change is neccessary, but you can now subscribe to changes in the retry interval by providing a callback to the `onRetry` option when creating the stream:

```ts
const stream = new EventSourceParserStream({
onRetry: (interval: number) => {
// …handle retry interval change…
},
})
```

There is also a new option to specify how parse should be handled - by default it will ignore them, but you can choose to terminate the stream or handle it manually:

```ts
const stream = new EventSourceParserStream({
onError: (error: Error) => {
// …handle parse error…
},
})

// …or…
const stream = new EventSourceParserStream({
onError: 'terminate',
})
```

Lastly, if you're interested in any comments that are encountered during parsing, you can provide a callback to the `onComment` option:

```ts
const stream = new EventSourceParserStream({
onComment: (comment: string) => {
// …handle comment…
},
})
```
91 changes: 79 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ A streaming parser for [server-sent events/eventsource](https://developer.mozill

You create an instance of the parser, and _feed_ it chunks of data - partial or complete, and the parse emits parsed messages once it receives a complete message. A [TransformStream variant](#stream-usage) is also available for environments that support it (modern browsers, Node 18 and higher).

Other modules in the EventSource family:

- [eventsource-client](https://github.com/rexxars/eventsource-client): modern, feature rich eventsource client for browsers, node.js, bun, deno and other modern JavaScript environments.
- [eventsource-encoder](https://github.com/rexxars/eventsource-encoder): encodes messages in the EventSource/Server-Sent Events format.
- [eventsource](https://github.com/eventsource/eventsource): Node.js polyfill for the WhatWG EventSource API.

> [!NOTE]
> Migrating from eventsource-parser 1.x/2.x? See the [migration guide](./MIGRATE-v3.md).
## Installation

```bash
Expand All @@ -15,20 +24,16 @@ npm install --save eventsource-parser
## Usage

```ts
import {createParser, type ParsedEvent, type ReconnectInterval} from 'eventsource-parser'

function onParse(event: ParsedEvent | ReconnectInterval) {
if (event.type === 'event') {
console.log('Received event!')
console.log('id: %s', event.id || '<none>')
console.log('name: %s', event.name || '<none>')
console.log('data: %s', event.data)
} else if (event.type === 'reconnect-interval') {
console.log('We should set reconnect interval to %d milliseconds', event.value)
}
import {createParser, type EventSourceMessage} from 'eventsource-parser'

function onEvent(event: EventSourceMessage) {
console.log('Received event!')
console.log('id: %s', event.id || '<none>')
console.log('name: %s', event.name || '<none>')
console.log('data: %s', event.data)
}

const parser = createParser(onParse)
const parser = createParser({onEvent})
const sseStream = getSomeReadableStream()

for await (const chunk of sseStream) {
Expand All @@ -40,6 +45,68 @@ parser.reset()
console.log('Done!')
```

### Retry intervals

If the server sends a `retry` field in the event stream, the parser will call any `onRetry` callback specified to the `createParser` function:

```ts
const parser = createParser({
onRetry(retryInterval) {
console.log('Server requested retry interval of %dms', retryInterval)
},
onEvent(event) {
//
},
})
```

### Parse errors

If the parser encounters an error while parsing, it will call any `onError` callback provided to the `createParser` function:

```ts
import {type ParseError} from 'eventsource-parser'

const parser = createParser({
onError(error: ParseError) {
console.error('Error parsing event:', error)
if (error.type === 'invalid-field') {
console.error('Field name:', error.field)
console.error('Field value:', error.value)
console.error('Line:', error.line)
} else if (error.type === 'invalid-retry') {
console.error('Invalid retry interval:', error.value)
}
},
onEvent(event) {
//
},
})
```

Note that `invalid-field` errors will usually be called for any invalid data - not only data shaped as `field: value`. This is because the EventSource specification says to treat anything prior to a `:` as the field name. Use the `error.line` property to get the full line that caused the error.

> [!NOTE]
> When encountering the end of a stream, calling `.reset({consume: true})` on the parser to flush any remaining data and reset the parser state. This will trigger the `onError` callback if the pending data is not a valid event.
### Comments

The parser will ignore comments (lines starting with `:`) by default. If you want to handle comments, you can provide an `onComment` callback to the `createParser` function:

```ts
const parser = createParser({
onComment(comment) {
console.log('Received comment:', comment)
},
onEvent(event) {
//
},
})
```

> [!NOTE]
> Leading whitespace is not stripped from comments, eg `: comment` will give ` comment` as the comment value, not `comment` (note the leading space).
## Stream usage

```ts
Expand Down
Loading

0 comments on commit d76aeb9

Please sign in to comment.