@noatgnu/cupcake-core 1.2.17 → 1.3.1

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.
@@ -2,9 +2,9 @@ import * as i0 from '@angular/core';
2
2
  import { inject, InjectionToken, Injectable, signal, computed, Component, effect, ChangeDetectorRef, NgModule } from '@angular/core';
3
3
  import * as i1 from '@angular/common/http';
4
4
  import { HttpClient, HttpParams, provideHttpClient, withInterceptors, HttpClientModule } from '@angular/common/http';
5
- import { BehaviorSubject, catchError, throwError, switchMap, filter, take, map, tap, Subject, interval, timer, EMPTY, debounceTime, distinctUntilChanged } from 'rxjs';
5
+ import { BehaviorSubject, catchError, throwError, switchMap, filter, take, map, tap, Subject, timer, EMPTY, interval, debounceTime, distinctUntilChanged } from 'rxjs';
6
6
  import { Router, ActivatedRoute, RouterModule } from '@angular/router';
7
- import { map as map$1, tap as tap$1, takeUntil, switchMap as switchMap$1 } from 'rxjs/operators';
7
+ import { map as map$1, takeUntil, tap as tap$1, switchMap as switchMap$1, filter as filter$1 } from 'rxjs/operators';
8
8
  import * as i1$1 from '@angular/forms';
9
9
  import { FormBuilder, Validators, ReactiveFormsModule, FormsModule, NonNullableFormBuilder } from '@angular/forms';
10
10
  import * as i2 from '@angular/common';
@@ -89,6 +89,56 @@ var AnnotationType;
89
89
  AnnotationType["Booking"] = "booking";
90
90
  })(AnnotationType || (AnnotationType = {}));
91
91
 
92
+ var TaskStatus;
93
+ (function (TaskStatus) {
94
+ TaskStatus["QUEUED"] = "QUEUED";
95
+ TaskStatus["STARTED"] = "STARTED";
96
+ TaskStatus["SUCCESS"] = "SUCCESS";
97
+ TaskStatus["FAILURE"] = "FAILURE";
98
+ TaskStatus["CANCELLED"] = "CANCELLED";
99
+ })(TaskStatus || (TaskStatus = {}));
100
+ var TaskType;
101
+ (function (TaskType) {
102
+ TaskType["EXPORT_EXCEL"] = "EXPORT_EXCEL";
103
+ TaskType["EXPORT_SDRF"] = "EXPORT_SDRF";
104
+ TaskType["IMPORT_SDRF"] = "IMPORT_SDRF";
105
+ TaskType["IMPORT_EXCEL"] = "IMPORT_EXCEL";
106
+ TaskType["EXPORT_MULTIPLE_SDRF"] = "EXPORT_MULTIPLE_SDRF";
107
+ TaskType["EXPORT_MULTIPLE_EXCEL"] = "EXPORT_MULTIPLE_EXCEL";
108
+ TaskType["VALIDATE_TABLE"] = "VALIDATE_TABLE";
109
+ TaskType["REORDER_TABLE_COLUMNS"] = "REORDER_TABLE_COLUMNS";
110
+ TaskType["REORDER_TEMPLATE_COLUMNS"] = "REORDER_TEMPLATE_COLUMNS";
111
+ TaskType["TRANSCRIBE_AUDIO"] = "TRANSCRIBE_AUDIO";
112
+ TaskType["TRANSCRIBE_VIDEO"] = "TRANSCRIBE_VIDEO";
113
+ })(TaskType || (TaskType = {}));
114
+ const TASK_TYPE_LABELS = {
115
+ [TaskType.EXPORT_EXCEL]: 'Export Excel Template',
116
+ [TaskType.EXPORT_SDRF]: 'Export SDRF File',
117
+ [TaskType.IMPORT_SDRF]: 'Import SDRF File',
118
+ [TaskType.IMPORT_EXCEL]: 'Import Excel File',
119
+ [TaskType.EXPORT_MULTIPLE_SDRF]: 'Export Multiple SDRF Files',
120
+ [TaskType.EXPORT_MULTIPLE_EXCEL]: 'Export Multiple Excel Templates',
121
+ [TaskType.VALIDATE_TABLE]: 'Validate Metadata Table',
122
+ [TaskType.REORDER_TABLE_COLUMNS]: 'Reorder Table Columns',
123
+ [TaskType.REORDER_TEMPLATE_COLUMNS]: 'Reorder Template Columns',
124
+ [TaskType.TRANSCRIBE_AUDIO]: 'Transcribe Audio',
125
+ [TaskType.TRANSCRIBE_VIDEO]: 'Transcribe Video',
126
+ };
127
+ const TASK_STATUS_LABELS = {
128
+ [TaskStatus.QUEUED]: 'Queued',
129
+ [TaskStatus.STARTED]: 'In Progress',
130
+ [TaskStatus.SUCCESS]: 'Completed',
131
+ [TaskStatus.FAILURE]: 'Failed',
132
+ [TaskStatus.CANCELLED]: 'Cancelled',
133
+ };
134
+ const TASK_STATUS_COLORS = {
135
+ [TaskStatus.QUEUED]: 'secondary',
136
+ [TaskStatus.STARTED]: 'primary',
137
+ [TaskStatus.SUCCESS]: 'success',
138
+ [TaskStatus.FAILURE]: 'danger',
139
+ [TaskStatus.CANCELLED]: 'warning',
140
+ };
141
+
92
142
  /**
93
143
  * CUPCAKE Core (CCC) - Models barrel export
94
144
  * User management, lab groups, and core functionality interfaces
@@ -1114,86 +1164,484 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
1114
1164
  }]
1115
1165
  }] });
1116
1166
 
1117
- class ToastService {
1118
- toastsSignal = signal([], ...(ngDevMode ? [{ debugName: "toastsSignal" }] : []));
1119
- toasts = this.toastsSignal.asReadonly();
1120
- show(message, type = 'info', duration = 5000) {
1121
- const id = this.generateId();
1122
- const toast = {
1123
- id,
1124
- message,
1125
- type,
1126
- duration,
1127
- dismissible: true
1167
+ const WEBSOCKET_ENDPOINT = new InjectionToken('WEBSOCKET_ENDPOINT', {
1168
+ providedIn: 'root',
1169
+ factory: () => 'notifications'
1170
+ });
1171
+ class WebSocketService {
1172
+ authService;
1173
+ ws = null;
1174
+ config;
1175
+ destroy$ = new Subject();
1176
+ reconnectAttempts = 0;
1177
+ isConnecting = false;
1178
+ connectionState = signal('disconnected', ...(ngDevMode ? [{ debugName: "connectionState" }] : []));
1179
+ lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : []));
1180
+ messageSubject = new Subject();
1181
+ connectionSubject = new BehaviorSubject(false);
1182
+ messages$ = this.messageSubject.asObservable();
1183
+ isConnected$ = this.connectionSubject.asObservable();
1184
+ connectionState$ = computed(() => this.connectionState(), ...(ngDevMode ? [{ debugName: "connectionState$" }] : []));
1185
+ lastError$ = computed(() => this.lastError(), ...(ngDevMode ? [{ debugName: "lastError$" }] : []));
1186
+ config_token = inject(CUPCAKE_CORE_CONFIG);
1187
+ endpoint = inject(WEBSOCKET_ENDPOINT, { optional: true }) || 'notifications';
1188
+ constructor(authService) {
1189
+ this.authService = authService;
1190
+ this.config = {
1191
+ url: this.getWebSocketUrl(),
1192
+ endpoint: this.endpoint,
1193
+ reconnectInterval: this.getAdaptiveReconnectInterval(),
1194
+ maxReconnectAttempts: 3
1128
1195
  };
1129
- this.toastsSignal.update(toasts => [...toasts, toast]);
1130
- if (duration > 0) {
1131
- setTimeout(() => {
1132
- this.remove(id);
1133
- }, duration);
1196
+ this.authService.isAuthenticated$.subscribe(isAuthenticated => {
1197
+ if (!isAuthenticated && this.ws) {
1198
+ console.log('User logged out - disconnecting WebSocket');
1199
+ this.disconnect();
1200
+ }
1201
+ });
1202
+ this.setupBrowserResourceHandling();
1203
+ }
1204
+ getWebSocketUrl() {
1205
+ const apiUrl = this.config_token.apiUrl;
1206
+ try {
1207
+ const url = new URL(apiUrl);
1208
+ const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
1209
+ const host = url.host;
1210
+ const endpoint = this.config?.endpoint || 'notifications';
1211
+ return `${protocol}//${host}/ws/${endpoint}/`;
1212
+ }
1213
+ catch (error) {
1214
+ console.error('Invalid API URL in cupcake config:', apiUrl);
1215
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1216
+ const host = window.location.host;
1217
+ const endpoint = this.config?.endpoint || 'notifications';
1218
+ return `${protocol}//${host}/ws/${endpoint}/`;
1134
1219
  }
1135
1220
  }
1136
- success(message, duration = 5000) {
1137
- this.show(message, 'success', duration);
1221
+ connect() {
1222
+ console.log('🔌 WebSocket connect() called');
1223
+ console.log('🔌 Current connection state:', this.connectionState());
1224
+ console.log('🔌 WebSocket URL:', this.config.url);
1225
+ if (this.isConnecting) {
1226
+ console.log('WebSocket connection already in progress');
1227
+ return;
1228
+ }
1229
+ if (this.ws?.readyState === WebSocket.OPEN) {
1230
+ console.log('WebSocket already connected');
1231
+ return;
1232
+ }
1233
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
1234
+ console.log('Closing existing WebSocket connection');
1235
+ this.ws.close();
1236
+ this.ws = null;
1237
+ }
1238
+ const token = this.authService.getAccessToken();
1239
+ if (!token) {
1240
+ console.error('❌ Cannot connect WebSocket - no authentication token');
1241
+ this.lastError.set('Authentication required');
1242
+ this.connectionState.set('error');
1243
+ return;
1244
+ }
1245
+ this.isConnecting = true;
1246
+ this.connectionState.set('connecting');
1247
+ this.lastError.set(null);
1248
+ try {
1249
+ const wsUrl = `${this.config.url}?token=${encodeURIComponent(token)}`;
1250
+ console.log('Connecting to WebSocket:', wsUrl.replace(token, '[TOKEN_HIDDEN]'));
1251
+ this.ws = new WebSocket(wsUrl);
1252
+ this.ws.onopen = this.onOpen.bind(this);
1253
+ this.ws.onmessage = this.onMessage.bind(this);
1254
+ this.ws.onerror = this.onError.bind(this);
1255
+ this.ws.onclose = this.onClose.bind(this);
1256
+ const connectionTimeout = setTimeout(() => {
1257
+ if (this.ws?.readyState === WebSocket.CONNECTING) {
1258
+ console.warn('WebSocket connection timeout - closing');
1259
+ this.ws.close();
1260
+ this.lastError.set('Connection timeout');
1261
+ this.connectionState.set('error');
1262
+ this.isConnecting = false;
1263
+ }
1264
+ }, 10000);
1265
+ this.ws.onopen = (event) => {
1266
+ clearTimeout(connectionTimeout);
1267
+ this.onOpen(event);
1268
+ };
1269
+ }
1270
+ catch (error) {
1271
+ console.error('WebSocket connection error:', error);
1272
+ this.connectionState.set('error');
1273
+ this.lastError.set('Connection failed');
1274
+ this.isConnecting = false;
1275
+ }
1138
1276
  }
1139
- error(message, duration = 8000) {
1140
- this.show(message, 'error', duration);
1277
+ disconnect() {
1278
+ this.destroy$.next();
1279
+ this.isConnecting = false;
1280
+ if (this.ws) {
1281
+ this.ws.close(1000, 'User disconnected');
1282
+ this.ws = null;
1283
+ }
1284
+ this.connectionState.set('disconnected');
1285
+ this.connectionSubject.next(false);
1286
+ this.reconnectAttempts = 0;
1141
1287
  }
1142
- warning(message, duration = 6000) {
1143
- this.show(message, 'warning', duration);
1288
+ send(message) {
1289
+ if (this.ws?.readyState === WebSocket.OPEN) {
1290
+ this.ws.send(JSON.stringify(message));
1291
+ }
1292
+ else {
1293
+ console.warn('WebSocket not connected, cannot send message:', message);
1294
+ }
1144
1295
  }
1145
- info(message, duration = 5000) {
1146
- this.show(message, 'info', duration);
1296
+ subscribe(subscriptionType, options = {}) {
1297
+ this.send({
1298
+ type: 'subscribe',
1299
+ subscription_type: subscriptionType,
1300
+ ...options
1301
+ });
1147
1302
  }
1148
- remove(id) {
1149
- this.toastsSignal.update(toasts => toasts.filter(toast => toast.id !== id));
1303
+ onOpen(event) {
1304
+ console.log('WebSocket connected');
1305
+ this.isConnecting = false;
1306
+ this.connectionState.set('connected');
1307
+ this.connectionSubject.next(true);
1308
+ this.reconnectAttempts = 0;
1309
+ this.lastError.set(null);
1150
1310
  }
1151
- clear() {
1152
- this.toastsSignal.set([]);
1311
+ onMessage(event) {
1312
+ try {
1313
+ const data = JSON.parse(event.data);
1314
+ console.log('WebSocket message received:', data);
1315
+ this.messageSubject.next(data);
1316
+ }
1317
+ catch (error) {
1318
+ console.error('Error parsing WebSocket message:', error);
1319
+ }
1153
1320
  }
1154
- generateId() {
1155
- return Math.random().toString(36).substring(2) + Date.now().toString(36);
1321
+ onError(event) {
1322
+ console.error('WebSocket error:', event);
1323
+ this.isConnecting = false;
1324
+ this.connectionState.set('error');
1325
+ this.lastError.set('Connection error occurred');
1156
1326
  }
1157
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1158
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ToastService, providedIn: 'root' });
1159
- }
1160
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ToastService, decorators: [{
1161
- type: Injectable,
1162
- args: [{
1163
- providedIn: 'root'
1164
- }]
1165
- }] });
1166
-
1167
- class NotificationService {
1168
- toastService;
1169
- notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : []));
1170
- notificationSubject = new Subject();
1171
- allNotifications = computed(() => this.notifications(), ...(ngDevMode ? [{ debugName: "allNotifications" }] : []));
1172
- unreadNotifications = computed(() => this.notifications().filter(n => !n.read), ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : []));
1173
- unreadCount = computed(() => this.unreadNotifications().length, ...(ngDevMode ? [{ debugName: "unreadCount" }] : []));
1174
- notification$ = this.notificationSubject.asObservable();
1175
- constructor(toastService) {
1176
- this.toastService = toastService;
1327
+ onClose(event) {
1328
+ console.log(`WebSocket closed: ${event.code} ${event.reason}`);
1329
+ this.isConnecting = false;
1330
+ this.connectionState.set('disconnected');
1331
+ this.connectionSubject.next(false);
1332
+ if (event.code === 4001) {
1333
+ this.lastError.set('Authentication failed');
1334
+ console.error('WebSocket authentication failed');
1335
+ return;
1336
+ }
1337
+ else if (event.code === 4003) {
1338
+ this.lastError.set('Insufficient permissions');
1339
+ console.error('WebSocket permission denied');
1340
+ return;
1341
+ }
1342
+ if (event.code !== 1000 && this.reconnectAttempts < (this.config.maxReconnectAttempts || 5)) {
1343
+ this.attemptReconnection();
1344
+ }
1177
1345
  }
1178
- addNotification(notification) {
1179
- this.notifications.update(notifications => {
1180
- const updated = [notification, ...notifications];
1181
- return updated.slice(0, 100);
1182
- });
1183
- this.notificationSubject.next(notification);
1184
- console.log('Notification added:', notification);
1346
+ attemptReconnection() {
1347
+ this.reconnectAttempts++;
1348
+ const delay = this.config.reconnectInterval || 5000;
1349
+ console.log(`WebSocket reconnection attempt ${this.reconnectAttempts} in ${delay}ms`);
1350
+ timer(delay).pipe(takeUntil(this.destroy$), tap$1(() => {
1351
+ if (this.reconnectAttempts <= (this.config.maxReconnectAttempts || 5)) {
1352
+ this.connect();
1353
+ }
1354
+ else {
1355
+ console.error('Max WebSocket reconnection attempts reached');
1356
+ this.lastError.set('Connection failed - max attempts reached');
1357
+ }
1358
+ })).subscribe();
1185
1359
  }
1186
- generateNotificationId() {
1187
- return `notification_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1360
+ filterMessages(type) {
1361
+ return this.messages$.pipe(tap$1(msg => console.log('Filtering message:', msg.type, 'looking for:', type)), switchMap$1(message => message.type === type ? [message] : EMPTY));
1188
1362
  }
1189
- markAsRead(notificationId) {
1190
- this.notifications.update(notifications => notifications.map(n => n.id === notificationId ? { ...n, read: true } : n));
1363
+ getNotifications() {
1364
+ return this.filterMessages('notification');
1191
1365
  }
1192
- markAllAsRead() {
1193
- this.notifications.update(notifications => notifications.map(n => ({ ...n, read: true })));
1366
+ getSystemNotifications() {
1367
+ return this.filterMessages('system.notification');
1194
1368
  }
1195
- removeNotification(notificationId) {
1196
- this.notifications.update(notifications => notifications.filter(n => n.id !== notificationId));
1369
+ reconnectWithNewToken() {
1370
+ if (this.ws?.readyState === WebSocket.OPEN) {
1371
+ console.log('Reconnecting WebSocket with new token');
1372
+ this.disconnect();
1373
+ setTimeout(() => this.connect(), 100);
1374
+ }
1375
+ }
1376
+ shouldConnect() {
1377
+ return this.authService.isAuthenticated() && !!this.authService.getAccessToken();
1378
+ }
1379
+ updateConfig() {
1380
+ this.config.url = this.getWebSocketUrl();
1381
+ console.log('WebSocket URL updated to:', this.config.url);
1382
+ }
1383
+ ngOnDestroy() {
1384
+ this.disconnect();
1385
+ }
1386
+ getAdaptiveReconnectInterval() {
1387
+ const baseInterval = 5000;
1388
+ const tabCount = this.estimateTabCount();
1389
+ if (tabCount > 20) {
1390
+ return baseInterval * 3;
1391
+ }
1392
+ else if (tabCount > 10) {
1393
+ return baseInterval * 2;
1394
+ }
1395
+ return baseInterval;
1396
+ }
1397
+ estimateTabCount() {
1398
+ try {
1399
+ if ('memory' in performance) {
1400
+ const memory = performance.memory;
1401
+ const usedMB = memory.usedJSHeapSize / (1024 * 1024);
1402
+ if (usedMB > 500)
1403
+ return 25;
1404
+ if (usedMB > 300)
1405
+ return 15;
1406
+ if (usedMB > 150)
1407
+ return 10;
1408
+ return 5;
1409
+ }
1410
+ return 10;
1411
+ }
1412
+ catch (error) {
1413
+ return 10;
1414
+ }
1415
+ }
1416
+ canConnectSafely() {
1417
+ try {
1418
+ if ('memory' in performance) {
1419
+ const memory = performance.memory;
1420
+ const usedMB = memory.usedJSHeapSize / (1024 * 1024);
1421
+ const totalMB = memory.totalJSHeapSize / (1024 * 1024);
1422
+ const memoryUsageRatio = usedMB / totalMB;
1423
+ if (memoryUsageRatio > 0.9) {
1424
+ console.warn('High memory usage detected, delaying WebSocket connection');
1425
+ return false;
1426
+ }
1427
+ }
1428
+ const tabCount = this.estimateTabCount();
1429
+ if (tabCount > 30) {
1430
+ console.warn('Too many tabs detected, may affect WebSocket reliability');
1431
+ return false;
1432
+ }
1433
+ return true;
1434
+ }
1435
+ catch (error) {
1436
+ return true;
1437
+ }
1438
+ }
1439
+ setupBrowserResourceHandling() {
1440
+ document.addEventListener('visibilitychange', () => {
1441
+ if (document.visibilityState === 'visible') {
1442
+ if (this.shouldConnect() && this.connectionState() === 'disconnected') {
1443
+ console.log('Tab became active - attempting WebSocket reconnection');
1444
+ setTimeout(() => this.connect(), 1000);
1445
+ }
1446
+ }
1447
+ else {
1448
+ const tabCount = this.estimateTabCount();
1449
+ if (tabCount > 15 && this.ws?.readyState === WebSocket.OPEN) {
1450
+ console.log('Tab inactive with high tab count - maintaining connection with reduced activity');
1451
+ }
1452
+ }
1453
+ });
1454
+ window.addEventListener('beforeunload', () => {
1455
+ if (this.ws) {
1456
+ this.ws.close(1000, 'Page unloading');
1457
+ }
1458
+ });
1459
+ }
1460
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: WebSocketService, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Injectable });
1461
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: WebSocketService, providedIn: 'root' });
1462
+ }
1463
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: WebSocketService, decorators: [{
1464
+ type: Injectable,
1465
+ args: [{
1466
+ providedIn: 'root'
1467
+ }]
1468
+ }], ctorParameters: () => [{ type: AuthService }] });
1469
+
1470
+ class AsyncTaskMonitorService extends BaseApiService {
1471
+ destroy$ = new Subject();
1472
+ tasksSubject = new BehaviorSubject([]);
1473
+ isSubscribed = false;
1474
+ websocket = inject(WebSocketService);
1475
+ tasks$ = this.tasksSubject.asObservable();
1476
+ activeTasks$ = this.tasks$.pipe(map$1(tasks => {
1477
+ const taskArray = Array.isArray(tasks) ? tasks : [];
1478
+ const active = taskArray.filter(task => task.status === TaskStatus.QUEUED || task.status === TaskStatus.STARTED);
1479
+ console.log('AsyncTaskMonitorService: activeTasks$ emitting:', active.length, 'active tasks out of', taskArray.length, 'total');
1480
+ return active;
1481
+ }));
1482
+ ngOnDestroy() {
1483
+ this.destroy$.next();
1484
+ this.destroy$.complete();
1485
+ }
1486
+ startRealtimeUpdates() {
1487
+ if (this.isSubscribed) {
1488
+ console.log('AsyncTaskMonitorService: Already subscribed to real-time updates');
1489
+ return;
1490
+ }
1491
+ console.log('AsyncTaskMonitorService: Starting real-time updates');
1492
+ this.isSubscribed = true;
1493
+ if (!this.websocket.connectionState$() || this.websocket.connectionState$() === 'disconnected') {
1494
+ console.log('AsyncTaskMonitorService: WebSocket not connected, connecting now...');
1495
+ this.websocket.connect();
1496
+ }
1497
+ else {
1498
+ console.log('AsyncTaskMonitorService: WebSocket already connected');
1499
+ }
1500
+ this.websocket.isConnected$.pipe(takeUntil(this.destroy$), filter$1(connected => connected)).subscribe(() => {
1501
+ console.log('AsyncTaskMonitorService: WebSocket connected, subscribing to async_task_updates');
1502
+ this.websocket.subscribe('async_task_updates');
1503
+ });
1504
+ this.websocket.filterMessages('async_task.update').pipe(takeUntil(this.destroy$)).subscribe((message) => {
1505
+ console.log('AsyncTaskMonitorService: Received async_task.update message:', message);
1506
+ this.handleTaskUpdate(message);
1507
+ });
1508
+ this.loadAllTasks();
1509
+ }
1510
+ stopRealtimeUpdates() {
1511
+ console.log('AsyncTaskMonitorService: Stopping real-time updates');
1512
+ this.isSubscribed = false;
1513
+ this.destroy$.next();
1514
+ }
1515
+ loadAllTasks() {
1516
+ const httpParams = this.buildHttpParams({ limit: 100 });
1517
+ this.get(`${this.apiUrl}/async-tasks/`, { params: httpParams }).subscribe({
1518
+ next: (response) => {
1519
+ const taskArray = Array.isArray(response.results) ? response.results : [];
1520
+ console.log('AsyncTaskMonitorService: Loaded tasks from server:', taskArray.length);
1521
+ this.tasksSubject.next(taskArray);
1522
+ },
1523
+ error: (error) => {
1524
+ console.error('AsyncTaskMonitorService: Error loading tasks:', error);
1525
+ }
1526
+ });
1527
+ }
1528
+ handleTaskUpdate(message) {
1529
+ console.log('AsyncTaskMonitorService: handleTaskUpdate called with message:', message);
1530
+ const taskId = message.task_id;
1531
+ const currentTasks = this.tasksSubject.value;
1532
+ console.log('AsyncTaskMonitorService: Current tasks in subject:', currentTasks.length);
1533
+ const existingTaskIndex = currentTasks.findIndex(t => t.id === taskId);
1534
+ console.log('AsyncTaskMonitorService: Existing task index:', existingTaskIndex);
1535
+ if (existingTaskIndex >= 0) {
1536
+ const existingTask = currentTasks[existingTaskIndex];
1537
+ const updatedTask = {
1538
+ ...existingTask,
1539
+ status: message.status,
1540
+ progressPercentage: message.progress_percentage || existingTask.progressPercentage,
1541
+ progressDescription: message.progress_description || existingTask.progressDescription,
1542
+ errorMessage: message.error_message || existingTask.errorMessage,
1543
+ result: message.result || existingTask.result,
1544
+ };
1545
+ const updatedTasks = [...currentTasks];
1546
+ updatedTasks[existingTaskIndex] = updatedTask;
1547
+ console.log('AsyncTaskMonitorService: Emitting updated tasks:', updatedTasks.length, 'active tasks:', updatedTasks.filter(t => t.status === TaskStatus.QUEUED || t.status === TaskStatus.STARTED).length);
1548
+ this.tasksSubject.next(updatedTasks);
1549
+ }
1550
+ else {
1551
+ console.log('AsyncTaskMonitorService: Task not found in current list, fetching all tasks');
1552
+ this.loadAllTasks();
1553
+ }
1554
+ }
1555
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: AsyncTaskMonitorService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1556
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: AsyncTaskMonitorService, providedIn: 'root' });
1557
+ }
1558
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: AsyncTaskMonitorService, decorators: [{
1559
+ type: Injectable,
1560
+ args: [{
1561
+ providedIn: 'root'
1562
+ }]
1563
+ }] });
1564
+
1565
+ class ToastService {
1566
+ toastsSignal = signal([], ...(ngDevMode ? [{ debugName: "toastsSignal" }] : []));
1567
+ toasts = this.toastsSignal.asReadonly();
1568
+ show(message, type = 'info', duration = 5000) {
1569
+ const id = this.generateId();
1570
+ const toast = {
1571
+ id,
1572
+ message,
1573
+ type,
1574
+ duration,
1575
+ dismissible: true
1576
+ };
1577
+ this.toastsSignal.update(toasts => [...toasts, toast]);
1578
+ if (duration > 0) {
1579
+ setTimeout(() => {
1580
+ this.remove(id);
1581
+ }, duration);
1582
+ }
1583
+ }
1584
+ success(message, duration = 5000) {
1585
+ this.show(message, 'success', duration);
1586
+ }
1587
+ error(message, duration = 8000) {
1588
+ this.show(message, 'error', duration);
1589
+ }
1590
+ warning(message, duration = 6000) {
1591
+ this.show(message, 'warning', duration);
1592
+ }
1593
+ info(message, duration = 5000) {
1594
+ this.show(message, 'info', duration);
1595
+ }
1596
+ remove(id) {
1597
+ this.toastsSignal.update(toasts => toasts.filter(toast => toast.id !== id));
1598
+ }
1599
+ clear() {
1600
+ this.toastsSignal.set([]);
1601
+ }
1602
+ generateId() {
1603
+ return Math.random().toString(36).substring(2) + Date.now().toString(36);
1604
+ }
1605
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1606
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ToastService, providedIn: 'root' });
1607
+ }
1608
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ToastService, decorators: [{
1609
+ type: Injectable,
1610
+ args: [{
1611
+ providedIn: 'root'
1612
+ }]
1613
+ }] });
1614
+
1615
+ class NotificationService {
1616
+ toastService;
1617
+ notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : []));
1618
+ notificationSubject = new Subject();
1619
+ allNotifications = computed(() => this.notifications(), ...(ngDevMode ? [{ debugName: "allNotifications" }] : []));
1620
+ unreadNotifications = computed(() => this.notifications().filter(n => !n.read), ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : []));
1621
+ unreadCount = computed(() => this.unreadNotifications().length, ...(ngDevMode ? [{ debugName: "unreadCount" }] : []));
1622
+ notification$ = this.notificationSubject.asObservable();
1623
+ constructor(toastService) {
1624
+ this.toastService = toastService;
1625
+ }
1626
+ addNotification(notification) {
1627
+ this.notifications.update(notifications => {
1628
+ const updated = [notification, ...notifications];
1629
+ return updated.slice(0, 100);
1630
+ });
1631
+ this.notificationSubject.next(notification);
1632
+ console.log('Notification added:', notification);
1633
+ }
1634
+ generateNotificationId() {
1635
+ return `notification_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1636
+ }
1637
+ markAsRead(notificationId) {
1638
+ this.notifications.update(notifications => notifications.map(n => n.id === notificationId ? { ...n, read: true } : n));
1639
+ }
1640
+ markAllAsRead() {
1641
+ this.notifications.update(notifications => notifications.map(n => ({ ...n, read: true })));
1642
+ }
1643
+ removeNotification(notificationId) {
1644
+ this.notifications.update(notifications => notifications.filter(n => n.id !== notificationId));
1197
1645
  }
1198
1646
  clearAll() {
1199
1647
  this.notifications.set([]);
@@ -1486,318 +1934,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
1486
1934
  }]
1487
1935
  }], ctorParameters: () => [] });
1488
1936
 
1489
- const WEBSOCKET_ENDPOINT = new InjectionToken('WEBSOCKET_ENDPOINT', {
1490
- providedIn: 'root',
1491
- factory: () => 'notifications'
1492
- });
1493
- class WebSocketService {
1494
- authService;
1495
- ws = null;
1496
- config;
1497
- destroy$ = new Subject();
1498
- reconnectAttempts = 0;
1499
- isConnecting = false;
1500
- connectionState = signal('disconnected', ...(ngDevMode ? [{ debugName: "connectionState" }] : []));
1501
- lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : []));
1502
- messageSubject = new Subject();
1503
- connectionSubject = new BehaviorSubject(false);
1504
- messages$ = this.messageSubject.asObservable();
1505
- isConnected$ = this.connectionSubject.asObservable();
1506
- connectionState$ = computed(() => this.connectionState(), ...(ngDevMode ? [{ debugName: "connectionState$" }] : []));
1507
- lastError$ = computed(() => this.lastError(), ...(ngDevMode ? [{ debugName: "lastError$" }] : []));
1508
- config_token = inject(CUPCAKE_CORE_CONFIG);
1509
- endpoint = inject(WEBSOCKET_ENDPOINT, { optional: true }) || 'notifications';
1510
- constructor(authService) {
1511
- this.authService = authService;
1512
- this.config = {
1513
- url: this.getWebSocketUrl(),
1514
- endpoint: this.endpoint,
1515
- reconnectInterval: this.getAdaptiveReconnectInterval(),
1516
- maxReconnectAttempts: 3
1517
- };
1518
- this.authService.isAuthenticated$.subscribe(isAuthenticated => {
1519
- if (!isAuthenticated && this.ws) {
1520
- console.log('User logged out - disconnecting WebSocket');
1521
- this.disconnect();
1522
- }
1523
- });
1524
- this.setupBrowserResourceHandling();
1525
- }
1526
- getWebSocketUrl() {
1527
- const apiUrl = this.config_token.apiUrl;
1528
- try {
1529
- const url = new URL(apiUrl);
1530
- const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
1531
- const host = url.host;
1532
- const endpoint = this.config?.endpoint || 'notifications';
1533
- return `${protocol}//${host}/ws/${endpoint}/`;
1534
- }
1535
- catch (error) {
1536
- console.error('Invalid API URL in cupcake config:', apiUrl);
1537
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1538
- const host = window.location.host;
1539
- const endpoint = this.config?.endpoint || 'notifications';
1540
- return `${protocol}//${host}/ws/${endpoint}/`;
1541
- }
1542
- }
1543
- connect() {
1544
- console.log('🔌 WebSocket connect() called');
1545
- console.log('🔌 Current connection state:', this.connectionState());
1546
- console.log('🔌 WebSocket URL:', this.config.url);
1547
- if (this.isConnecting) {
1548
- console.log('WebSocket connection already in progress');
1549
- return;
1550
- }
1551
- if (this.ws?.readyState === WebSocket.OPEN) {
1552
- console.log('WebSocket already connected');
1553
- return;
1554
- }
1555
- if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
1556
- console.log('Closing existing WebSocket connection');
1557
- this.ws.close();
1558
- this.ws = null;
1559
- }
1560
- const token = this.authService.getAccessToken();
1561
- if (!token) {
1562
- console.error('❌ Cannot connect WebSocket - no authentication token');
1563
- this.lastError.set('Authentication required');
1564
- this.connectionState.set('error');
1565
- return;
1566
- }
1567
- this.isConnecting = true;
1568
- this.connectionState.set('connecting');
1569
- this.lastError.set(null);
1570
- try {
1571
- const wsUrl = `${this.config.url}?token=${encodeURIComponent(token)}`;
1572
- console.log('Connecting to WebSocket:', wsUrl.replace(token, '[TOKEN_HIDDEN]'));
1573
- this.ws = new WebSocket(wsUrl);
1574
- this.ws.onopen = this.onOpen.bind(this);
1575
- this.ws.onmessage = this.onMessage.bind(this);
1576
- this.ws.onerror = this.onError.bind(this);
1577
- this.ws.onclose = this.onClose.bind(this);
1578
- const connectionTimeout = setTimeout(() => {
1579
- if (this.ws?.readyState === WebSocket.CONNECTING) {
1580
- console.warn('WebSocket connection timeout - closing');
1581
- this.ws.close();
1582
- this.lastError.set('Connection timeout');
1583
- this.connectionState.set('error');
1584
- this.isConnecting = false;
1585
- }
1586
- }, 10000);
1587
- this.ws.onopen = (event) => {
1588
- clearTimeout(connectionTimeout);
1589
- this.onOpen(event);
1590
- };
1591
- }
1592
- catch (error) {
1593
- console.error('WebSocket connection error:', error);
1594
- this.connectionState.set('error');
1595
- this.lastError.set('Connection failed');
1596
- this.isConnecting = false;
1597
- }
1598
- }
1599
- disconnect() {
1600
- this.destroy$.next();
1601
- this.isConnecting = false;
1602
- if (this.ws) {
1603
- this.ws.close(1000, 'User disconnected');
1604
- this.ws = null;
1605
- }
1606
- this.connectionState.set('disconnected');
1607
- this.connectionSubject.next(false);
1608
- this.reconnectAttempts = 0;
1609
- }
1610
- send(message) {
1611
- if (this.ws?.readyState === WebSocket.OPEN) {
1612
- this.ws.send(JSON.stringify(message));
1613
- }
1614
- else {
1615
- console.warn('WebSocket not connected, cannot send message:', message);
1616
- }
1617
- }
1618
- subscribe(subscriptionType, options = {}) {
1619
- this.send({
1620
- type: 'subscribe',
1621
- subscription_type: subscriptionType,
1622
- ...options
1623
- });
1624
- }
1625
- onOpen(event) {
1626
- console.log('WebSocket connected');
1627
- this.isConnecting = false;
1628
- this.connectionState.set('connected');
1629
- this.connectionSubject.next(true);
1630
- this.reconnectAttempts = 0;
1631
- this.lastError.set(null);
1632
- }
1633
- onMessage(event) {
1634
- try {
1635
- const data = JSON.parse(event.data);
1636
- console.log('WebSocket message received:', data);
1637
- this.messageSubject.next(data);
1638
- }
1639
- catch (error) {
1640
- console.error('Error parsing WebSocket message:', error);
1641
- }
1642
- }
1643
- onError(event) {
1644
- console.error('WebSocket error:', event);
1645
- this.isConnecting = false;
1646
- this.connectionState.set('error');
1647
- this.lastError.set('Connection error occurred');
1648
- }
1649
- onClose(event) {
1650
- console.log(`WebSocket closed: ${event.code} ${event.reason}`);
1651
- this.isConnecting = false;
1652
- this.connectionState.set('disconnected');
1653
- this.connectionSubject.next(false);
1654
- if (event.code === 4001) {
1655
- this.lastError.set('Authentication failed');
1656
- console.error('WebSocket authentication failed');
1657
- return;
1658
- }
1659
- else if (event.code === 4003) {
1660
- this.lastError.set('Insufficient permissions');
1661
- console.error('WebSocket permission denied');
1662
- return;
1663
- }
1664
- if (event.code !== 1000 && this.reconnectAttempts < (this.config.maxReconnectAttempts || 5)) {
1665
- this.attemptReconnection();
1666
- }
1667
- }
1668
- attemptReconnection() {
1669
- this.reconnectAttempts++;
1670
- const delay = this.config.reconnectInterval || 5000;
1671
- console.log(`WebSocket reconnection attempt ${this.reconnectAttempts} in ${delay}ms`);
1672
- timer(delay).pipe(takeUntil(this.destroy$), tap$1(() => {
1673
- if (this.reconnectAttempts <= (this.config.maxReconnectAttempts || 5)) {
1674
- this.connect();
1675
- }
1676
- else {
1677
- console.error('Max WebSocket reconnection attempts reached');
1678
- this.lastError.set('Connection failed - max attempts reached');
1679
- }
1680
- })).subscribe();
1681
- }
1682
- filterMessages(type) {
1683
- return this.messages$.pipe(tap$1(msg => console.log('Filtering message:', msg.type, 'looking for:', type)), switchMap$1(message => message.type === type ? [message] : EMPTY));
1684
- }
1685
- getNotifications() {
1686
- return this.filterMessages('notification');
1687
- }
1688
- getSystemNotifications() {
1689
- return this.filterMessages('system.notification');
1690
- }
1691
- reconnectWithNewToken() {
1692
- if (this.ws?.readyState === WebSocket.OPEN) {
1693
- console.log('Reconnecting WebSocket with new token');
1694
- this.disconnect();
1695
- setTimeout(() => this.connect(), 100);
1696
- }
1697
- }
1698
- shouldConnect() {
1699
- return this.authService.isAuthenticated() && !!this.authService.getAccessToken();
1700
- }
1701
- updateConfig() {
1702
- this.config.url = this.getWebSocketUrl();
1703
- console.log('WebSocket URL updated to:', this.config.url);
1704
- }
1705
- ngOnDestroy() {
1706
- this.disconnect();
1707
- }
1708
- getAdaptiveReconnectInterval() {
1709
- const baseInterval = 5000;
1710
- const tabCount = this.estimateTabCount();
1711
- if (tabCount > 20) {
1712
- return baseInterval * 3;
1713
- }
1714
- else if (tabCount > 10) {
1715
- return baseInterval * 2;
1716
- }
1717
- return baseInterval;
1718
- }
1719
- estimateTabCount() {
1720
- try {
1721
- if ('memory' in performance) {
1722
- const memory = performance.memory;
1723
- const usedMB = memory.usedJSHeapSize / (1024 * 1024);
1724
- if (usedMB > 500)
1725
- return 25;
1726
- if (usedMB > 300)
1727
- return 15;
1728
- if (usedMB > 150)
1729
- return 10;
1730
- return 5;
1731
- }
1732
- return 10;
1733
- }
1734
- catch (error) {
1735
- return 10;
1736
- }
1737
- }
1738
- canConnectSafely() {
1739
- try {
1740
- if ('memory' in performance) {
1741
- const memory = performance.memory;
1742
- const usedMB = memory.usedJSHeapSize / (1024 * 1024);
1743
- const totalMB = memory.totalJSHeapSize / (1024 * 1024);
1744
- const memoryUsageRatio = usedMB / totalMB;
1745
- if (memoryUsageRatio > 0.9) {
1746
- console.warn('High memory usage detected, delaying WebSocket connection');
1747
- return false;
1748
- }
1749
- }
1750
- const tabCount = this.estimateTabCount();
1751
- if (tabCount > 30) {
1752
- console.warn('Too many tabs detected, may affect WebSocket reliability');
1753
- return false;
1754
- }
1755
- return true;
1756
- }
1757
- catch (error) {
1758
- return true;
1759
- }
1760
- }
1761
- setupBrowserResourceHandling() {
1762
- document.addEventListener('visibilitychange', () => {
1763
- if (document.visibilityState === 'visible') {
1764
- if (this.shouldConnect() && this.connectionState() === 'disconnected') {
1765
- console.log('Tab became active - attempting WebSocket reconnection');
1766
- setTimeout(() => this.connect(), 1000);
1767
- }
1768
- }
1769
- else {
1770
- const tabCount = this.estimateTabCount();
1771
- if (tabCount > 15 && this.ws?.readyState === WebSocket.OPEN) {
1772
- console.log('Tab inactive with high tab count - maintaining connection with reduced activity');
1773
- }
1774
- }
1775
- });
1776
- window.addEventListener('beforeunload', () => {
1777
- if (this.ws) {
1778
- this.ws.close(1000, 'Page unloading');
1779
- }
1780
- });
1781
- }
1782
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: WebSocketService, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Injectable });
1783
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: WebSocketService, providedIn: 'root' });
1784
- }
1785
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: WebSocketService, decorators: [{
1786
- type: Injectable,
1787
- args: [{
1788
- providedIn: 'root'
1789
- }]
1790
- }], ctorParameters: () => [{ type: AuthService }] });
1791
-
1792
1937
  const WEBSOCKET_ENDPOINTS = new InjectionToken('WEBSOCKET_ENDPOINTS');
1793
1938
  class WebSocketEndpoints {
1794
1939
  static CORE_NOTIFICATIONS = 'notifications';
1795
1940
  static CORE_ADMIN = 'admin';
1796
- static CCV_NOTIFICATIONS = 'ccv/notifications';
1797
- static CCV_ADMIN = 'ccv/admin';
1798
- static CCM_NOTIFICATIONS = 'ccm/notifications';
1799
- static CCRV_NOTIFICATIONS = 'ccrv/notifications';
1800
- static CCSC_NOTIFICATIONS = 'ccsc/notifications';
1801
1941
  }
1802
1942
  class WebSocketConfigService {
1803
1943
  endpoints = new Map();
@@ -1815,31 +1955,6 @@ class WebSocketConfigService {
1815
1955
  endpoint: WebSocketEndpoints.CORE_ADMIN,
1816
1956
  description: 'Core admin notification endpoint'
1817
1957
  });
1818
- this.registerEndpoint({
1819
- app: 'ccv',
1820
- endpoint: WebSocketEndpoints.CCV_NOTIFICATIONS,
1821
- description: 'CCV notification endpoint'
1822
- });
1823
- this.registerEndpoint({
1824
- app: 'ccv',
1825
- endpoint: WebSocketEndpoints.CCV_ADMIN,
1826
- description: 'CCV admin notification endpoint'
1827
- });
1828
- this.registerEndpoint({
1829
- app: 'ccm',
1830
- endpoint: WebSocketEndpoints.CCM_NOTIFICATIONS,
1831
- description: 'CCM notification endpoint'
1832
- });
1833
- this.registerEndpoint({
1834
- app: 'ccrv',
1835
- endpoint: WebSocketEndpoints.CCRV_NOTIFICATIONS,
1836
- description: 'CCRV notification endpoint'
1837
- });
1838
- this.registerEndpoint({
1839
- app: 'ccsc',
1840
- endpoint: WebSocketEndpoints.CCSC_NOTIFICATIONS,
1841
- description: 'CCSC notification endpoint'
1842
- });
1843
1958
  }
1844
1959
  registerEndpoint(config) {
1845
1960
  this.endpoints.set(config.endpoint, config);
@@ -3483,5 +3598,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
3483
3598
  * Generated bundle index. Do not edit.
3484
3599
  */
3485
3600
 
3486
- export { AnnotationType, ApiService, AuthService, BaseApiService, CUPCAKE_CORE_CONFIG, CupcakeCoreModule, InvitationStatus, InvitationStatusLabels, LabGroupService, LabGroupsComponent, LoginComponent, NotificationService, PoweredByFooterComponent, RegisterComponent, ResourceRole, ResourceRoleLabels, ResourceService, ResourceType, ResourceTypeLabels, ResourceVisibility, ResourceVisibilityLabels, SiteConfigComponent, SiteConfigService, ThemeService, ToastContainerComponent, ToastService, UserManagementComponent, UserManagementService, UserProfileComponent, WEBSOCKET_ENDPOINT, WEBSOCKET_ENDPOINTS, WebSocketConfigService, WebSocketEndpoints, WebSocketService, adminGuard, authGuard, authInterceptor, resetRefreshState };
3601
+ export { AnnotationType, ApiService, AsyncTaskMonitorService, AuthService, BaseApiService, CUPCAKE_CORE_CONFIG, CupcakeCoreModule, InvitationStatus, InvitationStatusLabels, LabGroupService, LabGroupsComponent, LoginComponent, NotificationService, PoweredByFooterComponent, RegisterComponent, ResourceRole, ResourceRoleLabels, ResourceService, ResourceType, ResourceTypeLabels, ResourceVisibility, ResourceVisibilityLabels, SiteConfigComponent, SiteConfigService, TASK_STATUS_COLORS, TASK_STATUS_LABELS, TASK_TYPE_LABELS, TaskStatus, TaskType, ThemeService, ToastContainerComponent, ToastService, UserManagementComponent, UserManagementService, UserProfileComponent, WEBSOCKET_ENDPOINT, WEBSOCKET_ENDPOINTS, WebSocketConfigService, WebSocketEndpoints, WebSocketService, adminGuard, authGuard, authInterceptor, resetRefreshState };
3487
3602
  //# sourceMappingURL=noatgnu-cupcake-core.mjs.map