@karpeleslab/klbfw 0.2.22 → 0.2.24
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/index.d.ts +201 -5
- package/internal.js +2 -1
- package/package.json +1 -1
- package/rest.js +331 -34
package/index.d.ts
CHANGED
|
@@ -29,10 +29,196 @@ declare function hasCookie(name: string): boolean;
|
|
|
29
29
|
declare function setCookie(name: string, value: string, expires?: Date | number, path?: string, domain?: string, secure?: boolean): void;
|
|
30
30
|
|
|
31
31
|
// REST API types
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
|
|
33
|
+
/** Paging information returned by list endpoints */
|
|
34
|
+
interface RestPaging {
|
|
35
|
+
/** Current page number (1-indexed) */
|
|
36
|
+
page_no: number;
|
|
37
|
+
/** Total number of results across all pages */
|
|
38
|
+
count: number;
|
|
39
|
+
/** Maximum page number available */
|
|
40
|
+
page_max: number;
|
|
41
|
+
/** Number of results per page */
|
|
42
|
+
results_per_page: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Successful REST API response wrapper
|
|
47
|
+
* @typeParam T - Type of the data field
|
|
48
|
+
*/
|
|
49
|
+
interface RestResponse<T = any> {
|
|
50
|
+
/** Result status */
|
|
51
|
+
result: 'success' | 'redirect';
|
|
52
|
+
/** Unique request identifier for debugging */
|
|
53
|
+
request_id: string;
|
|
54
|
+
/** Request processing time in seconds */
|
|
55
|
+
time: number;
|
|
56
|
+
/** Response payload */
|
|
57
|
+
data: T;
|
|
58
|
+
/** Paging information for list endpoints */
|
|
59
|
+
paging?: RestPaging;
|
|
60
|
+
/** Additional response fields */
|
|
61
|
+
[key: string]: any;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** REST API error object (thrown on promise rejection) */
|
|
65
|
+
interface RestError {
|
|
66
|
+
/** Always 'error' for error responses */
|
|
67
|
+
result: 'error';
|
|
68
|
+
/** Exception class name from server */
|
|
69
|
+
exception: string;
|
|
70
|
+
/** Human-readable error message */
|
|
71
|
+
error: string;
|
|
72
|
+
/** HTTP status code */
|
|
73
|
+
code: number;
|
|
74
|
+
/** Translatable error token (e.g., 'error_invalid_field') */
|
|
75
|
+
token: string;
|
|
76
|
+
/** Request ID for debugging */
|
|
77
|
+
request: string;
|
|
78
|
+
/** Structured message data for translation */
|
|
79
|
+
message: Record<string, any>;
|
|
80
|
+
/** Parameter name that caused the error, if applicable */
|
|
81
|
+
param?: string;
|
|
82
|
+
/** Request processing time in seconds */
|
|
83
|
+
time: number;
|
|
84
|
+
/** Additional error fields */
|
|
85
|
+
[key: string]: any;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Server DateTime object
|
|
90
|
+
* @example
|
|
91
|
+
* // Convert to JavaScript Date
|
|
92
|
+
* new Date(Number(datetime.unixms))
|
|
93
|
+
*/
|
|
94
|
+
interface DateTime {
|
|
95
|
+
/** Unix timestamp in milliseconds (use this for JS Date conversion) */
|
|
96
|
+
unixms: string | number;
|
|
97
|
+
/** Unix timestamp in seconds */
|
|
98
|
+
unix?: number;
|
|
99
|
+
/** Microseconds component */
|
|
100
|
+
us?: number;
|
|
101
|
+
/** ISO 8601 formatted string with microseconds */
|
|
102
|
+
iso?: string;
|
|
103
|
+
/** Timezone identifier (e.g., 'Asia/Tokyo') */
|
|
104
|
+
tz?: string;
|
|
105
|
+
/** Full precision timestamp as string (unix seconds + microseconds) */
|
|
106
|
+
full?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extended integer for precise decimal arithmetic without floating-point errors.
|
|
111
|
+
* Value = v / 10^e = f
|
|
112
|
+
*
|
|
113
|
+
* When sending to API, you can provide either:
|
|
114
|
+
* - Just `f` (as string or number)
|
|
115
|
+
* - Both `v` and `e`
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // $358.20 represented as:
|
|
119
|
+
* { v: "35820000", e: 5, f: 358.2 }
|
|
120
|
+
*/
|
|
121
|
+
interface Xint {
|
|
122
|
+
/** Integer value (multiply by 10^-e to get actual value) */
|
|
123
|
+
v?: string;
|
|
124
|
+
/** Exponent (number of decimal places) */
|
|
125
|
+
e?: number;
|
|
126
|
+
/** Float value (convenience field, may have precision loss) */
|
|
127
|
+
f?: string | number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Base price value without tax breakdown */
|
|
131
|
+
interface PriceValue {
|
|
132
|
+
/** Decimal value as string (e.g., "358.20000") */
|
|
133
|
+
value: string;
|
|
134
|
+
/** Integer representation for precise arithmetic */
|
|
135
|
+
value_int: string;
|
|
136
|
+
/** Value in cents/smallest currency unit */
|
|
137
|
+
value_cent: string;
|
|
138
|
+
/** Display-ready decimal string (e.g., "358.20") */
|
|
139
|
+
value_disp: string;
|
|
140
|
+
/** Extended integer for precise calculations */
|
|
141
|
+
value_xint: Xint;
|
|
142
|
+
/** Formatted display string with currency symbol (e.g., "$358.20") */
|
|
143
|
+
display: string;
|
|
144
|
+
/** Short formatted display string */
|
|
145
|
+
display_short: string;
|
|
146
|
+
/** ISO 4217 currency code (e.g., "USD") */
|
|
147
|
+
currency: string;
|
|
148
|
+
/** Currency unit (usually same as currency) */
|
|
149
|
+
unit: string;
|
|
150
|
+
/** Whether VAT/tax is included in this value */
|
|
151
|
+
has_vat: boolean;
|
|
152
|
+
/** Tax profile identifier or null if exempt */
|
|
153
|
+
tax_profile: string | null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Full price object with optional tax breakdown
|
|
158
|
+
* @example
|
|
159
|
+
* // Display price with tax info
|
|
160
|
+
* console.log(price.display); // "$358.20"
|
|
161
|
+
* console.log(price.tax?.display); // "$358.20" (with tax)
|
|
162
|
+
* console.log(price.tax_only?.display); // "$0.00" (tax amount only)
|
|
163
|
+
*/
|
|
164
|
+
interface Price extends PriceValue {
|
|
165
|
+
/** Original price before any tax calculations */
|
|
166
|
+
raw?: PriceValue;
|
|
167
|
+
/** Price including tax */
|
|
168
|
+
tax?: PriceValue;
|
|
169
|
+
/** Tax amount only */
|
|
170
|
+
tax_only?: PriceValue;
|
|
171
|
+
/** Tax rate as decimal (e.g., 0.1 for 10%) */
|
|
172
|
+
tax_rate?: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
declare function rest<T = any>(name: string, verb: string, params?: Record<string, any>, context?: Record<string, any>): Promise<RestResponse<T>>;
|
|
176
|
+
declare function rest_get<T = any>(name: string, params?: Record<string, any>): Promise<RestResponse<T>>; // Backward compatibility
|
|
177
|
+
declare function restGet<T = any>(name: string, params?: Record<string, any>): Promise<RestResponse<T>>;
|
|
178
|
+
|
|
179
|
+
/** SSE message event */
|
|
180
|
+
interface SSEMessageEvent {
|
|
181
|
+
/** Event type */
|
|
182
|
+
type: string;
|
|
183
|
+
/** Event data */
|
|
184
|
+
data: string;
|
|
185
|
+
/** Last event ID */
|
|
186
|
+
lastEventId: string;
|
|
187
|
+
/** Origin */
|
|
188
|
+
origin: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** SSE error event */
|
|
192
|
+
interface SSEErrorEvent {
|
|
193
|
+
type: 'error';
|
|
194
|
+
error: Error | Record<string, any>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** EventSource-like object returned by restSSE */
|
|
198
|
+
interface SSESource {
|
|
199
|
+
/** Handler called when connection opens */
|
|
200
|
+
onopen: ((event: { type: 'open' }) => void) | null;
|
|
201
|
+
/** Handler called for message events */
|
|
202
|
+
onmessage: ((event: SSEMessageEvent) => void) | null;
|
|
203
|
+
/** Handler called on error */
|
|
204
|
+
onerror: ((event: SSEErrorEvent) => void) | null;
|
|
205
|
+
/** Connection state: 0 = CONNECTING, 1 = OPEN, 2 = CLOSED */
|
|
206
|
+
readyState: number;
|
|
207
|
+
/** CONNECTING state constant */
|
|
208
|
+
readonly CONNECTING: 0;
|
|
209
|
+
/** OPEN state constant */
|
|
210
|
+
readonly OPEN: 1;
|
|
211
|
+
/** CLOSED state constant */
|
|
212
|
+
readonly CLOSED: 2;
|
|
213
|
+
/** Add event listener for specific event type */
|
|
214
|
+
addEventListener(type: string, listener: (event: SSEMessageEvent) => void): void;
|
|
215
|
+
/** Remove event listener */
|
|
216
|
+
removeEventListener(type: string, listener: (event: SSEMessageEvent) => void): void;
|
|
217
|
+
/** Close the connection */
|
|
218
|
+
close(): void;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
declare function restSSE(name: string, method?: string, params?: Record<string, any>, context?: Record<string, any>): SSESource;
|
|
36
222
|
|
|
37
223
|
// Upload module types
|
|
38
224
|
|
|
@@ -146,7 +332,17 @@ export {
|
|
|
146
332
|
uploadManyFiles,
|
|
147
333
|
getI18N,
|
|
148
334
|
trimPrefix,
|
|
335
|
+
RestPaging,
|
|
336
|
+
RestResponse,
|
|
337
|
+
RestError,
|
|
338
|
+
DateTime,
|
|
339
|
+
Xint,
|
|
340
|
+
PriceValue,
|
|
341
|
+
Price,
|
|
149
342
|
UploadFileInput,
|
|
150
343
|
UploadFileOptions,
|
|
151
|
-
UploadManyFilesOptions
|
|
344
|
+
UploadManyFilesOptions,
|
|
345
|
+
SSEMessageEvent,
|
|
346
|
+
SSEErrorEvent,
|
|
347
|
+
SSESource
|
|
152
348
|
};
|
package/internal.js
CHANGED
|
@@ -344,4 +344,5 @@ module.exports.responseParse = responseParse;
|
|
|
344
344
|
module.exports.padNumber = padNumber;
|
|
345
345
|
module.exports.getTimezoneData = getTimezoneData;
|
|
346
346
|
module.exports.buildRestUrl = buildRestUrl;
|
|
347
|
-
module.exports.internalRest = internalRest;
|
|
347
|
+
module.exports.internalRest = internalRest;
|
|
348
|
+
module.exports.checkAndRefreshToken = checkAndRefreshToken;
|
package/package.json
CHANGED
package/rest.js
CHANGED
|
@@ -154,62 +154,359 @@ const restGet = (name, params) => {
|
|
|
154
154
|
};
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* Parses a single SSE event from text
|
|
158
|
+
* @param {string} eventText - The raw SSE event text
|
|
159
|
+
* @returns {Object} Parsed event object with type, data, id, and retry fields
|
|
160
|
+
*/
|
|
161
|
+
const parseSSEEvent = (eventText) => {
|
|
162
|
+
const event = {
|
|
163
|
+
type: 'message',
|
|
164
|
+
data: '',
|
|
165
|
+
id: null,
|
|
166
|
+
retry: null
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const lines = eventText.split('\n');
|
|
170
|
+
const dataLines = [];
|
|
171
|
+
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
if (line === '') continue;
|
|
174
|
+
|
|
175
|
+
// Handle lines with field:value format
|
|
176
|
+
const colonIndex = line.indexOf(':');
|
|
177
|
+
|
|
178
|
+
if (colonIndex === 0) {
|
|
179
|
+
// Comment line (starts with :), skip
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let field, value;
|
|
184
|
+
if (colonIndex === -1) {
|
|
185
|
+
// Field with no value
|
|
186
|
+
field = line;
|
|
187
|
+
value = '';
|
|
188
|
+
} else {
|
|
189
|
+
field = line.substring(0, colonIndex);
|
|
190
|
+
// Skip the optional space after colon
|
|
191
|
+
value = line.substring(colonIndex + 1);
|
|
192
|
+
if (value.charAt(0) === ' ') {
|
|
193
|
+
value = value.substring(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
switch (field) {
|
|
198
|
+
case 'event':
|
|
199
|
+
event.type = value;
|
|
200
|
+
break;
|
|
201
|
+
case 'data':
|
|
202
|
+
dataLines.push(value);
|
|
203
|
+
break;
|
|
204
|
+
case 'id':
|
|
205
|
+
event.id = value;
|
|
206
|
+
break;
|
|
207
|
+
case 'retry':
|
|
208
|
+
const retryMs = parseInt(value, 10);
|
|
209
|
+
if (!isNaN(retryMs)) {
|
|
210
|
+
event.retry = retryMs;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Join data lines with newlines (per SSE spec)
|
|
217
|
+
event.data = dataLines.join('\n');
|
|
218
|
+
|
|
219
|
+
return event;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Makes a REST API request that handles SSE streaming responses
|
|
158
224
|
* @param {string} name - API endpoint name
|
|
159
|
-
* @param {string} method - HTTP method (
|
|
225
|
+
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
160
226
|
* @param {Object} params - Request parameters
|
|
161
|
-
* @param {Object} context - Context object with additional parameters
|
|
162
|
-
* @returns {
|
|
227
|
+
* @param {Object} [context] - Context object with additional parameters
|
|
228
|
+
* @returns {Object} EventSource-like object with onmessage, onerror, addEventListener, close
|
|
163
229
|
*/
|
|
164
230
|
const restSSE = (name, method, params, context) => {
|
|
165
|
-
|
|
166
|
-
if (method !== 'GET') {
|
|
167
|
-
throw new Error('EventSource only supports GET method');
|
|
168
|
-
}
|
|
231
|
+
const abortController = new AbortController();
|
|
169
232
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
233
|
+
method = method || 'GET';
|
|
234
|
+
params = params || {};
|
|
235
|
+
context = context || {};
|
|
236
|
+
|
|
237
|
+
// EventSource-like object
|
|
238
|
+
const eventSource = {
|
|
239
|
+
onopen: null,
|
|
240
|
+
onmessage: null,
|
|
241
|
+
onerror: null,
|
|
242
|
+
readyState: 0, // 0 = CONNECTING, 1 = OPEN, 2 = CLOSED
|
|
243
|
+
CONNECTING: 0,
|
|
244
|
+
OPEN: 1,
|
|
245
|
+
CLOSED: 2,
|
|
246
|
+
_listeners: {},
|
|
247
|
+
|
|
248
|
+
addEventListener: function(type, listener) {
|
|
249
|
+
if (!this._listeners[type]) {
|
|
250
|
+
this._listeners[type] = [];
|
|
251
|
+
}
|
|
252
|
+
this._listeners[type].push(listener);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
removeEventListener: function(type, listener) {
|
|
256
|
+
if (!this._listeners[type]) return;
|
|
257
|
+
const idx = this._listeners[type].indexOf(listener);
|
|
258
|
+
if (idx !== -1) {
|
|
259
|
+
this._listeners[type].splice(idx, 1);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
dispatchEvent: function(event) {
|
|
264
|
+
// Call type-specific handler (onmessage, onerror, etc.)
|
|
265
|
+
const handlerName = 'on' + event.type;
|
|
266
|
+
if (typeof this[handlerName] === 'function') {
|
|
267
|
+
this[handlerName](event);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Call addEventListener listeners
|
|
271
|
+
const listeners = this._listeners[event.type];
|
|
272
|
+
if (listeners) {
|
|
273
|
+
for (const listener of listeners) {
|
|
274
|
+
listener(event);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
close: function() {
|
|
280
|
+
this.readyState = 2;
|
|
281
|
+
abortController.abort();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
174
284
|
|
|
175
285
|
if (!internal.checkSupport()) {
|
|
176
|
-
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
eventSource.readyState = 2;
|
|
288
|
+
eventSource.dispatchEvent({ type: 'error', error: new Error('Environment not supported') });
|
|
289
|
+
}, 0);
|
|
290
|
+
return eventSource;
|
|
177
291
|
}
|
|
178
292
|
|
|
179
|
-
params = params || {};
|
|
180
|
-
context = context || {};
|
|
181
|
-
|
|
182
293
|
// Add timezone data if in browser
|
|
183
294
|
if (typeof window !== 'undefined') {
|
|
184
295
|
context['t'] = internal.getTimezoneData();
|
|
185
296
|
}
|
|
186
297
|
|
|
187
|
-
// Build URL with authentication and context
|
|
188
298
|
let callUrl = internal.buildRestUrl(name, true, context);
|
|
299
|
+
const headers = {
|
|
300
|
+
'Accept': 'text/event-stream, application/json'
|
|
301
|
+
};
|
|
189
302
|
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
303
|
+
const token = fwWrapper.getToken();
|
|
304
|
+
if (token && token !== '') {
|
|
305
|
+
headers['Authorization'] = 'Session ' + token;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Build fetch options based on method
|
|
309
|
+
const fetchOptions = {
|
|
310
|
+
method: method,
|
|
311
|
+
credentials: 'include',
|
|
312
|
+
headers: headers,
|
|
313
|
+
signal: abortController.signal
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (method === 'GET') {
|
|
317
|
+
// For GET requests, add params to URL
|
|
318
|
+
if (params && Object.keys(params).length > 0) {
|
|
319
|
+
const glue = callUrl.indexOf('?') === -1 ? '?' : '&';
|
|
320
|
+
if (typeof params === 'string') {
|
|
321
|
+
callUrl += glue + '_=' + encodeURIComponent(params);
|
|
322
|
+
} else {
|
|
323
|
+
callUrl += glue + '_=' + encodeURIComponent(JSON.stringify(params));
|
|
324
|
+
}
|
|
197
325
|
}
|
|
326
|
+
} else {
|
|
327
|
+
// For other methods, add params to body as JSON
|
|
328
|
+
headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
329
|
+
fetchOptions.body = JSON.stringify(params);
|
|
198
330
|
}
|
|
199
331
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
332
|
+
// Helper to dispatch SSE events
|
|
333
|
+
const dispatchSSEEvent = (parsedEvent) => {
|
|
334
|
+
const event = {
|
|
335
|
+
type: parsedEvent.type,
|
|
336
|
+
data: parsedEvent.data,
|
|
337
|
+
lastEventId: parsedEvent.id || '',
|
|
338
|
+
origin: ''
|
|
339
|
+
};
|
|
206
340
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
341
|
+
// For 'message' type, use onmessage
|
|
342
|
+
if (parsedEvent.type === 'message') {
|
|
343
|
+
eventSource.dispatchEvent(event);
|
|
344
|
+
} else {
|
|
345
|
+
// For custom event types, dispatch to both the specific type and as a generic event
|
|
346
|
+
eventSource.dispatchEvent(event);
|
|
347
|
+
}
|
|
211
348
|
};
|
|
212
349
|
|
|
350
|
+
// Check and refresh token if needed, then make the request
|
|
351
|
+
internal.checkAndRefreshToken().then(() => {
|
|
352
|
+
fetch(callUrl, fetchOptions)
|
|
353
|
+
.then(response => {
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
// Handle HTTP errors
|
|
356
|
+
const contentType = response.headers.get('content-type') || '';
|
|
357
|
+
if (contentType.indexOf('application/json') !== -1) {
|
|
358
|
+
return response.json().then(json => {
|
|
359
|
+
json.headers = response.headers;
|
|
360
|
+
json.status = response.status;
|
|
361
|
+
throw json;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
throw {
|
|
365
|
+
message: `HTTP Error: ${response.status} ${response.statusText}`,
|
|
366
|
+
status: response.status,
|
|
367
|
+
headers: response.headers
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Connection is now open
|
|
372
|
+
eventSource.readyState = 1;
|
|
373
|
+
eventSource.dispatchEvent({ type: 'open' });
|
|
374
|
+
|
|
375
|
+
const contentType = response.headers.get('content-type') || '';
|
|
376
|
+
|
|
377
|
+
// Check if response is SSE
|
|
378
|
+
if (contentType.indexOf('text/event-stream') !== -1) {
|
|
379
|
+
// Stream SSE events
|
|
380
|
+
let buffer = '';
|
|
381
|
+
|
|
382
|
+
const processData = (chunk) => {
|
|
383
|
+
buffer += chunk;
|
|
384
|
+
|
|
385
|
+
// SSE events are separated by double newlines
|
|
386
|
+
const events = buffer.split(/\n\n/);
|
|
387
|
+
|
|
388
|
+
// Keep the last incomplete event in the buffer
|
|
389
|
+
buffer = events.pop() || '';
|
|
390
|
+
|
|
391
|
+
// Process complete events
|
|
392
|
+
for (const eventText of events) {
|
|
393
|
+
if (eventText.trim()) {
|
|
394
|
+
const parsed = parseSSEEvent(eventText);
|
|
395
|
+
if (parsed.data || parsed.type !== 'message') {
|
|
396
|
+
dispatchSSEEvent(parsed);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const processEnd = () => {
|
|
403
|
+
// Process any remaining data in buffer
|
|
404
|
+
if (buffer.trim()) {
|
|
405
|
+
const parsed = parseSSEEvent(buffer);
|
|
406
|
+
if (parsed.data || parsed.type !== 'message') {
|
|
407
|
+
dispatchSSEEvent(parsed);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
eventSource.readyState = 2;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Check if we have a web ReadableStream (browser) or Node.js stream
|
|
414
|
+
if (response.body && typeof response.body.getReader === 'function') {
|
|
415
|
+
// Browser environment - use ReadableStream API
|
|
416
|
+
const reader = response.body.getReader();
|
|
417
|
+
const decoder = new TextDecoder();
|
|
418
|
+
|
|
419
|
+
const processStream = () => {
|
|
420
|
+
reader.read().then(({ done, value }) => {
|
|
421
|
+
if (done) {
|
|
422
|
+
processEnd();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
processData(decoder.decode(value, { stream: true }));
|
|
427
|
+
|
|
428
|
+
// Continue reading
|
|
429
|
+
processStream();
|
|
430
|
+
}).catch(err => {
|
|
431
|
+
if (err.name !== 'AbortError') {
|
|
432
|
+
eventSource.dispatchEvent({ type: 'error', error: err });
|
|
433
|
+
}
|
|
434
|
+
eventSource.readyState = 2;
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
processStream();
|
|
439
|
+
} else if (response.body && typeof response.body.on === 'function') {
|
|
440
|
+
// Node.js environment - use Node stream API
|
|
441
|
+
response.body.on('data', (chunk) => {
|
|
442
|
+
processData(chunk.toString());
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
response.body.on('end', () => {
|
|
446
|
+
processEnd();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
response.body.on('error', (err) => {
|
|
450
|
+
// Handle abort errors gracefully
|
|
451
|
+
if (err.type !== 'aborted' && err.name !== 'AbortError') {
|
|
452
|
+
eventSource.dispatchEvent({ type: 'error', error: err });
|
|
453
|
+
}
|
|
454
|
+
eventSource.readyState = 2;
|
|
455
|
+
});
|
|
456
|
+
} else {
|
|
457
|
+
// Fallback - read entire body as text
|
|
458
|
+
response.text().then(text => {
|
|
459
|
+
processData(text);
|
|
460
|
+
processEnd();
|
|
461
|
+
}).catch(err => {
|
|
462
|
+
eventSource.dispatchEvent({ type: 'error', error: err });
|
|
463
|
+
eventSource.readyState = 2;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
} else if (contentType.indexOf('application/json') !== -1) {
|
|
467
|
+
// Non-SSE JSON response - emit as single event
|
|
468
|
+
response.json().then(json => {
|
|
469
|
+
// Check for gtag (consistent with responseParse)
|
|
470
|
+
if (json.gtag && typeof window !== 'undefined' && window.gtag) {
|
|
471
|
+
json.gtag.map(item => window.gtag.apply(null, item));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
dispatchSSEEvent({
|
|
475
|
+
type: 'message',
|
|
476
|
+
data: JSON.stringify(json),
|
|
477
|
+
id: null
|
|
478
|
+
});
|
|
479
|
+
eventSource.readyState = 2;
|
|
480
|
+
}).catch(err => {
|
|
481
|
+
eventSource.dispatchEvent({ type: 'error', error: err });
|
|
482
|
+
eventSource.readyState = 2;
|
|
483
|
+
});
|
|
484
|
+
} else {
|
|
485
|
+
// Other content types - emit raw text as single event
|
|
486
|
+
response.text().then(text => {
|
|
487
|
+
dispatchSSEEvent({
|
|
488
|
+
type: 'message',
|
|
489
|
+
data: text,
|
|
490
|
+
id: null
|
|
491
|
+
});
|
|
492
|
+
eventSource.readyState = 2;
|
|
493
|
+
}).catch(err => {
|
|
494
|
+
eventSource.dispatchEvent({ type: 'error', error: err });
|
|
495
|
+
eventSource.readyState = 2;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
.catch(err => {
|
|
500
|
+
if (err.name !== 'AbortError') {
|
|
501
|
+
eventSource.dispatchEvent({ type: 'error', error: err });
|
|
502
|
+
}
|
|
503
|
+
eventSource.readyState = 2;
|
|
504
|
+
});
|
|
505
|
+
}).catch(err => {
|
|
506
|
+
eventSource.dispatchEvent({ type: 'error', error: err });
|
|
507
|
+
eventSource.readyState = 2;
|
|
508
|
+
});
|
|
509
|
+
|
|
213
510
|
return eventSource;
|
|
214
511
|
};
|
|
215
512
|
|