@karpeleslab/klbfw 0.2.21 → 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/README.md CHANGED
@@ -50,6 +50,48 @@ npm run test:integration
50
50
  - **Standardized Naming**: Consistent use of camelCase with backward compatibility for legacy APIs
51
51
  - **Enhanced Error Handling**: More robust error handling and reporting
52
52
 
53
+ ## Migrating from upload.append() to uploadFile()
54
+
55
+ The new `uploadFile()` function provides a simpler Promise-based API for file uploads. Here are the key differences when migrating:
56
+
57
+ ### Return Value
58
+
59
+ **Legacy `upload.append()`** resolves with an upload object containing the result in `.final`:
60
+ ```javascript
61
+ upload.append('Misc/Debug:testUpload', file)
62
+ .then(result => {
63
+ console.log(result.final); // The completion response data
64
+ });
65
+ ```
66
+
67
+ **New `uploadFile()`** resolves with the full REST response:
68
+ ```javascript
69
+ uploadFile('Misc/Debug:testUpload', buffer)
70
+ .then(response => {
71
+ console.log(response.data); // The completion response data
72
+ });
73
+ ```
74
+
75
+ ### Migration Example
76
+
77
+ Before:
78
+ ```javascript
79
+ upload.append('Misc/Debug:testUpload', file, params, context)
80
+ .then(up => {
81
+ const data = up.final;
82
+ // use data
83
+ });
84
+ ```
85
+
86
+ After:
87
+ ```javascript
88
+ uploadFile('Misc/Debug:testUpload', file, 'POST', params, context)
89
+ .then(response => {
90
+ const data = response.data;
91
+ // use data
92
+ });
93
+ ```
94
+
53
95
  # API
54
96
 
55
97
  ## REST API Methods
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
- declare function restSSE(name: string, method: 'GET', params?: Record<string, any>, context?: Record<string, any>): EventSource;
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
 
@@ -90,7 +133,7 @@ declare const upload: {
90
133
  onprogress?: (status: { queue: any[]; running: any[]; failed: any[] }) => void;
91
134
  };
92
135
 
93
- /** Upload a single file */
136
+ /** Upload a single file. Resolves with the full REST response. */
94
137
  declare function uploadFile(
95
138
  api: string,
96
139
  buffer: UploadFileInput,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karpeleslab/klbfw",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "Frontend Framework",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/rest.js CHANGED
@@ -154,62 +154,359 @@ const restGet = (name, params) => {
154
154
  };
155
155
 
156
156
  /**
157
- * Creates a Server-Sent Events (SSE) connection to a REST API endpoint
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 (must be GET)
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 {EventSource} EventSource instance for the SSE connection
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
- // EventSource only supports GET requests
166
- if (method !== 'GET') {
167
- throw new Error('EventSource only supports GET method');
168
- }
231
+ const abortController = new AbortController();
169
232
 
170
- // EventSource only works in browsers
171
- if (typeof EventSource === 'undefined') {
172
- throw new Error('EventSource is not supported in this environment');
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
- throw new Error('Environment not supported');
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
- // Add params to the URL
191
- if (params) {
192
- const glue = callUrl.indexOf('?') === -1 ? '?' : '&';
193
- if (typeof params === 'string') {
194
- callUrl += glue + '_=' + encodeURIComponent(params);
195
- } else {
196
- callUrl += glue + '_=' + encodeURIComponent(JSON.stringify(params));
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
- // Create and return EventSource instance
201
- // Note: EventSource doesn't support custom headers directly,
202
- // but authentication is handled via URL parameters or cookies
203
- const eventSource = new EventSource(callUrl, {
204
- withCredentials: true
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
- // Handle errors and server-side closures
208
- eventSource.onerror = (error) => {
209
- console.error('EventSource error:', error);
210
- eventSource.close();
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
 
package/upload.js CHANGED
@@ -37,7 +37,7 @@ const { env, utils, awsReq, readChunkFromStream, readFileSlice } = require('./up
37
37
  * @param {Function} [options.onError] - Error callback(error, context). Can return a Promise
38
38
  * that, if resolved, will cause the failed operation to be retried. Context contains
39
39
  * { phase, blockNum, attempt } for block uploads or { phase, attempt } for other operations.
40
- * @returns {Promise<Object>} - Resolves with the upload result data
40
+ * @returns {Promise<Object>} - Resolves with the full REST response
41
41
  *
42
42
  * @example
43
43
  * // Upload a buffer with filename
@@ -290,7 +290,7 @@ async function doPutUpload(file, uploadInfo, context, options) {
290
290
  attempt++;
291
291
  try {
292
292
  const completeResponse = await rest.rest(uploadInfo.Complete, 'POST', {}, context);
293
- return completeResponse.data;
293
+ return completeResponse;
294
294
  } catch (error) {
295
295
  if (onError) {
296
296
  await onError(error, { phase: 'complete', attempt });
@@ -542,7 +542,7 @@ async function doAwsUpload(file, uploadInfo, context, options) {
542
542
  {},
543
543
  context
544
544
  );
545
- return finalResponse.data;
545
+ return finalResponse;
546
546
  } catch (error) {
547
547
  if (onError) {
548
548
  await onError(error, { phase: 'handleComplete', attempt: handleAttempt });