@karpeleslab/klbfw 0.2.22 → 0.2.23
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 +48 -2
- package/internal.js +2 -1
- package/package.json +1 -1
- package/rest.js +331 -34
package/index.d.ts
CHANGED
|
@@ -32,7 +32,50 @@ declare function setCookie(name: string, value: string, expires?: Date | number,
|
|
|
32
32
|
declare function rest(name: string, verb: string, params?: Record<string, any>, context?: Record<string, any>): Promise<any>;
|
|
33
33
|
declare function rest_get(name: string, params?: Record<string, any>): Promise<any>; // Backward compatibility
|
|
34
34
|
declare function restGet(name: string, params?: Record<string, any>): Promise<any>;
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
/** SSE message event */
|
|
37
|
+
interface SSEMessageEvent {
|
|
38
|
+
/** Event type */
|
|
39
|
+
type: string;
|
|
40
|
+
/** Event data */
|
|
41
|
+
data: string;
|
|
42
|
+
/** Last event ID */
|
|
43
|
+
lastEventId: string;
|
|
44
|
+
/** Origin */
|
|
45
|
+
origin: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** SSE error event */
|
|
49
|
+
interface SSEErrorEvent {
|
|
50
|
+
type: 'error';
|
|
51
|
+
error: Error | Record<string, any>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** EventSource-like object returned by restSSE */
|
|
55
|
+
interface SSESource {
|
|
56
|
+
/** Handler called when connection opens */
|
|
57
|
+
onopen: ((event: { type: 'open' }) => void) | null;
|
|
58
|
+
/** Handler called for message events */
|
|
59
|
+
onmessage: ((event: SSEMessageEvent) => void) | null;
|
|
60
|
+
/** Handler called on error */
|
|
61
|
+
onerror: ((event: SSEErrorEvent) => void) | null;
|
|
62
|
+
/** Connection state: 0 = CONNECTING, 1 = OPEN, 2 = CLOSED */
|
|
63
|
+
readyState: number;
|
|
64
|
+
/** CONNECTING state constant */
|
|
65
|
+
readonly CONNECTING: 0;
|
|
66
|
+
/** OPEN state constant */
|
|
67
|
+
readonly OPEN: 1;
|
|
68
|
+
/** CLOSED state constant */
|
|
69
|
+
readonly CLOSED: 2;
|
|
70
|
+
/** Add event listener for specific event type */
|
|
71
|
+
addEventListener(type: string, listener: (event: SSEMessageEvent) => void): void;
|
|
72
|
+
/** Remove event listener */
|
|
73
|
+
removeEventListener(type: string, listener: (event: SSEMessageEvent) => void): void;
|
|
74
|
+
/** Close the connection */
|
|
75
|
+
close(): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare function restSSE(name: string, method?: string, params?: Record<string, any>, context?: Record<string, any>): SSESource;
|
|
36
79
|
|
|
37
80
|
// Upload module types
|
|
38
81
|
|
|
@@ -148,5 +191,8 @@ export {
|
|
|
148
191
|
trimPrefix,
|
|
149
192
|
UploadFileInput,
|
|
150
193
|
UploadFileOptions,
|
|
151
|
-
UploadManyFilesOptions
|
|
194
|
+
UploadManyFilesOptions,
|
|
195
|
+
SSEMessageEvent,
|
|
196
|
+
SSEErrorEvent,
|
|
197
|
+
SSESource
|
|
152
198
|
};
|
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
|
|