@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.
Files changed (151) hide show
  1. package/README.md +35 -2
  2. package/dist/any-type.d.ts +2 -0
  3. package/dist/class-type.d.ts +2 -2
  4. package/dist/date-time-types.d.ts +2 -7
  5. package/dist/date-time-types.js +2 -4
  6. package/dist/date-time-types.js.map +1 -1
  7. package/dist/event-parser.d.ts +17 -0
  8. package/dist/event-parser.js +140 -0
  9. package/dist/event-parser.js.map +1 -0
  10. package/dist/fetch-event-source.d.ts +12 -10
  11. package/dist/fetch-event-source.js +141 -138
  12. package/dist/fetch-event-source.js.map +1 -1
  13. package/dist/fetch-request-factory.d.ts +10 -5
  14. package/dist/fetch-request-factory.js +87 -71
  15. package/dist/fetch-request-factory.js.map +1 -1
  16. package/dist/fetch.d.ts +7 -1
  17. package/dist/fetch.js +61 -8
  18. package/dist/fetch.js.map +1 -1
  19. package/dist/header-parameters.d.ts +3 -0
  20. package/dist/header-parameters.js +40 -0
  21. package/dist/header-parameters.js.map +1 -0
  22. package/dist/index.d.ts +12 -12
  23. package/dist/index.js +12 -12
  24. package/dist/index.js.map +1 -1
  25. package/dist/{binary-decoder.d.ts → media-type-codecs/binary-decoder.d.ts} +2 -2
  26. package/dist/{binary-decoder.js → media-type-codecs/binary-decoder.js} +0 -0
  27. package/dist/media-type-codecs/binary-decoder.js.map +1 -0
  28. package/dist/{binary-encoder.d.ts → media-type-codecs/binary-encoder.d.ts} +0 -0
  29. package/dist/{binary-encoder.js → media-type-codecs/binary-encoder.js} +0 -0
  30. package/dist/media-type-codecs/binary-encoder.js.map +1 -0
  31. package/dist/media-type-codecs/cbor-decoder.d.ts +39 -0
  32. package/dist/media-type-codecs/cbor-decoder.js +413 -0
  33. package/dist/media-type-codecs/cbor-decoder.js.map +1 -0
  34. package/dist/media-type-codecs/cbor-encoder.d.ts +43 -0
  35. package/dist/media-type-codecs/cbor-encoder.js +226 -0
  36. package/dist/media-type-codecs/cbor-encoder.js.map +1 -0
  37. package/dist/media-type-codecs/cbor-tags.d.ts +4 -0
  38. package/dist/media-type-codecs/cbor-tags.js +5 -0
  39. package/dist/media-type-codecs/cbor-tags.js.map +1 -0
  40. package/dist/media-type-codecs/json-decoder.d.ts +39 -0
  41. package/dist/media-type-codecs/json-decoder.js +367 -0
  42. package/dist/media-type-codecs/json-decoder.js.map +1 -0
  43. package/dist/media-type-codecs/json-encoder.d.ts +44 -0
  44. package/dist/media-type-codecs/json-encoder.js +263 -0
  45. package/dist/media-type-codecs/json-encoder.js.map +1 -0
  46. package/dist/media-type-codecs/media-type-decoder.d.ts +11 -0
  47. package/dist/media-type-codecs/media-type-decoder.js +6 -0
  48. package/dist/media-type-codecs/media-type-decoder.js.map +1 -0
  49. package/dist/{media-type-decoders.d.ts → media-type-codecs/media-type-decoders.d.ts} +5 -4
  50. package/dist/{media-type-decoders.js → media-type-codecs/media-type-decoders.js} +6 -6
  51. package/dist/media-type-codecs/media-type-decoders.js.map +1 -0
  52. package/dist/{media-type-encoder.d.ts → media-type-codecs/media-type-encoder.d.ts} +5 -1
  53. package/dist/media-type-codecs/media-type-encoder.js +11 -0
  54. package/dist/media-type-codecs/media-type-encoder.js.map +1 -0
  55. package/dist/{media-type-encoders.d.ts → media-type-codecs/media-type-encoders.d.ts} +5 -4
  56. package/dist/{media-type-encoders.js → media-type-codecs/media-type-encoders.js} +8 -8
  57. package/dist/media-type-codecs/media-type-encoders.js.map +1 -0
  58. package/dist/{url-encoder.d.ts → media-type-codecs/www-form-url-encoder.d.ts} +8 -8
  59. package/dist/{url-encoder.js → media-type-codecs/www-form-url-encoder.js} +38 -37
  60. package/dist/media-type-codecs/www-form-url-encoder.js.map +1 -0
  61. package/dist/media-type.d.ts +91 -13
  62. package/dist/media-type.js +273 -15
  63. package/dist/media-type.js.map +1 -1
  64. package/dist/problem.d.ts +24 -9
  65. package/dist/problem.js +140 -6
  66. package/dist/problem.js.map +1 -1
  67. package/dist/request-factory.d.ts +10 -10
  68. package/dist/sunday-error.d.ts +11 -0
  69. package/dist/sunday-error.js +18 -0
  70. package/dist/sunday-error.js.map +1 -0
  71. package/dist/util/any.d.ts +2 -0
  72. package/dist/util/any.js +11 -0
  73. package/dist/util/any.js.map +1 -0
  74. package/dist/util/error.d.ts +2 -0
  75. package/dist/util/error.js +13 -0
  76. package/dist/util/error.js.map +1 -0
  77. package/dist/util/hex.js +3 -2
  78. package/dist/util/hex.js.map +1 -1
  79. package/dist/util/rxjs.d.ts +3 -0
  80. package/dist/util/rxjs.js +9 -5
  81. package/dist/util/rxjs.js.map +1 -1
  82. package/dist/util/temporal.d.ts +2 -0
  83. package/dist/util/temporal.js +18 -0
  84. package/dist/util/temporal.js.map +1 -0
  85. package/package.json +27 -21
  86. package/src/any-type.ts +4 -0
  87. package/src/class-type.ts +6 -2
  88. package/src/date-time-types.ts +22 -9
  89. package/src/event-parser.ts +190 -0
  90. package/src/fetch-event-source.ts +149 -159
  91. package/src/fetch-request-factory.ts +129 -101
  92. package/src/fetch.ts +65 -14
  93. package/src/header-parameters.ts +52 -0
  94. package/src/index.ts +12 -12
  95. package/src/{binary-decoder.ts → media-type-codecs/binary-decoder.ts} +3 -3
  96. package/src/{binary-encoder.ts → media-type-codecs/binary-encoder.ts} +0 -0
  97. package/src/media-type-codecs/cbor-decoder.ts +515 -0
  98. package/src/media-type-codecs/cbor-encoder.ts +307 -0
  99. package/src/media-type-codecs/cbor-tags.ts +4 -0
  100. package/src/media-type-codecs/json-decoder.ts +470 -0
  101. package/src/media-type-codecs/json-encoder.ts +328 -0
  102. package/src/media-type-codecs/media-type-decoder.ts +20 -0
  103. package/src/{media-type-decoders.ts → media-type-codecs/media-type-decoders.ts} +21 -13
  104. package/src/media-type-codecs/media-type-encoder.ts +31 -0
  105. package/src/{media-type-encoders.ts → media-type-codecs/media-type-encoders.ts} +23 -15
  106. package/src/{url-encoder.ts → media-type-codecs/www-form-url-encoder.ts} +53 -47
  107. package/src/media-type.ts +326 -22
  108. package/src/problem.ts +144 -12
  109. package/src/request-factory.ts +21 -12
  110. package/src/sunday-error.ts +37 -0
  111. package/src/util/any.ts +10 -0
  112. package/src/util/error.ts +14 -0
  113. package/src/util/hex.ts +3 -2
  114. package/src/util/rxjs.ts +16 -5
  115. package/src/util/temporal.ts +18 -0
  116. package/dist/binary-decoder.js.map +0 -1
  117. package/dist/binary-encoder.js.map +0 -1
  118. package/dist/cbor-decoder.d.ts +0 -15
  119. package/dist/cbor-decoder.js +0 -126
  120. package/dist/cbor-decoder.js.map +0 -1
  121. package/dist/cbor-encoder.d.ts +0 -29
  122. package/dist/cbor-encoder.js +0 -81
  123. package/dist/cbor-encoder.js.map +0 -1
  124. package/dist/cbor-tags.d.ts +0 -3
  125. package/dist/cbor-tags.js +0 -4
  126. package/dist/cbor-tags.js.map +0 -1
  127. package/dist/http-error.d.ts +0 -10
  128. package/dist/http-error.js +0 -45
  129. package/dist/http-error.js.map +0 -1
  130. package/dist/json-decoder.d.ts +0 -31
  131. package/dist/json-decoder.js +0 -139
  132. package/dist/json-decoder.js.map +0 -1
  133. package/dist/json-encoder.d.ts +0 -35
  134. package/dist/json-encoder.js +0 -116
  135. package/dist/json-encoder.js.map +0 -1
  136. package/dist/media-type-decoder.d.ts +0 -4
  137. package/dist/media-type-decoder.js +0 -2
  138. package/dist/media-type-decoder.js.map +0 -1
  139. package/dist/media-type-decoders.js.map +0 -1
  140. package/dist/media-type-encoder.js +0 -6
  141. package/dist/media-type-encoder.js.map +0 -1
  142. package/dist/media-type-encoders.js.map +0 -1
  143. package/dist/url-encoder.js.map +0 -1
  144. package/src/cbor-decoder.ts +0 -148
  145. package/src/cbor-encoder.ts +0 -95
  146. package/src/cbor-tags.ts +0 -3
  147. package/src/http-error.ts +0 -55
  148. package/src/json-decoder.ts +0 -164
  149. package/src/json-encoder.ts +0 -144
  150. package/src/media-type-decoder.ts +0 -5
  151. 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 { map, switchMap, tap } from 'rxjs/operators';
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 decoder: TextDecoder = new TextDecoder('utf-8');
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 unprocessedBuffers: ArrayBuffer[] = [];
38
- private unprocessedText = '';
39
- private eventTimeout?: number;
43
+ private readonly eventTimeout?: number;
40
44
  private eventTimeoutCheckHandle?: number;
41
- private lastEventTime?: number;
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.EVENT_STREAM,
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 abort = new AbortController();
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: abort.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(() => abort.abort());
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?.('closed');
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
- private startEventTimeoutCheck() {
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() - (this.lastEventTime ?? 0);
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
- private receivedHeaders() {
206
+ //
207
+ // Connection Handlers
208
+ //
209
+
210
+ private receivedHeaders(response: Response) {
183
211
  if (this.readyState !== this.CONNECTING) {
184
- this.close();
185
- throw Error('Invalid readyState');
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?.('connected');
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.startEventTimeoutCheck();
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(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] : [];
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
- let text = '';
221
- for (const buffer of readyToProcess) {
222
- text += this.decoder.decode(buffer, { stream: true });
223
- }
243
+ this.fireErrorEvent(Error('InvalidState'));
224
244
 
225
- this.receivedText(text);
245
+ this.scheduleReconnect();
246
+ return;
226
247
  }
227
- }
228
248
 
229
- private receivedText(text: string) {
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.unprocessedText += text;
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 reconnect cycle took, except
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
- retryDelay = Math.max(retryDelay - connectionTime, 0);
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
- setTimeout(() => this.internalConnect(), retryDelay);
305
+ this.reconnectTimeoutHandle = window.setTimeout(
306
+ () => this.internalConnect(),
307
+ retryDelay
308
+ );
290
309
  }
291
310
 
292
- private extractEventStrings(): string[] {
293
- const received = this.unprocessedText;
294
- if (!received.length) {
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
- 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);
318
+ //
319
+ // Event Dispatch
320
+ //
314
321
 
315
- if (parsedEvent.retry) {
316
- const retryTime = Number.parseInt(parsedEvent.retry, 10);
322
+ private dispatchParsedEvent = (eventInfo: EventInfo) => {
323
+ this.lastEventReceivedTime = Date.now();
317
324
 
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 });
325
+ if (eventInfo.retry) {
326
+ const retryTime = Number.parseInt(eventInfo.retry, 10);
325
327
 
326
- this.retryTime = retryTime;
327
- } else {
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
- continue;
331
+ this.internalRetryTime = retryTime;
332
+ } else {
333
+ this.logger?.debug?.('ignoring invalid retry timeout event', {
334
+ eventInfo,
335
+ });
334
336
  }
337
+ }
335
338
 
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);
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
- 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();
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
- return event;
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
- const EVENT_SEPARATOR = /\n\n/;
368
+ this.onmessage?.(event);
369
+ this.dispatchEvent(event);
370
+ };
381
371
 
382
- interface EventInfo {
383
- id?: string;
384
- event?: string;
385
- data?: string;
386
- retry?: string;
372
+ fireErrorEvent(error: unknown): void {
373
+ const event = new Event('error');
374
+ unknownSet(event, 'error', error);
375
+ this.onerror?.(event);
376
+ }
387
377
  }