@outfoxx/sunday 1.0.9 → 1.1.0-alpha.11

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