@outfoxx/sunday 1.0.8 → 1.1.0-alpha.10
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 +35 -2
- package/dist/any-type.d.ts +2 -0
- package/dist/class-type.d.ts +2 -2
- package/dist/date-time-types.d.ts +2 -7
- package/dist/date-time-types.js +2 -4
- package/dist/date-time-types.js.map +1 -1
- package/dist/event-parser.d.ts +17 -0
- package/dist/event-parser.js +140 -0
- package/dist/event-parser.js.map +1 -0
- package/dist/fetch-event-source.d.ts +12 -10
- package/dist/fetch-event-source.js +141 -138
- package/dist/fetch-event-source.js.map +1 -1
- package/dist/fetch-request-factory.d.ts +10 -5
- package/dist/fetch-request-factory.js +87 -71
- package/dist/fetch-request-factory.js.map +1 -1
- package/dist/fetch.d.ts +7 -1
- package/dist/fetch.js +61 -8
- package/dist/fetch.js.map +1 -1
- package/dist/header-parameters.d.ts +3 -0
- package/dist/header-parameters.js +40 -0
- package/dist/header-parameters.js.map +1 -0
- package/dist/index.d.ts +12 -12
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/{binary-decoder.d.ts → media-type-codecs/binary-decoder.d.ts} +2 -2
- package/dist/{binary-decoder.js → media-type-codecs/binary-decoder.js} +0 -0
- package/dist/media-type-codecs/binary-decoder.js.map +1 -0
- package/dist/{binary-encoder.d.ts → media-type-codecs/binary-encoder.d.ts} +0 -0
- package/dist/{binary-encoder.js → media-type-codecs/binary-encoder.js} +0 -0
- package/dist/media-type-codecs/binary-encoder.js.map +1 -0
- package/dist/media-type-codecs/cbor-decoder.d.ts +39 -0
- package/dist/media-type-codecs/cbor-decoder.js +413 -0
- package/dist/media-type-codecs/cbor-decoder.js.map +1 -0
- package/dist/media-type-codecs/cbor-encoder.d.ts +43 -0
- package/dist/media-type-codecs/cbor-encoder.js +226 -0
- package/dist/media-type-codecs/cbor-encoder.js.map +1 -0
- package/dist/media-type-codecs/cbor-tags.d.ts +4 -0
- package/dist/media-type-codecs/cbor-tags.js +5 -0
- package/dist/media-type-codecs/cbor-tags.js.map +1 -0
- package/dist/media-type-codecs/json-decoder.d.ts +39 -0
- package/dist/media-type-codecs/json-decoder.js +367 -0
- package/dist/media-type-codecs/json-decoder.js.map +1 -0
- package/dist/media-type-codecs/json-encoder.d.ts +44 -0
- package/dist/media-type-codecs/json-encoder.js +263 -0
- package/dist/media-type-codecs/json-encoder.js.map +1 -0
- package/dist/media-type-codecs/media-type-decoder.d.ts +11 -0
- package/dist/media-type-codecs/media-type-decoder.js +6 -0
- package/dist/media-type-codecs/media-type-decoder.js.map +1 -0
- package/dist/{media-type-decoders.d.ts → media-type-codecs/media-type-decoders.d.ts} +5 -4
- package/dist/{media-type-decoders.js → media-type-codecs/media-type-decoders.js} +6 -6
- package/dist/media-type-codecs/media-type-decoders.js.map +1 -0
- package/dist/{media-type-encoder.d.ts → media-type-codecs/media-type-encoder.d.ts} +5 -1
- package/dist/media-type-codecs/media-type-encoder.js +11 -0
- package/dist/media-type-codecs/media-type-encoder.js.map +1 -0
- package/dist/{media-type-encoders.d.ts → media-type-codecs/media-type-encoders.d.ts} +5 -4
- package/dist/{media-type-encoders.js → media-type-codecs/media-type-encoders.js} +8 -8
- package/dist/media-type-codecs/media-type-encoders.js.map +1 -0
- package/dist/{url-encoder.d.ts → media-type-codecs/www-form-url-encoder.d.ts} +8 -8
- package/dist/{url-encoder.js → media-type-codecs/www-form-url-encoder.js} +38 -37
- package/dist/media-type-codecs/www-form-url-encoder.js.map +1 -0
- package/dist/media-type.d.ts +91 -13
- package/dist/media-type.js +273 -15
- package/dist/media-type.js.map +1 -1
- package/dist/problem.d.ts +24 -9
- package/dist/problem.js +140 -6
- package/dist/problem.js.map +1 -1
- package/dist/request-factory.d.ts +10 -10
- package/dist/sunday-error.d.ts +11 -0
- package/dist/sunday-error.js +18 -0
- package/dist/sunday-error.js.map +1 -0
- package/dist/util/any.d.ts +2 -0
- package/dist/util/any.js +11 -0
- package/dist/util/any.js.map +1 -0
- package/dist/util/error.d.ts +2 -0
- package/dist/util/error.js +13 -0
- package/dist/util/error.js.map +1 -0
- package/dist/util/hex.js +3 -2
- package/dist/util/hex.js.map +1 -1
- package/dist/util/rxjs.d.ts +3 -0
- package/dist/util/rxjs.js +9 -5
- package/dist/util/rxjs.js.map +1 -1
- package/dist/util/temporal.d.ts +2 -0
- package/dist/util/temporal.js +18 -0
- package/dist/util/temporal.js.map +1 -0
- package/package.json +27 -21
- package/src/any-type.ts +4 -0
- package/src/class-type.ts +6 -2
- package/src/date-time-types.ts +22 -9
- package/src/event-parser.ts +190 -0
- package/src/fetch-event-source.ts +149 -159
- package/src/fetch-request-factory.ts +129 -101
- package/src/fetch.ts +65 -14
- package/src/header-parameters.ts +52 -0
- package/src/index.ts +12 -12
- package/src/{binary-decoder.ts → media-type-codecs/binary-decoder.ts} +3 -3
- package/src/{binary-encoder.ts → media-type-codecs/binary-encoder.ts} +0 -0
- package/src/media-type-codecs/cbor-decoder.ts +515 -0
- package/src/media-type-codecs/cbor-encoder.ts +307 -0
- package/src/media-type-codecs/cbor-tags.ts +4 -0
- package/src/media-type-codecs/json-decoder.ts +470 -0
- package/src/media-type-codecs/json-encoder.ts +328 -0
- package/src/media-type-codecs/media-type-decoder.ts +20 -0
- package/src/{media-type-decoders.ts → media-type-codecs/media-type-decoders.ts} +21 -13
- package/src/media-type-codecs/media-type-encoder.ts +31 -0
- package/src/{media-type-encoders.ts → media-type-codecs/media-type-encoders.ts} +23 -15
- package/src/{url-encoder.ts → media-type-codecs/www-form-url-encoder.ts} +53 -47
- package/src/media-type.ts +326 -22
- package/src/problem.ts +144 -12
- package/src/request-factory.ts +21 -12
- package/src/sunday-error.ts +37 -0
- package/src/util/any.ts +10 -0
- package/src/util/error.ts +14 -0
- package/src/util/hex.ts +3 -2
- package/src/util/rxjs.ts +16 -5
- package/src/util/temporal.ts +18 -0
- package/dist/binary-decoder.js.map +0 -1
- package/dist/binary-encoder.js.map +0 -1
- package/dist/cbor-decoder.d.ts +0 -15
- package/dist/cbor-decoder.js +0 -126
- package/dist/cbor-decoder.js.map +0 -1
- package/dist/cbor-encoder.d.ts +0 -29
- package/dist/cbor-encoder.js +0 -81
- package/dist/cbor-encoder.js.map +0 -1
- package/dist/cbor-tags.d.ts +0 -3
- package/dist/cbor-tags.js +0 -4
- package/dist/cbor-tags.js.map +0 -1
- package/dist/http-error.d.ts +0 -10
- package/dist/http-error.js +0 -45
- package/dist/http-error.js.map +0 -1
- package/dist/json-decoder.d.ts +0 -31
- package/dist/json-decoder.js +0 -139
- package/dist/json-decoder.js.map +0 -1
- package/dist/json-encoder.d.ts +0 -35
- package/dist/json-encoder.js +0 -116
- package/dist/json-encoder.js.map +0 -1
- package/dist/media-type-decoder.d.ts +0 -4
- package/dist/media-type-decoder.js +0 -2
- package/dist/media-type-decoder.js.map +0 -1
- package/dist/media-type-decoders.js.map +0 -1
- package/dist/media-type-encoder.js +0 -6
- package/dist/media-type-encoder.js.map +0 -1
- package/dist/media-type-encoders.js.map +0 -1
- package/dist/url-encoder.js.map +0 -1
- package/src/cbor-decoder.ts +0 -148
- package/src/cbor-encoder.ts +0 -95
- package/src/cbor-tags.ts +0 -3
- package/src/http-error.ts +0 -55
- package/src/json-decoder.ts +0 -164
- package/src/json-encoder.ts +0 -144
- package/src/media-type-decoder.ts +0 -5
- package/src/media-type-encoder.ts +0 -16
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export interface EventInfo {
|
|
2
|
+
id?: string;
|
|
3
|
+
event?: string;
|
|
4
|
+
data?: string;
|
|
5
|
+
retry?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class EventParser {
|
|
9
|
+
private decoder: TextDecoder = new TextDecoder('utf-8');
|
|
10
|
+
private unprocessedBuffer?: ArrayBuffer;
|
|
11
|
+
|
|
12
|
+
process(
|
|
13
|
+
buffer: ArrayBuffer,
|
|
14
|
+
dispatcher: (eventInfo: EventInfo) => void
|
|
15
|
+
): void {
|
|
16
|
+
const availableBuffer = this.buildAvailableBuffer(buffer);
|
|
17
|
+
if (!availableBuffer) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const eventStrings = this.extractEventStringsFromBuffer(availableBuffer);
|
|
22
|
+
|
|
23
|
+
if (!eventStrings.length) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
EventParser.parseAndDispatchEvents(eventStrings, dispatcher);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private buildAvailableBuffer(buffer: ArrayBuffer): ArrayBuffer | undefined {
|
|
31
|
+
const unprocessedBuffer = this.unprocessedBuffer;
|
|
32
|
+
this.unprocessedBuffer = undefined;
|
|
33
|
+
|
|
34
|
+
if (!buffer.byteLength) {
|
|
35
|
+
return unprocessedBuffer;
|
|
36
|
+
} else if (!unprocessedBuffer?.byteLength) {
|
|
37
|
+
return buffer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const newBuffer = new Uint8Array(
|
|
41
|
+
unprocessedBuffer.byteLength + buffer.byteLength
|
|
42
|
+
);
|
|
43
|
+
newBuffer.set(new Uint8Array(unprocessedBuffer), 0);
|
|
44
|
+
newBuffer.set(new Uint8Array(buffer), unprocessedBuffer.byteLength);
|
|
45
|
+
|
|
46
|
+
return newBuffer.buffer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private extractEventStringsFromBuffer(buffer: ArrayBuffer): string[] {
|
|
50
|
+
const eventStrings: string[] = [];
|
|
51
|
+
|
|
52
|
+
while (buffer.byteLength) {
|
|
53
|
+
// Find end of next event separator in buffer, exiting if none found.
|
|
54
|
+
const eventSeparator = EventParser.findEventSeparator(buffer);
|
|
55
|
+
if (!eventSeparator) {
|
|
56
|
+
// Save unprocessed data
|
|
57
|
+
this.unprocessedBuffer = buffer;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [endOfCurrentEventIdx, startOfNextEventIdx] = eventSeparator;
|
|
62
|
+
|
|
63
|
+
const eventBuffer = buffer.slice(0, endOfCurrentEventIdx);
|
|
64
|
+
buffer = buffer.slice(startOfNextEventIdx);
|
|
65
|
+
|
|
66
|
+
const eventString = this.decoder.decode(eventBuffer, {
|
|
67
|
+
stream: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
eventStrings.push(eventString);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return eventStrings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private static findEventSeparator(
|
|
77
|
+
buffer: ArrayBuffer
|
|
78
|
+
): [number, number] | undefined {
|
|
79
|
+
const bytes = new Uint8Array(buffer);
|
|
80
|
+
|
|
81
|
+
for (let idx = 0; idx < bytes.length; ++idx) {
|
|
82
|
+
const byte = bytes[idx];
|
|
83
|
+
|
|
84
|
+
switch (byte) {
|
|
85
|
+
// line-feed
|
|
86
|
+
case 0xa: {
|
|
87
|
+
// if next byte is same,
|
|
88
|
+
// we found a separator
|
|
89
|
+
if (bytes[idx + 1] == 0xa) {
|
|
90
|
+
return [idx, idx + 2];
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// carriage-return
|
|
96
|
+
case 0xd: {
|
|
97
|
+
// if next byte is same,
|
|
98
|
+
// we found a separator
|
|
99
|
+
if (bytes[idx + 1] == 0xd) {
|
|
100
|
+
return [idx, idx + 2];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// if next is line-feed, and pattern
|
|
104
|
+
// repeats, we found a separator.
|
|
105
|
+
if (
|
|
106
|
+
bytes[idx + 1] == 0xa &&
|
|
107
|
+
bytes[idx + 2] == 0xd &&
|
|
108
|
+
bytes[idx + 3] == 0xa
|
|
109
|
+
) {
|
|
110
|
+
return [idx, idx + 4];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private static parseAndDispatchEvents(
|
|
123
|
+
eventStrings: string[],
|
|
124
|
+
dispatcher: (eventInfo: EventInfo) => void
|
|
125
|
+
) {
|
|
126
|
+
for (const eventString of eventStrings) {
|
|
127
|
+
if (!eventString.length) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const parsedEvent = EventParser.parseEvent(eventString);
|
|
132
|
+
|
|
133
|
+
dispatcher(parsedEvent);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static parseEvent(eventString: string): EventInfo {
|
|
138
|
+
const event: EventInfo = {};
|
|
139
|
+
|
|
140
|
+
for (const line of eventString.split(lineSeparatorsRegEx)) {
|
|
141
|
+
const keyValueSeparatorIdx = line.indexOf(':');
|
|
142
|
+
|
|
143
|
+
let key: string;
|
|
144
|
+
let value: string;
|
|
145
|
+
if (keyValueSeparatorIdx != -1) {
|
|
146
|
+
key = line.slice(0, keyValueSeparatorIdx);
|
|
147
|
+
value = line.slice(keyValueSeparatorIdx + 1);
|
|
148
|
+
} else {
|
|
149
|
+
key = line;
|
|
150
|
+
value = '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
switch (key) {
|
|
154
|
+
case 'retry':
|
|
155
|
+
event.retry = EventParser.trimFieldValue(value);
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case 'data': {
|
|
159
|
+
const data = event.data ?? '';
|
|
160
|
+
event.data = `${data}${EventParser.trimFieldValue(value)}\n`;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case '':
|
|
165
|
+
// comment do nothing
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
default: {
|
|
169
|
+
(event as Record<string, string>)[key] =
|
|
170
|
+
EventParser.trimFieldValue(value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (event.data?.[event.data?.length - 1] == '\n') {
|
|
176
|
+
event.data = event.data.slice(0, -1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return event;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private static trimFieldValue(value: string): string {
|
|
183
|
+
if (value[0] != ' ') {
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
return value.slice(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lineSeparatorsRegEx = /\r\n|\r|\n/;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { EMPTY, Observable, of, Subscription } from 'rxjs';
|
|
2
|
-
import {
|
|
1
|
+
import { EMPTY, map, Observable, of, Subscription, switchMap, tap } from 'rxjs';
|
|
2
|
+
import { EventInfo, EventParser } from './event-parser';
|
|
3
3
|
import { validate } from './fetch';
|
|
4
4
|
import { Logger } from './logger';
|
|
5
5
|
import { MediaType } from './media-type';
|
|
6
6
|
import { ExtEventSource } from './request-factory';
|
|
7
|
+
import { unknownSet } from './util/any';
|
|
7
8
|
import { fromStream } from './util/stream-rxjs';
|
|
8
9
|
|
|
9
10
|
export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
@@ -23,22 +24,27 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
23
24
|
onmessage: ((this: EventSource, ev: MessageEvent) => unknown) | null = null;
|
|
24
25
|
onopen: ((this: EventSource, ev: Event) => unknown) | null = null;
|
|
25
26
|
|
|
27
|
+
get retryTime(): number {
|
|
28
|
+
return this.internalRetryTime;
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
private adapter: (
|
|
27
32
|
url: string,
|
|
28
33
|
requestInit: RequestInit
|
|
29
34
|
) => Observable<Request>;
|
|
30
35
|
private connectionSubscription?: Subscription;
|
|
31
|
-
private
|
|
32
|
-
private retryTime = 100;
|
|
36
|
+
private internalRetryTime = 100;
|
|
33
37
|
private retryAttempt = 0;
|
|
34
38
|
private connectionAttemptTime = 0;
|
|
39
|
+
private connectionOrigin?: string;
|
|
40
|
+
private reconnectTimeoutHandle?: number;
|
|
35
41
|
private lastEventId?: string;
|
|
36
42
|
private logger?: Logger;
|
|
37
|
-
private
|
|
38
|
-
private unprocessedText = '';
|
|
39
|
-
private eventTimeout?: number;
|
|
43
|
+
private readonly eventTimeout?: number;
|
|
40
44
|
private eventTimeoutCheckHandle?: number;
|
|
41
|
-
private
|
|
45
|
+
private lastEventReceivedTime = 0;
|
|
46
|
+
private eventParser = new EventParser();
|
|
47
|
+
private readonly externalAbortController?: AbortController;
|
|
42
48
|
|
|
43
49
|
constructor(
|
|
44
50
|
url: string,
|
|
@@ -46,6 +52,7 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
46
52
|
adapter?: (url: string, requestInit: RequestInit) => Observable<Request>;
|
|
47
53
|
eventTimeout?: number;
|
|
48
54
|
logger?: Logger;
|
|
55
|
+
abortController?: AbortController;
|
|
49
56
|
}
|
|
50
57
|
) {
|
|
51
58
|
super();
|
|
@@ -57,8 +64,13 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
57
64
|
eventSourceInit?.eventTimeout ??
|
|
58
65
|
FetchEventSource.EVENT_TIMEOUT_DEFAULT * 1000;
|
|
59
66
|
this.logger = eventSourceInit?.logger;
|
|
67
|
+
this.externalAbortController = eventSourceInit?.abortController;
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
//
|
|
71
|
+
// Connect
|
|
72
|
+
//
|
|
73
|
+
|
|
62
74
|
connect(): void {
|
|
63
75
|
if (this.readyState === this.CONNECTING || this.readyState === this.OPEN) {
|
|
64
76
|
// this.logger?.debug?.('skipping connect', { state: this.readyState });
|
|
@@ -74,19 +86,20 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
74
86
|
this.readyState = this.CONNECTING;
|
|
75
87
|
|
|
76
88
|
const headers = new Headers({
|
|
77
|
-
accept: MediaType.
|
|
89
|
+
accept: MediaType.EventStream.toString(),
|
|
78
90
|
});
|
|
79
91
|
if (this.lastEventId) {
|
|
80
92
|
headers.append(FetchEventSource.LAST_EVENT_ID_HEADER, this.lastEventId);
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
const
|
|
95
|
+
const abortController =
|
|
96
|
+
this.externalAbortController ?? new AbortController();
|
|
84
97
|
|
|
85
98
|
const requestInit: RequestInit = {
|
|
86
99
|
headers,
|
|
87
100
|
cache: 'no-store',
|
|
88
101
|
redirect: 'follow',
|
|
89
|
-
signal:
|
|
102
|
+
signal: abortController.signal,
|
|
90
103
|
};
|
|
91
104
|
|
|
92
105
|
this.connectionAttemptTime = Date.now();
|
|
@@ -95,7 +108,7 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
95
108
|
.pipe(
|
|
96
109
|
switchMap((request) => fetch(request)),
|
|
97
110
|
switchMap((response) => validate(response, true)),
|
|
98
|
-
tap(() => this.receivedHeaders()),
|
|
111
|
+
tap((response) => this.receivedHeaders(response)),
|
|
99
112
|
switchMap((response) => {
|
|
100
113
|
const body = response.body;
|
|
101
114
|
if (!body) {
|
|
@@ -108,19 +121,23 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
108
121
|
)
|
|
109
122
|
.subscribe({
|
|
110
123
|
error: (error: unknown) => {
|
|
111
|
-
abort.abort();
|
|
112
124
|
this.receivedError(error);
|
|
113
125
|
},
|
|
114
126
|
complete: () => {
|
|
115
|
-
abort.abort();
|
|
116
127
|
this.receivedComplete();
|
|
117
128
|
},
|
|
118
129
|
});
|
|
119
|
-
this.connectionSubscription.add(() =>
|
|
130
|
+
this.connectionSubscription.add(() => {
|
|
131
|
+
abortController.abort();
|
|
132
|
+
});
|
|
120
133
|
}
|
|
121
134
|
|
|
135
|
+
//
|
|
136
|
+
// Close
|
|
137
|
+
//
|
|
138
|
+
|
|
122
139
|
close(): void {
|
|
123
|
-
this.logger?.debug?.('
|
|
140
|
+
this.logger?.debug?.('close requested');
|
|
124
141
|
|
|
125
142
|
this.readyState = this.CLOSED;
|
|
126
143
|
|
|
@@ -131,19 +148,27 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
131
148
|
this.connectionSubscription?.unsubscribe();
|
|
132
149
|
this.connectionSubscription = undefined;
|
|
133
150
|
|
|
151
|
+
this.clearReconnect();
|
|
152
|
+
|
|
134
153
|
this.stopEventTimeoutCheck();
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
|
|
156
|
+
//
|
|
157
|
+
// Event Timeout
|
|
158
|
+
//
|
|
159
|
+
|
|
160
|
+
private startEventTimeoutCheck(lastEventReceivedTime: number) {
|
|
138
161
|
this.stopEventTimeoutCheck();
|
|
139
162
|
|
|
140
163
|
if (!this.eventTimeout) {
|
|
141
164
|
return;
|
|
142
165
|
}
|
|
143
166
|
|
|
167
|
+
this.lastEventReceivedTime = lastEventReceivedTime;
|
|
168
|
+
|
|
144
169
|
// this.logger?.debug?.('starting event timeout checks');
|
|
145
170
|
|
|
146
|
-
this.eventTimeoutCheckHandle = setInterval(
|
|
171
|
+
this.eventTimeoutCheckHandle = window.setInterval(
|
|
147
172
|
() => this.checkEventTimeout(),
|
|
148
173
|
FetchEventSource.EVENT_TIMEOUT_CHECK_INTERVAL * 1000
|
|
149
174
|
);
|
|
@@ -167,107 +192,95 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
167
192
|
// this.logger?.debug?.('checking event timeout');
|
|
168
193
|
|
|
169
194
|
// Check elapsed time since last received event
|
|
170
|
-
const elapsed = Date.now() -
|
|
195
|
+
const elapsed = Date.now() - this.lastEventReceivedTime;
|
|
171
196
|
if (elapsed > this.eventTimeout) {
|
|
172
197
|
this.logger?.debug?.('event timeout reached', {
|
|
173
198
|
elapsed,
|
|
174
199
|
});
|
|
175
|
-
|
|
176
|
-
this.internalClose();
|
|
200
|
+
this.fireErrorEvent(Error('EventTimeout'));
|
|
177
201
|
|
|
178
202
|
this.scheduleReconnect();
|
|
179
203
|
}
|
|
180
204
|
}
|
|
181
205
|
|
|
182
|
-
|
|
206
|
+
//
|
|
207
|
+
// Connection Handlers
|
|
208
|
+
//
|
|
209
|
+
|
|
210
|
+
private receivedHeaders(response: Response) {
|
|
183
211
|
if (this.readyState !== this.CONNECTING) {
|
|
184
|
-
this.
|
|
185
|
-
|
|
212
|
+
this.logger?.error?.('Invalid readyState for receiveHaders', {
|
|
213
|
+
readyState: this.readyState,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
this.fireErrorEvent(Error('InvalidState'));
|
|
217
|
+
|
|
218
|
+
this.scheduleReconnect();
|
|
219
|
+
return;
|
|
186
220
|
}
|
|
187
221
|
|
|
188
|
-
this.logger?.debug?.('
|
|
222
|
+
this.logger?.debug?.('opened');
|
|
189
223
|
|
|
224
|
+
this.connectionOrigin = response.url;
|
|
190
225
|
this.retryAttempt = 0;
|
|
191
226
|
this.readyState = this.OPEN;
|
|
192
227
|
|
|
193
|
-
this
|
|
228
|
+
// Start event timeout check, treating this
|
|
229
|
+
// connect as last time we received an event
|
|
230
|
+
this.startEventTimeoutCheck(Date.now());
|
|
194
231
|
|
|
195
232
|
const event = new Event('open');
|
|
196
233
|
this.onopen?.(event);
|
|
197
234
|
this.dispatchEvent(event);
|
|
198
235
|
}
|
|
199
236
|
|
|
200
|
-
private receivedData(
|
|
201
|
-
this.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.unprocessedBuffers.length - 1
|
|
206
|
-
];
|
|
207
|
-
const latestBytes = new Uint8Array(latest);
|
|
208
|
-
const latestNewLine = latestBytes.indexOf(0xa);
|
|
209
|
-
if (latestNewLine == -1) {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const nextLine = latestNewLine + 1;
|
|
213
|
-
|
|
214
|
-
const readyToProcess = this.unprocessedBuffers.slice(0, -1);
|
|
215
|
-
readyToProcess.push(latest.slice(0, nextLine));
|
|
216
|
-
|
|
217
|
-
const leftOver = latest.slice(nextLine);
|
|
218
|
-
this.unprocessedBuffers = leftOver.byteLength ? [leftOver] : [];
|
|
237
|
+
private receivedData(buffer: ArrayBuffer) {
|
|
238
|
+
if (this.readyState !== this.OPEN) {
|
|
239
|
+
this.logger?.error?.('Invalid readyState for receiveData', {
|
|
240
|
+
readyState: this.readyState,
|
|
241
|
+
});
|
|
219
242
|
|
|
220
|
-
|
|
221
|
-
for (const buffer of readyToProcess) {
|
|
222
|
-
text += this.decoder.decode(buffer, { stream: true });
|
|
223
|
-
}
|
|
243
|
+
this.fireErrorEvent(Error('InvalidState'));
|
|
224
244
|
|
|
225
|
-
this.
|
|
245
|
+
this.scheduleReconnect();
|
|
246
|
+
return;
|
|
226
247
|
}
|
|
227
|
-
}
|
|
228
248
|
|
|
229
|
-
|
|
230
|
-
// Clear out carriage returns
|
|
231
|
-
text = text.replace('\r\n', '\n');
|
|
249
|
+
this.logger?.debug?.('received data', { length: validate.length });
|
|
232
250
|
|
|
233
|
-
this.
|
|
234
|
-
|
|
235
|
-
const eventStrings = this.extractEventStrings();
|
|
236
|
-
|
|
237
|
-
this.parseEvents(eventStrings);
|
|
251
|
+
this.eventParser.process(buffer, this.dispatchParsedEvent);
|
|
238
252
|
}
|
|
239
253
|
|
|
240
254
|
private receivedError(error: unknown) {
|
|
241
|
-
if (
|
|
242
|
-
error instanceof DOMException &&
|
|
243
|
-
error.code === DOMException.ABORT_ERR
|
|
244
|
-
) {
|
|
245
|
-
// this.logger?.debug?.('aborted');
|
|
246
|
-
|
|
255
|
+
if (this.readyState === this.CLOSED) {
|
|
247
256
|
return;
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
this.logger?.debug?.('received error', { error });
|
|
251
|
-
|
|
252
|
-
this.scheduleReconnect();
|
|
253
|
-
|
|
254
|
-
const event = new Event('error');
|
|
255
|
-
((event as unknown) as Record<string, unknown>).error = error;
|
|
256
|
-
|
|
257
|
-
this.onerror?.(event);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private receivedComplete() {
|
|
261
|
-
this.logger?.debug?.('received complete');
|
|
260
|
+
this.fireErrorEvent(error);
|
|
262
261
|
|
|
263
262
|
if (this.readyState !== this.CLOSED) {
|
|
264
263
|
this.scheduleReconnect();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
265
266
|
|
|
267
|
+
private receivedComplete() {
|
|
268
|
+
if (this.readyState == this.CLOSED) {
|
|
266
269
|
return;
|
|
267
270
|
}
|
|
271
|
+
|
|
272
|
+
this.logger?.debug?.('received complete');
|
|
273
|
+
|
|
274
|
+
this.scheduleReconnect();
|
|
268
275
|
}
|
|
269
276
|
|
|
277
|
+
//
|
|
278
|
+
// Reconnection
|
|
279
|
+
//
|
|
280
|
+
|
|
270
281
|
private scheduleReconnect() {
|
|
282
|
+
this.internalClose();
|
|
283
|
+
|
|
271
284
|
// calculate total delay
|
|
272
285
|
const backOffDelay = Math.pow(this.retryAttempt, 2) * this.retryTime;
|
|
273
286
|
let retryDelay = Math.min(
|
|
@@ -275,113 +288,90 @@ export class FetchEventSource extends EventTarget implements ExtEventSource {
|
|
|
275
288
|
this.retryTime * FetchEventSource.MAX_RETRY_TIME_MULTIPLE
|
|
276
289
|
);
|
|
277
290
|
|
|
278
|
-
// Adjust delay by amount of time last
|
|
279
|
-
// on the first attempt
|
|
291
|
+
// Adjust delay by amount of time last connect
|
|
292
|
+
// cycle took, except on the first attempt
|
|
280
293
|
if (this.retryAttempt > 0) {
|
|
281
294
|
const connectionTime = Date.now() - this.connectionAttemptTime;
|
|
282
|
-
|
|
295
|
+
// Ensure delay is at least as large as
|
|
296
|
+
// minimum retry time interval
|
|
297
|
+
retryDelay = Math.max(retryDelay - connectionTime, this.retryTime);
|
|
283
298
|
}
|
|
284
299
|
|
|
285
300
|
this.retryAttempt++;
|
|
301
|
+
this.readyState = this.CONNECTING;
|
|
286
302
|
|
|
287
303
|
this.logger?.debug?.('scheduling reconnect', { retryDelay });
|
|
288
304
|
|
|
289
|
-
|
|
305
|
+
this.reconnectTimeoutHandle = window.setTimeout(
|
|
306
|
+
() => this.internalConnect(),
|
|
307
|
+
retryDelay
|
|
308
|
+
);
|
|
290
309
|
}
|
|
291
310
|
|
|
292
|
-
private
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return [];
|
|
311
|
+
private clearReconnect() {
|
|
312
|
+
if (this.reconnectTimeoutHandle) {
|
|
313
|
+
clearTimeout(this.reconnectTimeoutHandle);
|
|
296
314
|
}
|
|
297
|
-
|
|
298
|
-
const eventStrings = received.split(EVENT_SEPARATOR);
|
|
299
|
-
|
|
300
|
-
const last = eventStrings.pop();
|
|
301
|
-
this.unprocessedText = last?.length ? last : '';
|
|
302
|
-
|
|
303
|
-
return eventStrings;
|
|
315
|
+
this.reconnectTimeoutHandle = undefined;
|
|
304
316
|
}
|
|
305
317
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (!line.length) {
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const parsedEvent = FetchEventSource.parseEvent(eventString);
|
|
318
|
+
//
|
|
319
|
+
// Event Dispatch
|
|
320
|
+
//
|
|
314
321
|
|
|
315
|
-
|
|
316
|
-
|
|
322
|
+
private dispatchParsedEvent = (eventInfo: EventInfo) => {
|
|
323
|
+
this.lastEventReceivedTime = Date.now();
|
|
317
324
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
parsedEvent.id == null &&
|
|
321
|
-
parsedEvent.event == null &&
|
|
322
|
-
parsedEvent.data == null
|
|
323
|
-
) {
|
|
324
|
-
this.logger?.debug?.('updating retry timeout', { retryTime });
|
|
325
|
+
if (eventInfo.retry) {
|
|
326
|
+
const retryTime = Number.parseInt(eventInfo.retry, 10);
|
|
325
327
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.logger?.debug?.('ignoring invalid retry timeout event', {
|
|
329
|
-
parsedEvent,
|
|
330
|
-
});
|
|
331
|
-
}
|
|
328
|
+
if (Number.isSafeInteger(retryTime)) {
|
|
329
|
+
this.logger?.debug?.('updating retry timeout', { retryTime });
|
|
332
330
|
|
|
333
|
-
|
|
331
|
+
this.internalRetryTime = retryTime;
|
|
332
|
+
} else {
|
|
333
|
+
this.logger?.debug?.('ignoring invalid retry timeout event', {
|
|
334
|
+
eventInfo,
|
|
335
|
+
});
|
|
334
336
|
}
|
|
337
|
+
}
|
|
335
338
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// Dispatch event
|
|
346
|
-
const event = new MessageEvent(parsedEvent.event ?? 'message', {
|
|
347
|
-
data: parsedEvent.data,
|
|
348
|
-
lastEventId: this.lastEventId,
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
this.onmessage?.(event);
|
|
352
|
-
this.dispatchEvent(event);
|
|
339
|
+
// skip empty events
|
|
340
|
+
if (
|
|
341
|
+
eventInfo.id == null &&
|
|
342
|
+
eventInfo.event == null &&
|
|
343
|
+
eventInfo.data == null
|
|
344
|
+
) {
|
|
345
|
+
// skip empty event
|
|
346
|
+
return;
|
|
353
347
|
}
|
|
354
|
-
}
|
|
355
348
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
case 'retry':
|
|
366
|
-
event.retry = value;
|
|
367
|
-
break;
|
|
368
|
-
case '':
|
|
369
|
-
// comment do nothing
|
|
370
|
-
break;
|
|
371
|
-
default:
|
|
372
|
-
(event as Record<string, string>)[key] = value.trim();
|
|
349
|
+
// Save last-event-id if the new id is valid
|
|
350
|
+
if (eventInfo.id != null) {
|
|
351
|
+
// Check for NULL as it is not allowed
|
|
352
|
+
if (eventInfo.id.indexOf('\0') == -1) {
|
|
353
|
+
this.lastEventId = eventInfo.id;
|
|
354
|
+
} else {
|
|
355
|
+
this.logger?.debug?.(
|
|
356
|
+
'event id contains null, unable to use for last-event-id'
|
|
357
|
+
);
|
|
373
358
|
}
|
|
374
359
|
}
|
|
375
360
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
361
|
+
// Dispatch event
|
|
362
|
+
const event = new MessageEvent(eventInfo.event ?? 'message', {
|
|
363
|
+
data: eventInfo.data,
|
|
364
|
+
lastEventId: this.lastEventId,
|
|
365
|
+
origin: this.connectionOrigin,
|
|
366
|
+
});
|
|
379
367
|
|
|
380
|
-
|
|
368
|
+
this.onmessage?.(event);
|
|
369
|
+
this.dispatchEvent(event);
|
|
370
|
+
};
|
|
381
371
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
372
|
+
fireErrorEvent(error: unknown): void {
|
|
373
|
+
const event = new Event('error');
|
|
374
|
+
unknownSet(event, 'error', error);
|
|
375
|
+
this.onerror?.(event);
|
|
376
|
+
}
|
|
387
377
|
}
|