@standardserver/core 0.0.12 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -0
- package/dist/index.d.mts +81 -4
- package/dist/index.d.ts +81 -4
- package/dist/index.mjs +208 -2
- package/package.json +4 -8
- package/dist/event-stream/index.d.mts +0 -79
- package/dist/event-stream/index.d.ts +0 -79
- package/dist/event-stream/index.mjs +0 -209
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# @standardserver/core
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="https://codecov.io/gh/middleapi/standardserver">
|
|
5
|
+
<img alt="codecov" src="https://codecov.io/gh/middleapi/standardserver/branch/main/graph/badge.svg">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@standardserver/core">
|
|
8
|
+
<img alt="weekly downloads" src="https://img.shields.io/npm/dw/%40standardserver%2Fcore?logo=npm" />
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://github.com/middleapi/standardserver/blob/main/LICENSE">
|
|
11
|
+
<img alt="MIT License" src="https://img.shields.io/github/license/middleapi/standardserver?logo=open-source-initiative" />
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://discord.gg/TXEbwRBvQn">
|
|
14
|
+
<img alt="Discord" src="https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff" />
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://deepwiki.com/middleapi/standardserver">
|
|
17
|
+
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
|
|
18
|
+
</a>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
`@standardserver/core` is the shared contract package for Standard Server.
|
|
22
|
+
|
|
23
|
+
Standard Server provides a unified interface for client-server communication across HTTP and message-based transports. It lets you keep handler and client code transport-agnostic by working with the same request, response, body, and streaming abstractions whether the transport is Fetch, Node.js HTTP, or a peer-style message channel.
|
|
24
|
+
|
|
25
|
+
This package is the foundation of that model. It defines the core request and response types, shared runtime validators, small utility helpers, and event stream (SSE) helpers.
|
|
26
|
+
|
|
27
|
+
## Entry Points
|
|
28
|
+
|
|
29
|
+
| Entry point | Purpose |
|
|
30
|
+
| ---------------------- | -------------------------------------------------------- |
|
|
31
|
+
| `@standardserver/core` | Shared request/response types, utilities, and validators |
|
|
32
|
+
|
|
33
|
+
## Request and response types
|
|
34
|
+
|
|
35
|
+
The main entry point exposes four transport-agnostic shapes:
|
|
36
|
+
|
|
37
|
+
| Export | Description |
|
|
38
|
+
| ---------------------- | --------------------------------------------------------------- |
|
|
39
|
+
| `StandardRequest` | Eager request object with a parsed `body` |
|
|
40
|
+
| `StandardLazyRequest` | Request object with `resolveBody(hint?)` for lazy body parsing |
|
|
41
|
+
| `StandardResponse` | Eager response object with a parsed `body` |
|
|
42
|
+
| `StandardLazyResponse` | Response object with `resolveBody(hint?)` for lazy body parsing |
|
|
43
|
+
|
|
44
|
+
Supporting primitives:
|
|
45
|
+
|
|
46
|
+
| Export | Description |
|
|
47
|
+
| ------------------ | ------------------------------------------------------------- | -------- | ----------- |
|
|
48
|
+
| `StandardMethod` | Common HTTP verbs plus any custom string value |
|
|
49
|
+
| `StandardUrl` | A request URL that must start with `/` and exclude the origin |
|
|
50
|
+
| `StandardHeaders` | `Record<string, string | string[] | undefined>` |
|
|
51
|
+
| `StandardBodyHint` | Parsing hint for lazy body resolution |
|
|
52
|
+
| `StandardBody` | Shared body union used by requests and responses |
|
|
53
|
+
|
|
54
|
+
By convention, adapters normalize headers to lowercase keys. `signal` is part of `StandardRequest` only and is used to propagate request cancellation.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import type { StandardLazyRequest, StandardResponse } from '@standardserver/core'
|
|
58
|
+
|
|
59
|
+
export async function handle(request: StandardLazyRequest): Promise<StandardResponse> {
|
|
60
|
+
const body = await request.resolveBody()
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
status: 200,
|
|
64
|
+
headers: { 'content-type': 'application/json' },
|
|
65
|
+
body: {
|
|
66
|
+
ok: true,
|
|
67
|
+
method: request.method,
|
|
68
|
+
url: request.url,
|
|
69
|
+
received: body,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Body hints and body values
|
|
76
|
+
|
|
77
|
+
`StandardBodyHint` and `StandardBody` describe the shared body contract used across adapters:
|
|
78
|
+
|
|
79
|
+
| Hint | `StandardBody` value | Typical content type | Notes |
|
|
80
|
+
| ------------------- | ------------------------------ | ----------------------------------- | ----------------------------------------------------- |
|
|
81
|
+
| `json` | `unknown` | `application/json` | Primitives, objects, and arrays |
|
|
82
|
+
| `form-data` | `FormData` | `multipart/form-data` | Multipart form submissions |
|
|
83
|
+
| `url-search-params` | `URLSearchParams` | `application/x-www-form-urlencoded` | URL-encoded forms |
|
|
84
|
+
| `event-stream` | `AsyncIteratorObject<unknown>` | `text/event-stream` | Server-Sent Events (SSE) |
|
|
85
|
+
| `octet-stream` | `ReadableStream<Uint8Array>` | any | Binary payloads |
|
|
86
|
+
| `file` | `File` | any | Fixed-size binary payloads for both `File` and `Blob` |
|
|
87
|
+
| `none` | `undefined` | | Empty body |
|
|
88
|
+
|
|
89
|
+
### Resolving Body
|
|
90
|
+
|
|
91
|
+
`resolveBody(hint?)` determines how to parse the body using the following priority:
|
|
92
|
+
|
|
93
|
+
1. If the `standard-server` header is present, use it as the `StandardBodyHint`.
|
|
94
|
+
2. Otherwise, if `hint?` is provided, use it as the `StandardBodyHint`.
|
|
95
|
+
3. Otherwise, if `content-type` is one of the common types, parse accordingly.
|
|
96
|
+
4. Otherwise, if `content-length` exists, treat the body as `file`; if not, treat it as `octet-stream`.
|
|
97
|
+
|
|
98
|
+
For efficient communication, set the `standard-server` header to explicitly hint the body type, especially for file or binary streaming. For example, if you upload a file with a common `content-type` such as `application/json` but omit the `standard-server` header, the server may interpret it as JSON and parse it unexpectedly.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const response = await fetch('/upload', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'content-type': 'application/json',
|
|
105
|
+
'standard-server': 'file', // <- hint the body type to avoid misinterpretation
|
|
106
|
+
},
|
|
107
|
+
body: new Blob(['{"message": "Hello, world!"}'], { type: 'application/json' }),
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Utilities
|
|
112
|
+
|
|
113
|
+
The main entry point also exports a small set of helpers for common header and URL operations.
|
|
114
|
+
|
|
115
|
+
### Content-Disposition helpers
|
|
116
|
+
|
|
117
|
+
Use `generateContentDisposition()` to produce a safe `Content-Disposition` value and `getFilenameFromContentDisposition()` to read a filename back from an existing header.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import {
|
|
121
|
+
generateContentDisposition,
|
|
122
|
+
getFilenameFromContentDisposition,
|
|
123
|
+
} from '@standardserver/core'
|
|
124
|
+
|
|
125
|
+
const disposition = generateContentDisposition('report "Q2".csv')
|
|
126
|
+
// inline; filename="report \"Q2\".csv"; filename*=utf-8''report%20%22Q2%22.csv
|
|
127
|
+
|
|
128
|
+
const filename = getFilenameFromContentDisposition(disposition)
|
|
129
|
+
// 'report "Q2".csv'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`generateContentDisposition()` preserves an ASCII-safe `filename="..."` value and also emits `filename*=` for UTF-8 aware clients.
|
|
133
|
+
|
|
134
|
+
### Header helpers
|
|
135
|
+
|
|
136
|
+
`mergeStandardHeaders()` combines two `StandardHeaders` objects while preserving duplicate values, and `flattenStandardHeader()` turns a single header value into a plain string when needed.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import {
|
|
140
|
+
flattenStandardHeader,
|
|
141
|
+
mergeStandardHeaders,
|
|
142
|
+
} from '@standardserver/core'
|
|
143
|
+
|
|
144
|
+
const headers = mergeStandardHeaders(
|
|
145
|
+
{ 'accept': 'application/json', 'set-cookie': ['a=1'] },
|
|
146
|
+
{ 'set-cookie': 'b=2', 'vary': 'accept', 'warning': undefined },
|
|
147
|
+
)
|
|
148
|
+
// {
|
|
149
|
+
// accept: 'application/json',
|
|
150
|
+
// 'set-cookie': ['a=1', 'b=2'],
|
|
151
|
+
// vary: 'accept',
|
|
152
|
+
// }
|
|
153
|
+
|
|
154
|
+
const cookieHeader = flattenStandardHeader(headers['set-cookie'])
|
|
155
|
+
// 'a=1, b=2'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### URL parsing
|
|
159
|
+
|
|
160
|
+
`parseStandardUrl()` splits a `StandardUrl` into `[pathname, search, hash]` without requiring a full origin.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { parseStandardUrl } from '@standardserver/core'
|
|
164
|
+
|
|
165
|
+
const [pathname, search, hash] = parseStandardUrl('/users/123?tab=settings#profile')
|
|
166
|
+
// pathname => '/users/123'
|
|
167
|
+
// search => '?tab=settings'
|
|
168
|
+
// hash => '#profile'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Validators
|
|
172
|
+
|
|
173
|
+
Runtime type guards are useful when requests or responses cross process, transport, or message boundaries.
|
|
174
|
+
|
|
175
|
+
| Export | Checks |
|
|
176
|
+
| ---------------------- | ------------------------------------------------------ |
|
|
177
|
+
| `isStandardMethod()` | Any string value |
|
|
178
|
+
| `isStandardUrl()` | A string starting with `/` |
|
|
179
|
+
| `isStandardHeaders()` | Object values are `string`, `string[]`, or `undefined` |
|
|
180
|
+
| `isStandardRequest()` | |
|
|
181
|
+
| `isStandardResponse()` | |
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { isStandardRequest } from '@standardserver/core'
|
|
185
|
+
|
|
186
|
+
export function expectStandardRequest(input: unknown) {
|
|
187
|
+
if (!isStandardRequest(input)) {
|
|
188
|
+
throw new TypeError('Expected a StandardRequest-compatible value')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return input
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Event-Stream Helpers
|
|
196
|
+
|
|
197
|
+
Use Event-Stream Helpers when you need explicit SSE encoding, decoding, or metadata handling.
|
|
198
|
+
|
|
199
|
+
### Message types and codecs
|
|
200
|
+
|
|
201
|
+
The event-stream entry point exposes:
|
|
202
|
+
|
|
203
|
+
- `EventStreamMessageMeta` for `id`, `retry`, and `comments`
|
|
204
|
+
- `EventStreamMessage` for complete SSE messages
|
|
205
|
+
- `encodeEventStreamMessage()` and `decodeEventStreamMessage()` for single-message codec operations
|
|
206
|
+
- `EventStreamDecoder` and `EventStreamDecoderStream` for chunked stream decoding
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import {
|
|
210
|
+
decodeEventStreamMessage,
|
|
211
|
+
encodeEventStreamMessage,
|
|
212
|
+
} from '@standardserver/core'
|
|
213
|
+
|
|
214
|
+
const encoded = encodeEventStreamMessage({
|
|
215
|
+
comments: ['bootstrap'],
|
|
216
|
+
event: 'message',
|
|
217
|
+
id: '42',
|
|
218
|
+
retry: 3000,
|
|
219
|
+
data: 'hello\nworld',
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const decoded = decodeEventStreamMessage(encoded)
|
|
223
|
+
// {
|
|
224
|
+
// comments: ['bootstrap'],
|
|
225
|
+
// event: 'message',
|
|
226
|
+
// id: '42',
|
|
227
|
+
// retry: 3000,
|
|
228
|
+
// data: 'hello\nworld',
|
|
229
|
+
// }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
For streaming decode, pipe text chunks through `EventStreamDecoderStream`:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { EventStreamDecoderStream } from '@standardserver/core'
|
|
236
|
+
|
|
237
|
+
const messages = response.body!
|
|
238
|
+
.pipeThrough(new TextDecoderStream())
|
|
239
|
+
.pipeThrough(new EventStreamDecoderStream())
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Iterator metadata helpers
|
|
243
|
+
|
|
244
|
+
`StandardBody` uses async iterators for event-stream bodies. To attach SSE metadata to a yielded value without changing its visible shape, use `withEventIteratorEventMeta()`.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import type { StandardResponse } from '@standardserver/core'
|
|
248
|
+
import {
|
|
249
|
+
unwrapEventIteratorEvent,
|
|
250
|
+
withEventIteratorEventMeta,
|
|
251
|
+
} from '@standardserver/core'
|
|
252
|
+
|
|
253
|
+
const eventValue = withEventIteratorEventMeta(
|
|
254
|
+
{ message: 'hello' },
|
|
255
|
+
{ id: '1', retry: 3000, comments: ['bootstrap'] },
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const [data, meta] = unwrapEventIteratorEvent(eventValue)
|
|
259
|
+
// data => { message: 'hello' }
|
|
260
|
+
// meta => { id: '1', retry: 3000, comments: ['bootstrap'] }
|
|
261
|
+
|
|
262
|
+
const response: StandardResponse = {
|
|
263
|
+
status: 200,
|
|
264
|
+
headers: {},
|
|
265
|
+
async* body() {
|
|
266
|
+
yield eventValue
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
> [!WARNING]
|
|
272
|
+
> Metadata is validated before it is attached: `id`, `event`, and comments must not contain line breaks, and `retry` must be a non-negative integer.
|
|
273
|
+
|
|
274
|
+
### Errors and low-level assertions
|
|
275
|
+
|
|
276
|
+
The subpath also exports:
|
|
277
|
+
|
|
278
|
+
- `EventStreamEncoderError` for invalid outbound SSE messages
|
|
279
|
+
- `EventStreamDecoderError` for incomplete or invalid inbound stream decoding
|
|
280
|
+
- `EventIteratorErrorEvent` for wrapping structured event-stream error payloads in an `Error`
|
|
281
|
+
- `assertEventStreamMessageId()`, `assertEventStreamMessageName()`, `assertEventStreamMessageRetry()`, and `assertEventStreamMessageComment()` for low-level validation when building custom SSE tooling
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { EventIteratorErrorEvent } from '@standardserver/core'
|
|
285
|
+
|
|
286
|
+
const error = new EventIteratorErrorEvent(
|
|
287
|
+
{ code: 'E_STREAM', detail: 'Connection lost' },
|
|
288
|
+
{ message: 'stream error' },
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
error.message
|
|
292
|
+
// 'stream error'
|
|
293
|
+
|
|
294
|
+
error.data
|
|
295
|
+
// { code: 'E_STREAM', detail: 'Connection lost' }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Learn more
|
|
299
|
+
|
|
300
|
+
For the higher-level project overview, see the root [Standard Server README](../../README.md).
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
+
interface EventStreamMessageMeta {
|
|
2
|
+
/**
|
|
3
|
+
* Event identifier, sent back by the client as `lastEventId` for reconnection attempts.
|
|
4
|
+
*
|
|
5
|
+
* @warning id cannot contain newline characters (`\n`)
|
|
6
|
+
*/
|
|
7
|
+
id?: string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The number of milliseconds the client should wait before attempting to reconnect.
|
|
10
|
+
*/
|
|
11
|
+
retry?: number | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Comments associated with the event.
|
|
14
|
+
*
|
|
15
|
+
* @warning Comments must not contain newline characters (`\n`).
|
|
16
|
+
*/
|
|
17
|
+
comments?: string[] | undefined;
|
|
18
|
+
}
|
|
19
|
+
interface EventStreamMessage extends EventStreamMessageMeta {
|
|
20
|
+
/**
|
|
21
|
+
* Event name (e.g., `message`, `error`).
|
|
22
|
+
*/
|
|
23
|
+
event?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Event data, typically JSON-encoded.
|
|
26
|
+
*/
|
|
27
|
+
data?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare function decodeEventStreamMessage(encoded: string): EventStreamMessage;
|
|
31
|
+
declare class EventStreamDecoder {
|
|
32
|
+
private readonly onEvent;
|
|
33
|
+
private incomplete;
|
|
34
|
+
private trailingCR;
|
|
35
|
+
constructor(onEvent: (event: EventStreamMessage) => void);
|
|
36
|
+
feed(chunk: string): void;
|
|
37
|
+
end(): void;
|
|
38
|
+
}
|
|
39
|
+
declare class EventStreamDecoderStream extends TransformStream<string, EventStreamMessage> {
|
|
40
|
+
constructor();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare function assertEventStreamMessageId(id: string): void;
|
|
44
|
+
declare function assertEventStreamMessageName(event: string): void;
|
|
45
|
+
declare function assertEventStreamMessageRetry(retry: number): void;
|
|
46
|
+
declare function assertEventStreamMessageComment(comment: string): void;
|
|
47
|
+
declare function encodeEventStreamMessageData(data: string | undefined): string;
|
|
48
|
+
declare function encodeEventStreamMessageComments(comments: readonly string[] | undefined): string;
|
|
49
|
+
declare function encodeEventStreamMessage(message: EventStreamMessage): string;
|
|
50
|
+
|
|
51
|
+
declare class EventStreamEncoderError extends TypeError {
|
|
52
|
+
}
|
|
53
|
+
declare class EventStreamDecoderError extends TypeError {
|
|
54
|
+
}
|
|
55
|
+
interface EventIteratorErrorEventOptions extends ErrorOptions {
|
|
56
|
+
message?: string;
|
|
57
|
+
}
|
|
58
|
+
declare class EventIteratorErrorEvent extends Error {
|
|
59
|
+
readonly data: unknown;
|
|
60
|
+
constructor(data: unknown, options?: EventIteratorErrorEventOptions);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
declare const EVENT_ITERATOR_EVENT_META_SYMBOL: unique symbol;
|
|
64
|
+
declare const EVENT_ITERATOR_EVENT_SOURCE_SYMBOL: unique symbol;
|
|
65
|
+
/**
|
|
66
|
+
* Returns a new iterator *event value* with attached, validated metadata.
|
|
67
|
+
*/
|
|
68
|
+
declare function withEventIteratorEventMeta<T extends object>(container: T, meta: EventStreamMessageMeta): T;
|
|
69
|
+
/**
|
|
70
|
+
* Unwraps an iterator event value and extracts its associated metadata.
|
|
71
|
+
*/
|
|
72
|
+
declare function unwrapEventIteratorEvent<T>(container: T): [data: T, meta: EventStreamMessageMeta | undefined];
|
|
73
|
+
/**
|
|
74
|
+
* Retrieves metadata attached to a single iterator event value.
|
|
75
|
+
*/
|
|
76
|
+
declare function getEventIteratorEventMeta(container: unknown): EventStreamMessageMeta | undefined;
|
|
77
|
+
|
|
1
78
|
type StandardMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD' | (string & {});
|
|
2
79
|
type StandardUrl = `/${string}` | `/${string}?${string}` | `/${string}#${string}` | `/${string}?${string}#${string}`;
|
|
3
80
|
interface StandardHeaders {
|
|
@@ -21,7 +98,7 @@ interface StandardRequest {
|
|
|
21
98
|
/**
|
|
22
99
|
* The body has been parsed based on the content headers.
|
|
23
100
|
*/
|
|
24
|
-
body
|
|
101
|
+
body?: StandardBody | undefined;
|
|
25
102
|
/**
|
|
26
103
|
* An AbortSignal to communicate aborting of request.
|
|
27
104
|
*/
|
|
@@ -50,7 +127,7 @@ interface StandardResponse {
|
|
|
50
127
|
/**
|
|
51
128
|
* The body has been parsed based on the content headers.
|
|
52
129
|
*/
|
|
53
|
-
body
|
|
130
|
+
body?: StandardBody | undefined;
|
|
54
131
|
}
|
|
55
132
|
interface StandardLazyResponse extends Omit<StandardResponse, 'body'> {
|
|
56
133
|
/**
|
|
@@ -80,5 +157,5 @@ declare function isStandardHeaders(maybe: unknown): maybe is StandardHeaders;
|
|
|
80
157
|
declare function isStandardRequest(maybe: unknown): maybe is StandardRequest;
|
|
81
158
|
declare function isStandardResponse(maybe: unknown): maybe is StandardResponse;
|
|
82
159
|
|
|
83
|
-
export { flattenStandardHeader, generateContentDisposition, getFilenameFromContentDisposition, isStandardHeaders, isStandardMethod, isStandardRequest, isStandardResponse, isStandardUrl, mergeStandardHeaders, parseStandardUrl };
|
|
84
|
-
export type { StandardBody, StandardBodyHint, StandardHeaders, StandardLazyRequest, StandardLazyResponse, StandardMethod, StandardRequest, StandardResponse, StandardUrl };
|
|
160
|
+
export { EVENT_ITERATOR_EVENT_META_SYMBOL, EVENT_ITERATOR_EVENT_SOURCE_SYMBOL, EventIteratorErrorEvent, EventStreamDecoder, EventStreamDecoderError, EventStreamDecoderStream, EventStreamEncoderError, assertEventStreamMessageComment, assertEventStreamMessageId, assertEventStreamMessageName, assertEventStreamMessageRetry, decodeEventStreamMessage, encodeEventStreamMessage, encodeEventStreamMessageComments, encodeEventStreamMessageData, flattenStandardHeader, generateContentDisposition, getEventIteratorEventMeta, getFilenameFromContentDisposition, isStandardHeaders, isStandardMethod, isStandardRequest, isStandardResponse, isStandardUrl, mergeStandardHeaders, parseStandardUrl, unwrapEventIteratorEvent, withEventIteratorEventMeta };
|
|
161
|
+
export type { EventIteratorErrorEventOptions, EventStreamMessage, EventStreamMessageMeta, StandardBody, StandardBodyHint, StandardHeaders, StandardLazyRequest, StandardLazyResponse, StandardMethod, StandardRequest, StandardResponse, StandardUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
+
interface EventStreamMessageMeta {
|
|
2
|
+
/**
|
|
3
|
+
* Event identifier, sent back by the client as `lastEventId` for reconnection attempts.
|
|
4
|
+
*
|
|
5
|
+
* @warning id cannot contain newline characters (`\n`)
|
|
6
|
+
*/
|
|
7
|
+
id?: string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The number of milliseconds the client should wait before attempting to reconnect.
|
|
10
|
+
*/
|
|
11
|
+
retry?: number | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Comments associated with the event.
|
|
14
|
+
*
|
|
15
|
+
* @warning Comments must not contain newline characters (`\n`).
|
|
16
|
+
*/
|
|
17
|
+
comments?: string[] | undefined;
|
|
18
|
+
}
|
|
19
|
+
interface EventStreamMessage extends EventStreamMessageMeta {
|
|
20
|
+
/**
|
|
21
|
+
* Event name (e.g., `message`, `error`).
|
|
22
|
+
*/
|
|
23
|
+
event?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Event data, typically JSON-encoded.
|
|
26
|
+
*/
|
|
27
|
+
data?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare function decodeEventStreamMessage(encoded: string): EventStreamMessage;
|
|
31
|
+
declare class EventStreamDecoder {
|
|
32
|
+
private readonly onEvent;
|
|
33
|
+
private incomplete;
|
|
34
|
+
private trailingCR;
|
|
35
|
+
constructor(onEvent: (event: EventStreamMessage) => void);
|
|
36
|
+
feed(chunk: string): void;
|
|
37
|
+
end(): void;
|
|
38
|
+
}
|
|
39
|
+
declare class EventStreamDecoderStream extends TransformStream<string, EventStreamMessage> {
|
|
40
|
+
constructor();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare function assertEventStreamMessageId(id: string): void;
|
|
44
|
+
declare function assertEventStreamMessageName(event: string): void;
|
|
45
|
+
declare function assertEventStreamMessageRetry(retry: number): void;
|
|
46
|
+
declare function assertEventStreamMessageComment(comment: string): void;
|
|
47
|
+
declare function encodeEventStreamMessageData(data: string | undefined): string;
|
|
48
|
+
declare function encodeEventStreamMessageComments(comments: readonly string[] | undefined): string;
|
|
49
|
+
declare function encodeEventStreamMessage(message: EventStreamMessage): string;
|
|
50
|
+
|
|
51
|
+
declare class EventStreamEncoderError extends TypeError {
|
|
52
|
+
}
|
|
53
|
+
declare class EventStreamDecoderError extends TypeError {
|
|
54
|
+
}
|
|
55
|
+
interface EventIteratorErrorEventOptions extends ErrorOptions {
|
|
56
|
+
message?: string;
|
|
57
|
+
}
|
|
58
|
+
declare class EventIteratorErrorEvent extends Error {
|
|
59
|
+
readonly data: unknown;
|
|
60
|
+
constructor(data: unknown, options?: EventIteratorErrorEventOptions);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
declare const EVENT_ITERATOR_EVENT_META_SYMBOL: unique symbol;
|
|
64
|
+
declare const EVENT_ITERATOR_EVENT_SOURCE_SYMBOL: unique symbol;
|
|
65
|
+
/**
|
|
66
|
+
* Returns a new iterator *event value* with attached, validated metadata.
|
|
67
|
+
*/
|
|
68
|
+
declare function withEventIteratorEventMeta<T extends object>(container: T, meta: EventStreamMessageMeta): T;
|
|
69
|
+
/**
|
|
70
|
+
* Unwraps an iterator event value and extracts its associated metadata.
|
|
71
|
+
*/
|
|
72
|
+
declare function unwrapEventIteratorEvent<T>(container: T): [data: T, meta: EventStreamMessageMeta | undefined];
|
|
73
|
+
/**
|
|
74
|
+
* Retrieves metadata attached to a single iterator event value.
|
|
75
|
+
*/
|
|
76
|
+
declare function getEventIteratorEventMeta(container: unknown): EventStreamMessageMeta | undefined;
|
|
77
|
+
|
|
1
78
|
type StandardMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD' | (string & {});
|
|
2
79
|
type StandardUrl = `/${string}` | `/${string}?${string}` | `/${string}#${string}` | `/${string}?${string}#${string}`;
|
|
3
80
|
interface StandardHeaders {
|
|
@@ -21,7 +98,7 @@ interface StandardRequest {
|
|
|
21
98
|
/**
|
|
22
99
|
* The body has been parsed based on the content headers.
|
|
23
100
|
*/
|
|
24
|
-
body
|
|
101
|
+
body?: StandardBody | undefined;
|
|
25
102
|
/**
|
|
26
103
|
* An AbortSignal to communicate aborting of request.
|
|
27
104
|
*/
|
|
@@ -50,7 +127,7 @@ interface StandardResponse {
|
|
|
50
127
|
/**
|
|
51
128
|
* The body has been parsed based on the content headers.
|
|
52
129
|
*/
|
|
53
|
-
body
|
|
130
|
+
body?: StandardBody | undefined;
|
|
54
131
|
}
|
|
55
132
|
interface StandardLazyResponse extends Omit<StandardResponse, 'body'> {
|
|
56
133
|
/**
|
|
@@ -80,5 +157,5 @@ declare function isStandardHeaders(maybe: unknown): maybe is StandardHeaders;
|
|
|
80
157
|
declare function isStandardRequest(maybe: unknown): maybe is StandardRequest;
|
|
81
158
|
declare function isStandardResponse(maybe: unknown): maybe is StandardResponse;
|
|
82
159
|
|
|
83
|
-
export { flattenStandardHeader, generateContentDisposition, getFilenameFromContentDisposition, isStandardHeaders, isStandardMethod, isStandardRequest, isStandardResponse, isStandardUrl, mergeStandardHeaders, parseStandardUrl };
|
|
84
|
-
export type { StandardBody, StandardBodyHint, StandardHeaders, StandardLazyRequest, StandardLazyResponse, StandardMethod, StandardRequest, StandardResponse, StandardUrl };
|
|
160
|
+
export { EVENT_ITERATOR_EVENT_META_SYMBOL, EVENT_ITERATOR_EVENT_SOURCE_SYMBOL, EventIteratorErrorEvent, EventStreamDecoder, EventStreamDecoderError, EventStreamDecoderStream, EventStreamEncoderError, assertEventStreamMessageComment, assertEventStreamMessageId, assertEventStreamMessageName, assertEventStreamMessageRetry, decodeEventStreamMessage, encodeEventStreamMessage, encodeEventStreamMessageComments, encodeEventStreamMessageData, flattenStandardHeader, generateContentDisposition, getEventIteratorEventMeta, getFilenameFromContentDisposition, isStandardHeaders, isStandardMethod, isStandardRequest, isStandardResponse, isStandardUrl, mergeStandardHeaders, parseStandardUrl, unwrapEventIteratorEvent, withEventIteratorEventMeta };
|
|
161
|
+
export type { EventIteratorErrorEventOptions, EventStreamMessage, EventStreamMessageMeta, StandardBody, StandardBodyHint, StandardHeaders, StandardLazyRequest, StandardLazyResponse, StandardMethod, StandardRequest, StandardResponse, StandardUrl };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,210 @@
|
|
|
1
|
-
import { tryDecodeURIComponent, toArray
|
|
1
|
+
import { getOrBind, isTypescriptObject, tryDecodeURIComponent, toArray } from '@standardserver/shared';
|
|
2
|
+
|
|
3
|
+
class EventStreamEncoderError extends TypeError {
|
|
4
|
+
}
|
|
5
|
+
class EventStreamDecoderError extends TypeError {
|
|
6
|
+
}
|
|
7
|
+
class EventIteratorErrorEvent extends Error {
|
|
8
|
+
constructor(data, options = {}) {
|
|
9
|
+
super(options?.message ?? "Error Event", options);
|
|
10
|
+
this.data = data;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const EVENT_STREAM_LINE_DELIMITER_REGEX = /\r\n|[\n\r]/;
|
|
15
|
+
const EVENT_STREAM_DELIMITER_REGEX = /(?:\r\n|\r(?!\n)|\n)(?:\r\n|\r(?!\n)|\n)/;
|
|
16
|
+
const LEADING_WHITESPACE_REGEX = /^\s/;
|
|
17
|
+
function decodeEventStreamMessage(encoded) {
|
|
18
|
+
const lines = encoded.split(EVENT_STREAM_LINE_DELIMITER_REGEX);
|
|
19
|
+
const message = {};
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const index = line.indexOf(":");
|
|
22
|
+
const key = index === -1 ? line : line.slice(0, index);
|
|
23
|
+
const value = index === -1 ? "" : line.slice(index + 1).replace(LEADING_WHITESPACE_REGEX, "");
|
|
24
|
+
if (index === 0) {
|
|
25
|
+
message.comments ??= [];
|
|
26
|
+
message.comments.push(value);
|
|
27
|
+
} else if (key === "data") {
|
|
28
|
+
if (message.data !== void 0) {
|
|
29
|
+
message.data += `
|
|
30
|
+
${value}`;
|
|
31
|
+
} else {
|
|
32
|
+
message.data = value;
|
|
33
|
+
}
|
|
34
|
+
} else if (key === "event") {
|
|
35
|
+
message.event = value;
|
|
36
|
+
} else if (key === "id") {
|
|
37
|
+
message.id = value;
|
|
38
|
+
} else if (key === "retry") {
|
|
39
|
+
const maybeInteger = Number.parseInt(value);
|
|
40
|
+
if (Number.isInteger(maybeInteger) && maybeInteger >= 0 && maybeInteger.toString() === value) {
|
|
41
|
+
message.retry = maybeInteger;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return message;
|
|
46
|
+
}
|
|
47
|
+
class EventStreamDecoder {
|
|
48
|
+
constructor(onEvent) {
|
|
49
|
+
this.onEvent = onEvent;
|
|
50
|
+
}
|
|
51
|
+
incomplete = "";
|
|
52
|
+
trailingCR = false;
|
|
53
|
+
feed(chunk) {
|
|
54
|
+
if (this.trailingCR && chunk.startsWith("\n")) {
|
|
55
|
+
chunk = chunk.slice(1);
|
|
56
|
+
}
|
|
57
|
+
this.trailingCR = chunk.endsWith("\r");
|
|
58
|
+
this.incomplete += chunk;
|
|
59
|
+
const parts = this.incomplete.split(EVENT_STREAM_DELIMITER_REGEX);
|
|
60
|
+
this.incomplete = parts.pop();
|
|
61
|
+
for (const encoded of parts) {
|
|
62
|
+
const message = decodeEventStreamMessage(encoded);
|
|
63
|
+
this.onEvent(message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
end() {
|
|
67
|
+
if (this.incomplete) {
|
|
68
|
+
throw new EventStreamDecoderError("Event Stream ended before complete");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
class EventStreamDecoderStream extends TransformStream {
|
|
73
|
+
constructor() {
|
|
74
|
+
let decoder;
|
|
75
|
+
super({
|
|
76
|
+
start(controller) {
|
|
77
|
+
decoder = new EventStreamDecoder((event) => {
|
|
78
|
+
controller.enqueue(event);
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
transform(chunk) {
|
|
82
|
+
decoder.feed(chunk);
|
|
83
|
+
},
|
|
84
|
+
flush() {
|
|
85
|
+
decoder.end();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const EVENT_STREAM_LINE_ENDING_REGEX = /\r\n|[\n\r]/;
|
|
92
|
+
function containsEventStreamLineBreak(value) {
|
|
93
|
+
return EVENT_STREAM_LINE_ENDING_REGEX.test(value);
|
|
94
|
+
}
|
|
95
|
+
function assertEventStreamMessageId(id) {
|
|
96
|
+
if (containsEventStreamLineBreak(id)) {
|
|
97
|
+
throw new EventStreamEncoderError("Event's id must not contain a carriage return or newline character");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function assertEventStreamMessageName(event) {
|
|
101
|
+
if (containsEventStreamLineBreak(event)) {
|
|
102
|
+
throw new EventStreamEncoderError("Event's event must not contain a carriage return or newline character");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function assertEventStreamMessageRetry(retry) {
|
|
106
|
+
if (!Number.isInteger(retry) || retry < 0) {
|
|
107
|
+
throw new EventStreamEncoderError("Event's retry must be a integer and >= 0");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function assertEventStreamMessageComment(comment) {
|
|
111
|
+
if (containsEventStreamLineBreak(comment)) {
|
|
112
|
+
throw new EventStreamEncoderError("Event's comment must not contain a carriage return or newline character");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function encodeEventStreamMessageData(data) {
|
|
116
|
+
let output = "";
|
|
117
|
+
if (data !== void 0) {
|
|
118
|
+
const lines = data.split(EVENT_STREAM_LINE_ENDING_REGEX);
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
output += `data: ${line}
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return output;
|
|
125
|
+
}
|
|
126
|
+
function encodeEventStreamMessageComments(comments) {
|
|
127
|
+
let output = "";
|
|
128
|
+
for (const comment of comments ?? []) {
|
|
129
|
+
assertEventStreamMessageComment(comment);
|
|
130
|
+
output += `: ${comment}
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
return output;
|
|
134
|
+
}
|
|
135
|
+
function encodeEventStreamMessage(message) {
|
|
136
|
+
let output = "";
|
|
137
|
+
output += encodeEventStreamMessageComments(message.comments);
|
|
138
|
+
if (message.event !== void 0) {
|
|
139
|
+
assertEventStreamMessageName(message.event);
|
|
140
|
+
output += `event: ${message.event}
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
if (message.retry !== void 0) {
|
|
144
|
+
assertEventStreamMessageRetry(message.retry);
|
|
145
|
+
output += `retry: ${message.retry}
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
if (message.id !== void 0) {
|
|
149
|
+
assertEventStreamMessageId(message.id);
|
|
150
|
+
output += `id: ${message.id}
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
output += encodeEventStreamMessageData(message.data);
|
|
154
|
+
output += "\n";
|
|
155
|
+
return output;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const EVENT_ITERATOR_EVENT_META_SYMBOL = Symbol.for("STANDARDSERVER_EVENT_ITERATOR_EVENT_META");
|
|
159
|
+
const EVENT_ITERATOR_EVENT_SOURCE_SYMBOL = Symbol.for("STANDARDSERVER_EVENT_ITERATOR_EVENT_SOURCE");
|
|
160
|
+
function withEventIteratorEventMeta(container, meta) {
|
|
161
|
+
let assertedMeta;
|
|
162
|
+
if (meta.id !== void 0) {
|
|
163
|
+
assertEventStreamMessageId(meta.id);
|
|
164
|
+
assertedMeta ??= {};
|
|
165
|
+
assertedMeta.id = meta.id;
|
|
166
|
+
}
|
|
167
|
+
if (meta.retry !== void 0) {
|
|
168
|
+
assertEventStreamMessageRetry(meta.retry);
|
|
169
|
+
assertedMeta ??= {};
|
|
170
|
+
assertedMeta.retry = meta.retry;
|
|
171
|
+
}
|
|
172
|
+
if (meta.comments !== void 0) {
|
|
173
|
+
for (const comment of meta.comments) {
|
|
174
|
+
assertEventStreamMessageComment(comment);
|
|
175
|
+
}
|
|
176
|
+
assertedMeta ??= {};
|
|
177
|
+
assertedMeta.comments = meta.comments;
|
|
178
|
+
}
|
|
179
|
+
if (!assertedMeta) {
|
|
180
|
+
return container;
|
|
181
|
+
}
|
|
182
|
+
return new Proxy(container, {
|
|
183
|
+
get(target, prop, _receiver) {
|
|
184
|
+
if (prop === EVENT_ITERATOR_EVENT_SOURCE_SYMBOL) {
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
if (prop === EVENT_ITERATOR_EVENT_META_SYMBOL) {
|
|
188
|
+
return assertedMeta;
|
|
189
|
+
}
|
|
190
|
+
return getOrBind(target, prop);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function unwrapEventIteratorEvent(container) {
|
|
195
|
+
if (!isTypescriptObject(container)) {
|
|
196
|
+
return [container, void 0];
|
|
197
|
+
}
|
|
198
|
+
const meta = container[EVENT_ITERATOR_EVENT_META_SYMBOL];
|
|
199
|
+
const target = container[EVENT_ITERATOR_EVENT_SOURCE_SYMBOL] ?? container;
|
|
200
|
+
return [target, meta];
|
|
201
|
+
}
|
|
202
|
+
function getEventIteratorEventMeta(container) {
|
|
203
|
+
if (!isTypescriptObject(container)) {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
return container[EVENT_ITERATOR_EVENT_META_SYMBOL];
|
|
207
|
+
}
|
|
2
208
|
|
|
3
209
|
function generateContentDisposition(filename) {
|
|
4
210
|
const encodedFilename = filename.replace(/[^\x20-\x7E]/g, "_").replace(/"/g, '\\"');
|
|
@@ -86,4 +292,4 @@ function isStandardResponse(maybe) {
|
|
|
86
292
|
return isStandardHeaders(maybe.headers);
|
|
87
293
|
}
|
|
88
294
|
|
|
89
|
-
export { flattenStandardHeader, generateContentDisposition, getFilenameFromContentDisposition, isStandardHeaders, isStandardMethod, isStandardRequest, isStandardResponse, isStandardUrl, mergeStandardHeaders, parseStandardUrl };
|
|
295
|
+
export { EVENT_ITERATOR_EVENT_META_SYMBOL, EVENT_ITERATOR_EVENT_SOURCE_SYMBOL, EventIteratorErrorEvent, EventStreamDecoder, EventStreamDecoderError, EventStreamDecoderStream, EventStreamEncoderError, assertEventStreamMessageComment, assertEventStreamMessageId, assertEventStreamMessageName, assertEventStreamMessageRetry, decodeEventStreamMessage, encodeEventStreamMessage, encodeEventStreamMessageComments, encodeEventStreamMessageData, flattenStandardHeader, generateContentDisposition, getEventIteratorEventMeta, getFilenameFromContentDisposition, isStandardHeaders, isStandardMethod, isStandardRequest, isStandardResponse, isStandardUrl, mergeStandardHeaders, parseStandardUrl, unwrapEventIteratorEvent, withEventIteratorEventMeta };
|
package/package.json
CHANGED
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@standardserver/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.18",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://standardserver.dev",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/middleapi/standardserver.git",
|
|
10
10
|
"directory": "packages/core"
|
|
11
11
|
},
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"exports": {
|
|
14
|
+
"./package.json": "./package.json",
|
|
14
15
|
".": {
|
|
15
16
|
"types": "./dist/index.d.mts",
|
|
16
17
|
"import": "./dist/index.mjs",
|
|
17
18
|
"default": "./dist/index.mjs"
|
|
18
|
-
},
|
|
19
|
-
"./event-stream": {
|
|
20
|
-
"types": "./dist/event-stream/index.d.mts",
|
|
21
|
-
"import": "./dist/event-stream/index.mjs",
|
|
22
|
-
"default": "./dist/event-stream/index.mjs"
|
|
23
19
|
}
|
|
24
20
|
},
|
|
25
21
|
"files": [
|
|
26
22
|
"dist"
|
|
27
23
|
],
|
|
28
24
|
"dependencies": {
|
|
29
|
-
"@standardserver/shared": "0.0.
|
|
25
|
+
"@standardserver/shared": "0.0.18"
|
|
30
26
|
},
|
|
31
27
|
"scripts": {
|
|
32
28
|
"build": "unbuild",
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
interface EventStreamMessageMeta {
|
|
2
|
-
/**
|
|
3
|
-
* Event identifier, sent back by the client as `lastEventId` for reconnection attempts.
|
|
4
|
-
*
|
|
5
|
-
* @warning id cannot contain newline characters (`\n`)
|
|
6
|
-
*/
|
|
7
|
-
id?: string | undefined;
|
|
8
|
-
/**
|
|
9
|
-
* The number of milliseconds the client should wait before attempting to reconnect.
|
|
10
|
-
*/
|
|
11
|
-
retry?: number | undefined;
|
|
12
|
-
/**
|
|
13
|
-
* Comments associated with the event.
|
|
14
|
-
*
|
|
15
|
-
* @warning Comments must not contain newline characters (`\n`).
|
|
16
|
-
*/
|
|
17
|
-
comments?: string[] | undefined;
|
|
18
|
-
}
|
|
19
|
-
interface EventStreamMessage extends EventStreamMessageMeta {
|
|
20
|
-
/**
|
|
21
|
-
* Event name (e.g., `message`, `error`).
|
|
22
|
-
*/
|
|
23
|
-
event?: string | undefined;
|
|
24
|
-
/**
|
|
25
|
-
* Event data, typically JSON-encoded.
|
|
26
|
-
*/
|
|
27
|
-
data?: string | undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
declare function decodeEventStreamMessage(encoded: string): EventStreamMessage;
|
|
31
|
-
declare class EventStreamDecoder {
|
|
32
|
-
private readonly onEvent;
|
|
33
|
-
private incomplete;
|
|
34
|
-
private trailingCR;
|
|
35
|
-
constructor(onEvent: (event: EventStreamMessage) => void);
|
|
36
|
-
feed(chunk: string): void;
|
|
37
|
-
end(): void;
|
|
38
|
-
}
|
|
39
|
-
declare class EventStreamDecoderStream extends TransformStream<string, EventStreamMessage> {
|
|
40
|
-
constructor();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
declare function assertEventStreamMessageId(id: string): void;
|
|
44
|
-
declare function assertEventStreamMessageName(event: string): void;
|
|
45
|
-
declare function assertEventStreamMessageRetry(retry: number): void;
|
|
46
|
-
declare function assertEventStreamMessageComment(comment: string): void;
|
|
47
|
-
declare function encodeEventStreamMessageData(data: string | undefined): string;
|
|
48
|
-
declare function encodeEventStreamMessageComments(comments: readonly string[] | undefined): string;
|
|
49
|
-
declare function encodeEventStreamMessage(message: EventStreamMessage): string;
|
|
50
|
-
|
|
51
|
-
declare class EventStreamEncoderError extends TypeError {
|
|
52
|
-
}
|
|
53
|
-
declare class EventStreamDecoderError extends TypeError {
|
|
54
|
-
}
|
|
55
|
-
interface EventIteratorErrorEventOptions extends ErrorOptions {
|
|
56
|
-
message?: string;
|
|
57
|
-
}
|
|
58
|
-
declare class EventIteratorErrorEvent extends Error {
|
|
59
|
-
readonly data: unknown;
|
|
60
|
-
constructor(data: unknown, options?: EventIteratorErrorEventOptions);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
declare const EVENT_ITERATOR_EVENT_META_SYMBOL: symbol;
|
|
64
|
-
declare const EVENT_ITERATOR_EVENT_SOURCE_SYMBOL: symbol;
|
|
65
|
-
/**
|
|
66
|
-
* Returns a new iterator *event value* with attached, validated metadata.
|
|
67
|
-
*/
|
|
68
|
-
declare function withEventIteratorEventMeta<T extends object>(container: T, meta: EventStreamMessageMeta): T;
|
|
69
|
-
/**
|
|
70
|
-
* Unwraps an iterator event value and extracts its associated metadata.
|
|
71
|
-
*/
|
|
72
|
-
declare function unwrapEventIteratorEvent<T>(container: T): [data: T, meta: EventStreamMessageMeta | undefined];
|
|
73
|
-
/**
|
|
74
|
-
* Retrieves metadata attached to a single iterator event value.
|
|
75
|
-
*/
|
|
76
|
-
declare function getEventIteratorEventMeta(container: unknown): EventStreamMessageMeta | undefined;
|
|
77
|
-
|
|
78
|
-
export { EVENT_ITERATOR_EVENT_META_SYMBOL, EVENT_ITERATOR_EVENT_SOURCE_SYMBOL, EventIteratorErrorEvent, EventStreamDecoder, EventStreamDecoderError, EventStreamDecoderStream, EventStreamEncoderError, assertEventStreamMessageComment, assertEventStreamMessageId, assertEventStreamMessageName, assertEventStreamMessageRetry, decodeEventStreamMessage, encodeEventStreamMessage, encodeEventStreamMessageComments, encodeEventStreamMessageData, getEventIteratorEventMeta, unwrapEventIteratorEvent, withEventIteratorEventMeta };
|
|
79
|
-
export type { EventIteratorErrorEventOptions, EventStreamMessage, EventStreamMessageMeta };
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
interface EventStreamMessageMeta {
|
|
2
|
-
/**
|
|
3
|
-
* Event identifier, sent back by the client as `lastEventId` for reconnection attempts.
|
|
4
|
-
*
|
|
5
|
-
* @warning id cannot contain newline characters (`\n`)
|
|
6
|
-
*/
|
|
7
|
-
id?: string | undefined;
|
|
8
|
-
/**
|
|
9
|
-
* The number of milliseconds the client should wait before attempting to reconnect.
|
|
10
|
-
*/
|
|
11
|
-
retry?: number | undefined;
|
|
12
|
-
/**
|
|
13
|
-
* Comments associated with the event.
|
|
14
|
-
*
|
|
15
|
-
* @warning Comments must not contain newline characters (`\n`).
|
|
16
|
-
*/
|
|
17
|
-
comments?: string[] | undefined;
|
|
18
|
-
}
|
|
19
|
-
interface EventStreamMessage extends EventStreamMessageMeta {
|
|
20
|
-
/**
|
|
21
|
-
* Event name (e.g., `message`, `error`).
|
|
22
|
-
*/
|
|
23
|
-
event?: string | undefined;
|
|
24
|
-
/**
|
|
25
|
-
* Event data, typically JSON-encoded.
|
|
26
|
-
*/
|
|
27
|
-
data?: string | undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
declare function decodeEventStreamMessage(encoded: string): EventStreamMessage;
|
|
31
|
-
declare class EventStreamDecoder {
|
|
32
|
-
private readonly onEvent;
|
|
33
|
-
private incomplete;
|
|
34
|
-
private trailingCR;
|
|
35
|
-
constructor(onEvent: (event: EventStreamMessage) => void);
|
|
36
|
-
feed(chunk: string): void;
|
|
37
|
-
end(): void;
|
|
38
|
-
}
|
|
39
|
-
declare class EventStreamDecoderStream extends TransformStream<string, EventStreamMessage> {
|
|
40
|
-
constructor();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
declare function assertEventStreamMessageId(id: string): void;
|
|
44
|
-
declare function assertEventStreamMessageName(event: string): void;
|
|
45
|
-
declare function assertEventStreamMessageRetry(retry: number): void;
|
|
46
|
-
declare function assertEventStreamMessageComment(comment: string): void;
|
|
47
|
-
declare function encodeEventStreamMessageData(data: string | undefined): string;
|
|
48
|
-
declare function encodeEventStreamMessageComments(comments: readonly string[] | undefined): string;
|
|
49
|
-
declare function encodeEventStreamMessage(message: EventStreamMessage): string;
|
|
50
|
-
|
|
51
|
-
declare class EventStreamEncoderError extends TypeError {
|
|
52
|
-
}
|
|
53
|
-
declare class EventStreamDecoderError extends TypeError {
|
|
54
|
-
}
|
|
55
|
-
interface EventIteratorErrorEventOptions extends ErrorOptions {
|
|
56
|
-
message?: string;
|
|
57
|
-
}
|
|
58
|
-
declare class EventIteratorErrorEvent extends Error {
|
|
59
|
-
readonly data: unknown;
|
|
60
|
-
constructor(data: unknown, options?: EventIteratorErrorEventOptions);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
declare const EVENT_ITERATOR_EVENT_META_SYMBOL: symbol;
|
|
64
|
-
declare const EVENT_ITERATOR_EVENT_SOURCE_SYMBOL: symbol;
|
|
65
|
-
/**
|
|
66
|
-
* Returns a new iterator *event value* with attached, validated metadata.
|
|
67
|
-
*/
|
|
68
|
-
declare function withEventIteratorEventMeta<T extends object>(container: T, meta: EventStreamMessageMeta): T;
|
|
69
|
-
/**
|
|
70
|
-
* Unwraps an iterator event value and extracts its associated metadata.
|
|
71
|
-
*/
|
|
72
|
-
declare function unwrapEventIteratorEvent<T>(container: T): [data: T, meta: EventStreamMessageMeta | undefined];
|
|
73
|
-
/**
|
|
74
|
-
* Retrieves metadata attached to a single iterator event value.
|
|
75
|
-
*/
|
|
76
|
-
declare function getEventIteratorEventMeta(container: unknown): EventStreamMessageMeta | undefined;
|
|
77
|
-
|
|
78
|
-
export { EVENT_ITERATOR_EVENT_META_SYMBOL, EVENT_ITERATOR_EVENT_SOURCE_SYMBOL, EventIteratorErrorEvent, EventStreamDecoder, EventStreamDecoderError, EventStreamDecoderStream, EventStreamEncoderError, assertEventStreamMessageComment, assertEventStreamMessageId, assertEventStreamMessageName, assertEventStreamMessageRetry, decodeEventStreamMessage, encodeEventStreamMessage, encodeEventStreamMessageComments, encodeEventStreamMessageData, getEventIteratorEventMeta, unwrapEventIteratorEvent, withEventIteratorEventMeta };
|
|
79
|
-
export type { EventIteratorErrorEventOptions, EventStreamMessage, EventStreamMessageMeta };
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { getPackageSymbol, getOrBind, isTypescriptObject } from '@standardserver/shared';
|
|
2
|
-
|
|
3
|
-
class EventStreamEncoderError extends TypeError {
|
|
4
|
-
}
|
|
5
|
-
class EventStreamDecoderError extends TypeError {
|
|
6
|
-
}
|
|
7
|
-
class EventIteratorErrorEvent extends Error {
|
|
8
|
-
constructor(data, options = {}) {
|
|
9
|
-
super(options?.message ?? "Error Event", options);
|
|
10
|
-
this.data = data;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const EVENT_STREAM_LINE_DELIMITER_REGEX = /\r\n|[\n\r]/;
|
|
15
|
-
const EVENT_STREAM_DELIMITER_REGEX = /(?:\r\n|\r(?!\n)|\n)(?:\r\n|\r(?!\n)|\n)/;
|
|
16
|
-
const LEADING_WHITESPACE_REGEX = /^\s/;
|
|
17
|
-
function decodeEventStreamMessage(encoded) {
|
|
18
|
-
const lines = encoded.split(EVENT_STREAM_LINE_DELIMITER_REGEX);
|
|
19
|
-
const message = {};
|
|
20
|
-
for (const line of lines) {
|
|
21
|
-
const index = line.indexOf(":");
|
|
22
|
-
const key = index === -1 ? line : line.slice(0, index);
|
|
23
|
-
const value = index === -1 ? "" : line.slice(index + 1).replace(LEADING_WHITESPACE_REGEX, "");
|
|
24
|
-
if (index === 0) {
|
|
25
|
-
message.comments ??= [];
|
|
26
|
-
message.comments.push(value);
|
|
27
|
-
} else if (key === "data") {
|
|
28
|
-
if (message.data !== void 0) {
|
|
29
|
-
message.data += `
|
|
30
|
-
${value}`;
|
|
31
|
-
} else {
|
|
32
|
-
message.data = value;
|
|
33
|
-
}
|
|
34
|
-
} else if (key === "event") {
|
|
35
|
-
message.event = value;
|
|
36
|
-
} else if (key === "id") {
|
|
37
|
-
message.id = value;
|
|
38
|
-
} else if (key === "retry") {
|
|
39
|
-
const maybeInteger = Number.parseInt(value);
|
|
40
|
-
if (Number.isInteger(maybeInteger) && maybeInteger >= 0 && maybeInteger.toString() === value) {
|
|
41
|
-
message.retry = maybeInteger;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return message;
|
|
46
|
-
}
|
|
47
|
-
class EventStreamDecoder {
|
|
48
|
-
constructor(onEvent) {
|
|
49
|
-
this.onEvent = onEvent;
|
|
50
|
-
}
|
|
51
|
-
incomplete = "";
|
|
52
|
-
trailingCR = false;
|
|
53
|
-
feed(chunk) {
|
|
54
|
-
if (this.trailingCR && chunk.startsWith("\n")) {
|
|
55
|
-
chunk = chunk.slice(1);
|
|
56
|
-
}
|
|
57
|
-
this.trailingCR = chunk.endsWith("\r");
|
|
58
|
-
this.incomplete += chunk;
|
|
59
|
-
const parts = this.incomplete.split(EVENT_STREAM_DELIMITER_REGEX);
|
|
60
|
-
this.incomplete = parts.pop();
|
|
61
|
-
for (const encoded of parts) {
|
|
62
|
-
const message = decodeEventStreamMessage(encoded);
|
|
63
|
-
this.onEvent(message);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
end() {
|
|
67
|
-
if (this.incomplete) {
|
|
68
|
-
throw new EventStreamDecoderError("Event Stream ended before complete");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
class EventStreamDecoderStream extends TransformStream {
|
|
73
|
-
constructor() {
|
|
74
|
-
let decoder;
|
|
75
|
-
super({
|
|
76
|
-
start(controller) {
|
|
77
|
-
decoder = new EventStreamDecoder((event) => {
|
|
78
|
-
controller.enqueue(event);
|
|
79
|
-
});
|
|
80
|
-
},
|
|
81
|
-
transform(chunk) {
|
|
82
|
-
decoder.feed(chunk);
|
|
83
|
-
},
|
|
84
|
-
flush() {
|
|
85
|
-
decoder.end();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const EVENT_STREAM_LINE_ENDING_REGEX = /\r\n|[\n\r]/;
|
|
92
|
-
function containsEventStreamLineBreak(value) {
|
|
93
|
-
return EVENT_STREAM_LINE_ENDING_REGEX.test(value);
|
|
94
|
-
}
|
|
95
|
-
function assertEventStreamMessageId(id) {
|
|
96
|
-
if (containsEventStreamLineBreak(id)) {
|
|
97
|
-
throw new EventStreamEncoderError("Event's id must not contain a carriage return or newline character");
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function assertEventStreamMessageName(event) {
|
|
101
|
-
if (containsEventStreamLineBreak(event)) {
|
|
102
|
-
throw new EventStreamEncoderError("Event's event must not contain a carriage return or newline character");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function assertEventStreamMessageRetry(retry) {
|
|
106
|
-
if (!Number.isInteger(retry) || retry < 0) {
|
|
107
|
-
throw new EventStreamEncoderError("Event's retry must be a integer and >= 0");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function assertEventStreamMessageComment(comment) {
|
|
111
|
-
if (containsEventStreamLineBreak(comment)) {
|
|
112
|
-
throw new EventStreamEncoderError("Event's comment must not contain a carriage return or newline character");
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function encodeEventStreamMessageData(data) {
|
|
116
|
-
let output = "";
|
|
117
|
-
if (data !== void 0) {
|
|
118
|
-
const lines = data.split(EVENT_STREAM_LINE_ENDING_REGEX);
|
|
119
|
-
for (const line of lines) {
|
|
120
|
-
output += `data: ${line}
|
|
121
|
-
`;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return output;
|
|
125
|
-
}
|
|
126
|
-
function encodeEventStreamMessageComments(comments) {
|
|
127
|
-
let output = "";
|
|
128
|
-
for (const comment of comments ?? []) {
|
|
129
|
-
assertEventStreamMessageComment(comment);
|
|
130
|
-
output += `: ${comment}
|
|
131
|
-
`;
|
|
132
|
-
}
|
|
133
|
-
return output;
|
|
134
|
-
}
|
|
135
|
-
function encodeEventStreamMessage(message) {
|
|
136
|
-
let output = "";
|
|
137
|
-
output += encodeEventStreamMessageComments(message.comments);
|
|
138
|
-
if (message.event !== void 0) {
|
|
139
|
-
assertEventStreamMessageName(message.event);
|
|
140
|
-
output += `event: ${message.event}
|
|
141
|
-
`;
|
|
142
|
-
}
|
|
143
|
-
if (message.retry !== void 0) {
|
|
144
|
-
assertEventStreamMessageRetry(message.retry);
|
|
145
|
-
output += `retry: ${message.retry}
|
|
146
|
-
`;
|
|
147
|
-
}
|
|
148
|
-
if (message.id !== void 0) {
|
|
149
|
-
assertEventStreamMessageId(message.id);
|
|
150
|
-
output += `id: ${message.id}
|
|
151
|
-
`;
|
|
152
|
-
}
|
|
153
|
-
output += encodeEventStreamMessageData(message.data);
|
|
154
|
-
output += "\n";
|
|
155
|
-
return output;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const EVENT_ITERATOR_EVENT_META_SYMBOL = getPackageSymbol("EVENT_ITERATOR_EVENT_META");
|
|
159
|
-
const EVENT_ITERATOR_EVENT_SOURCE_SYMBOL = getPackageSymbol("EVENT_ITERATOR_EVENT_SOURCE");
|
|
160
|
-
function withEventIteratorEventMeta(container, meta) {
|
|
161
|
-
let assertedMeta;
|
|
162
|
-
if (meta.id !== void 0) {
|
|
163
|
-
assertEventStreamMessageId(meta.id);
|
|
164
|
-
assertedMeta ??= {};
|
|
165
|
-
assertedMeta.id = meta.id;
|
|
166
|
-
}
|
|
167
|
-
if (meta.retry !== void 0) {
|
|
168
|
-
assertEventStreamMessageRetry(meta.retry);
|
|
169
|
-
assertedMeta ??= {};
|
|
170
|
-
assertedMeta.retry = meta.retry;
|
|
171
|
-
}
|
|
172
|
-
if (meta.comments !== void 0) {
|
|
173
|
-
for (const comment of meta.comments) {
|
|
174
|
-
assertEventStreamMessageComment(comment);
|
|
175
|
-
}
|
|
176
|
-
assertedMeta ??= {};
|
|
177
|
-
assertedMeta.comments = meta.comments;
|
|
178
|
-
}
|
|
179
|
-
if (!assertedMeta) {
|
|
180
|
-
return container;
|
|
181
|
-
}
|
|
182
|
-
return new Proxy(container, {
|
|
183
|
-
get(target, prop, _receiver) {
|
|
184
|
-
if (prop === EVENT_ITERATOR_EVENT_SOURCE_SYMBOL) {
|
|
185
|
-
return target;
|
|
186
|
-
}
|
|
187
|
-
if (prop === EVENT_ITERATOR_EVENT_META_SYMBOL) {
|
|
188
|
-
return assertedMeta;
|
|
189
|
-
}
|
|
190
|
-
return getOrBind(target, prop);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
function unwrapEventIteratorEvent(container) {
|
|
195
|
-
if (!isTypescriptObject(container)) {
|
|
196
|
-
return [container, void 0];
|
|
197
|
-
}
|
|
198
|
-
const meta = container[EVENT_ITERATOR_EVENT_META_SYMBOL];
|
|
199
|
-
const target = container[EVENT_ITERATOR_EVENT_SOURCE_SYMBOL] ?? container;
|
|
200
|
-
return [target, meta];
|
|
201
|
-
}
|
|
202
|
-
function getEventIteratorEventMeta(container) {
|
|
203
|
-
if (!isTypescriptObject(container)) {
|
|
204
|
-
return void 0;
|
|
205
|
-
}
|
|
206
|
-
return container[EVENT_ITERATOR_EVENT_META_SYMBOL];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export { EVENT_ITERATOR_EVENT_META_SYMBOL, EVENT_ITERATOR_EVENT_SOURCE_SYMBOL, EventIteratorErrorEvent, EventStreamDecoder, EventStreamDecoderError, EventStreamDecoderStream, EventStreamEncoderError, assertEventStreamMessageComment, assertEventStreamMessageId, assertEventStreamMessageName, assertEventStreamMessageRetry, decodeEventStreamMessage, encodeEventStreamMessage, encodeEventStreamMessageComments, encodeEventStreamMessageData, getEventIteratorEventMeta, unwrapEventIteratorEvent, withEventIteratorEventMeta };
|