mdas-jsview-sdk 1.0.17-uat.0 → 1.0.23-uat.0
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/dist/mdas-sdk.esm.js +622 -659
- package/dist/mdas-sdk.esm.js.map +1 -1
- package/dist/mdas-sdk.js +622 -659
- package/dist/mdas-sdk.js.map +1 -1
- package/dist/mdas-sdk.min.js +9 -9
- package/dist/mdas-sdk.min.js.map +1 -1
- package/package.json +1 -1
package/dist/mdas-sdk.esm.js
CHANGED
|
@@ -406,6 +406,85 @@ class BaseWidget {
|
|
|
406
406
|
console.log(`[${this.constructor.name}] Connection failed after ${status.maxAttempts} attempts`);
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Validate initial symbol with the API and handle access/permission errors
|
|
412
|
+
* This is a reusable method for all widgets that need to validate symbols on initialization
|
|
413
|
+
*
|
|
414
|
+
* @param {string} apiMethod - The API method name to call (e.g., 'quotel1', 'quoteOptionl1')
|
|
415
|
+
* @param {string} symbol - The symbol to validate
|
|
416
|
+
* @param {Function} onSuccess - Callback when validation succeeds with data, receives (data, result)
|
|
417
|
+
* @returns {boolean} - Returns false if access denied (stops initialization), true otherwise
|
|
418
|
+
*/
|
|
419
|
+
async validateInitialSymbol(apiMethod, symbol) {
|
|
420
|
+
let onSuccess = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
421
|
+
try {
|
|
422
|
+
const apiService = this.wsManager?.getApiService();
|
|
423
|
+
if (!apiService) {
|
|
424
|
+
if (this.debug) {
|
|
425
|
+
console.log(`[${this.constructor.name}] API service not available for initial validation`);
|
|
426
|
+
}
|
|
427
|
+
return true; // Continue without validation
|
|
428
|
+
}
|
|
429
|
+
if (!apiService[apiMethod]) {
|
|
430
|
+
console.error(`[${this.constructor.name}] API method ${apiMethod} does not exist`);
|
|
431
|
+
return true; // Continue without validation
|
|
432
|
+
}
|
|
433
|
+
const result = await apiService[apiMethod](symbol);
|
|
434
|
+
if (result && result.data && result.data.length > 0) {
|
|
435
|
+
if (this.debug) {
|
|
436
|
+
console.log(`[${this.constructor.name}] Initial symbol validated:`, {
|
|
437
|
+
symbol,
|
|
438
|
+
method: apiMethod,
|
|
439
|
+
dataCount: result.data.length
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Call success callback if provided
|
|
444
|
+
if (onSuccess && typeof onSuccess === 'function') {
|
|
445
|
+
onSuccess(result.data, result);
|
|
446
|
+
}
|
|
447
|
+
return true; // Validation successful
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// No data returned - might be access issue, but continue anyway
|
|
451
|
+
if (this.debug) {
|
|
452
|
+
console.log(`[${this.constructor.name}] No data returned from ${apiMethod} for ${symbol}`);
|
|
453
|
+
}
|
|
454
|
+
return true; // Continue anyway, WebSocket might still work
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (this.debug) {
|
|
457
|
+
console.warn(`[${this.constructor.name}] Initial symbol validation failed:`, error);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check if it's an access/permission error
|
|
461
|
+
const errorMessage = (error.message || '').toLowerCase();
|
|
462
|
+
const isAccessError = errorMessage.includes('400') ||
|
|
463
|
+
// Bad Request (often used for access issues)
|
|
464
|
+
errorMessage.includes('401') ||
|
|
465
|
+
// Unauthorized
|
|
466
|
+
errorMessage.includes('403') ||
|
|
467
|
+
// Forbidden
|
|
468
|
+
errorMessage.includes('forbidden') || errorMessage.includes('unauthorized') || errorMessage.includes('no access') || errorMessage.includes('no opra access') ||
|
|
469
|
+
// Specific OPRA access denial
|
|
470
|
+
errorMessage.includes('permission denied') || errorMessage.includes('access denied');
|
|
471
|
+
if (isAccessError) {
|
|
472
|
+
// Hide loading and show access error
|
|
473
|
+
this.hideLoading();
|
|
474
|
+
|
|
475
|
+
// Extract more specific error message if available
|
|
476
|
+
let userMessage = `Access denied: You don't have permission to view data for ${symbol}`;
|
|
477
|
+
if (errorMessage.includes('no opra access')) {
|
|
478
|
+
userMessage = `Access denied: You don't have OPRA access for option ${symbol}`;
|
|
479
|
+
}
|
|
480
|
+
this.showError(userMessage);
|
|
481
|
+
return false; // Stop initialization
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// For other errors, continue with WebSocket (might still work)
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
409
488
|
}
|
|
410
489
|
|
|
411
490
|
// src/models/MarketDataModel.js
|
|
@@ -1277,6 +1356,24 @@ const ONBBOLevel2Template = `
|
|
|
1277
1356
|
<span class="l2-market-name"></span>
|
|
1278
1357
|
</div>
|
|
1279
1358
|
<span class="symbol editable-symbol">--</span>
|
|
1359
|
+
<div class="level1-info">
|
|
1360
|
+
<div class="l1-item">
|
|
1361
|
+
<span class="l1-label">Last:</span>
|
|
1362
|
+
<span class="l1-value l1-last-px">--</span>
|
|
1363
|
+
</div>
|
|
1364
|
+
<div class="l1-item">
|
|
1365
|
+
<span class="l1-label">Low:</span>
|
|
1366
|
+
<span class="l1-value l1-low-px">--</span>
|
|
1367
|
+
</div>
|
|
1368
|
+
<div class="l1-item">
|
|
1369
|
+
<span class="l1-label">High:</span>
|
|
1370
|
+
<span class="l1-value l1-high-px">--</span>
|
|
1371
|
+
</div>
|
|
1372
|
+
<div class="l1-item">
|
|
1373
|
+
<span class="l1-label">Volume:</span>
|
|
1374
|
+
<span class="l1-value l1-volume">--</span>
|
|
1375
|
+
</div>
|
|
1376
|
+
</div>
|
|
1280
1377
|
</div>
|
|
1281
1378
|
</div>
|
|
1282
1379
|
|
|
@@ -1951,6 +2048,38 @@ const ONBBOLevel2Styles = `
|
|
|
1951
2048
|
color: #111827;
|
|
1952
2049
|
}
|
|
1953
2050
|
|
|
2051
|
+
/* ========================================
|
|
2052
|
+
LEVEL 1 INFO (INSIDE HEADER)
|
|
2053
|
+
======================================== */
|
|
2054
|
+
|
|
2055
|
+
.onbbo-level2-widget .level1-info {
|
|
2056
|
+
display: grid;
|
|
2057
|
+
grid-template-columns: repeat(4, 1fr);
|
|
2058
|
+
gap: 12px;
|
|
2059
|
+
margin-top: 10px;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
.onbbo-level2-widget .l1-item {
|
|
2063
|
+
display: flex;
|
|
2064
|
+
flex-direction: column;
|
|
2065
|
+
gap: 3px;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
.onbbo-level2-widget .l1-label {
|
|
2069
|
+
font-size: 11px;
|
|
2070
|
+
font-weight: 600;
|
|
2071
|
+
color: #6b7280;
|
|
2072
|
+
text-transform: uppercase;
|
|
2073
|
+
letter-spacing: 0.5px;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
.onbbo-level2-widget .l1-value {
|
|
2077
|
+
font-size: 15px;
|
|
2078
|
+
font-weight: 700;
|
|
2079
|
+
color: #111827;
|
|
2080
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1954
2083
|
/* ========================================
|
|
1955
2084
|
ORDER BOOK CONTAINER
|
|
1956
2085
|
======================================== */
|
|
@@ -2335,6 +2464,11 @@ const ONBBOLevel2Styles = `
|
|
|
2335
2464
|
.onbbo-level2-widget .orderbook-panel:last-child {
|
|
2336
2465
|
border-bottom: none;
|
|
2337
2466
|
}
|
|
2467
|
+
|
|
2468
|
+
.onbbo-level2-widget .level1-info {
|
|
2469
|
+
grid-template-columns: repeat(2, 1fr);
|
|
2470
|
+
gap: 6px;
|
|
2471
|
+
}
|
|
2338
2472
|
}
|
|
2339
2473
|
|
|
2340
2474
|
@media (max-width: 480px) {
|
|
@@ -2353,6 +2487,19 @@ const ONBBOLevel2Styles = `
|
|
|
2353
2487
|
text-align: left;
|
|
2354
2488
|
}
|
|
2355
2489
|
|
|
2490
|
+
.onbbo-level2-widget .level1-info {
|
|
2491
|
+
grid-template-columns: repeat(2, 1fr);
|
|
2492
|
+
gap: 4px;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
.onbbo-level2-widget .l1-label {
|
|
2496
|
+
font-size: 9px;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
.onbbo-level2-widget .l1-value {
|
|
2500
|
+
font-size: 11px;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2356
2503
|
.onbbo-level2-widget .panel-header {
|
|
2357
2504
|
font-size: 10px;
|
|
2358
2505
|
padding: 6px 2px;
|
|
@@ -2436,28 +2583,6 @@ const TimeSalesStyles = `
|
|
|
2436
2583
|
font-size: 1.71em;
|
|
2437
2584
|
font-weight: 700;
|
|
2438
2585
|
color: #111827;
|
|
2439
|
-
cursor: pointer;
|
|
2440
|
-
transition: all 0.2s ease;
|
|
2441
|
-
position: relative;
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
.time-sales-widget .symbol.editable-symbol:hover {
|
|
2445
|
-
opacity: 0.8;
|
|
2446
|
-
transform: scale(1.02);
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
.time-sales-widget .symbol.editable-symbol:hover::after {
|
|
2450
|
-
content: "✎";
|
|
2451
|
-
position: absolute;
|
|
2452
|
-
top: -8px;
|
|
2453
|
-
right: -8px;
|
|
2454
|
-
background: #3b82f6;
|
|
2455
|
-
color: white;
|
|
2456
|
-
font-size: 10px;
|
|
2457
|
-
padding: 2px 4px;
|
|
2458
|
-
border-radius: 4px;
|
|
2459
|
-
opacity: 0.8;
|
|
2460
|
-
pointer-events: none;
|
|
2461
2586
|
}
|
|
2462
2587
|
|
|
2463
2588
|
/* ========================================
|
|
@@ -4409,105 +4534,53 @@ class MarketDataWidget extends BaseWidget {
|
|
|
4409
4534
|
this.showLoading();
|
|
4410
4535
|
|
|
4411
4536
|
// Validate initial symbol via API to get company info
|
|
4412
|
-
|
|
4413
|
-
this.
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
const apiService = this.wsManager.getApiService();
|
|
4418
|
-
if (!apiService) {
|
|
4419
|
-
if (this.debug) {
|
|
4420
|
-
console.log('[MarketDataWidget] API service not available for initial validation');
|
|
4421
|
-
}
|
|
4422
|
-
return;
|
|
4537
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting data
|
|
4538
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
4539
|
+
// Store for use in updateWidget
|
|
4540
|
+
if (data && data[0]) {
|
|
4541
|
+
this.initialValidationData = data[0];
|
|
4423
4542
|
}
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
if (this.debug) {
|
|
4430
|
-
console.log('[MarketDataWidget] Initial symbol validated:', {
|
|
4431
|
-
symbol: this.symbol,
|
|
4432
|
-
companyName: symbolData.comp_name,
|
|
4433
|
-
exchangeName: symbolData.market_name
|
|
4434
|
-
});
|
|
4435
|
-
}
|
|
4436
|
-
}
|
|
4437
|
-
} catch (error) {
|
|
4438
|
-
if (this.debug) {
|
|
4439
|
-
console.warn('[MarketDataWidget] Initial symbol validation failed:', error);
|
|
4440
|
-
}
|
|
4441
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
4543
|
+
});
|
|
4544
|
+
|
|
4545
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
4546
|
+
if (validationSuccess === false) {
|
|
4547
|
+
return; // Error is already shown, don't continue
|
|
4442
4548
|
}
|
|
4549
|
+
this.subscribeToData();
|
|
4443
4550
|
}
|
|
4444
4551
|
subscribeToData() {
|
|
4445
4552
|
// Subscribe with symbol for routing
|
|
4446
4553
|
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
|
|
4447
4554
|
);
|
|
4448
|
-
|
|
4449
|
-
// Send subscription message
|
|
4450
|
-
/* this.wsManager.send({
|
|
4451
|
-
type: 'queryl1',
|
|
4452
|
-
symbol: this.symbol
|
|
4453
|
-
}); */
|
|
4454
4555
|
}
|
|
4556
|
+
handleData(message) {
|
|
4557
|
+
//console.log('DEBUG', message)
|
|
4455
4558
|
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
if (this.debug) {
|
|
4461
|
-
console.log('[MarketDataWidget] Received:', event, data);
|
|
4462
|
-
}
|
|
4463
|
-
if (event === 'connection') {
|
|
4464
|
-
this.handleConnectionStatus(data);
|
|
4465
|
-
return;
|
|
4466
|
-
}
|
|
4467
|
-
if (event === 'data') {
|
|
4468
|
-
this.handleData(data);
|
|
4469
|
-
}
|
|
4470
|
-
if (event === 'session_revoked') {
|
|
4471
|
-
if (data.status === 'attempting_relogin') {
|
|
4472
|
-
this.showLoading();
|
|
4473
|
-
} else if (data.status === 'relogin_failed') {
|
|
4474
|
-
this.showError(data.error);
|
|
4475
|
-
} else if (data.status === 'relogin_successful') {
|
|
4476
|
-
this.hideLoading();
|
|
4477
|
-
}
|
|
4478
|
-
return;
|
|
4479
|
-
}
|
|
4480
|
-
} catch (error) {
|
|
4481
|
-
console.error('[MarketDataWidget] Error handling message:', error);
|
|
4482
|
-
this.showError('Error processing data');
|
|
4559
|
+
// Check for error: false and display message if present
|
|
4560
|
+
if (message.error === true) {
|
|
4561
|
+
if (this.debug) {
|
|
4562
|
+
console.log('[MarketDataWidget] Received error response:', message.message);
|
|
4483
4563
|
}
|
|
4484
|
-
|
|
4564
|
+
const errorMsg = message.message || message.Message;
|
|
4485
4565
|
|
|
4486
|
-
|
|
4487
|
-
if (
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
// Re-send subscription when reconnected
|
|
4492
|
-
this.wsManager.send({
|
|
4493
|
-
type: 'queryl1',
|
|
4494
|
-
symbol: this.symbol
|
|
4495
|
-
});
|
|
4496
|
-
} else if (status.status === 'disconnected') {
|
|
4497
|
-
this.showError('Disconnected from data service');
|
|
4498
|
-
} else if (status.status === 'error') {
|
|
4499
|
-
this.showError(status.error || 'Connection error');
|
|
4566
|
+
// If there's a message field, show it as an error
|
|
4567
|
+
if (errorMsg) {
|
|
4568
|
+
this.hideLoading();
|
|
4569
|
+
this.showError(errorMsg);
|
|
4570
|
+
return;
|
|
4500
4571
|
}
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4572
|
+
} else if (message.Data && typeof message.Data === 'string') {
|
|
4573
|
+
this.hideLoading();
|
|
4574
|
+
this.showError(message.Message);
|
|
4575
|
+
return;
|
|
4576
|
+
}
|
|
4504
4577
|
if (this.loadingTimeout) {
|
|
4505
4578
|
clearTimeout(this.loadingTimeout);
|
|
4506
4579
|
this.loadingTimeout = null;
|
|
4507
4580
|
}
|
|
4508
4581
|
|
|
4509
4582
|
// Handle error messages from server (plain text converted to structured format)
|
|
4510
|
-
if (message.type === 'error'
|
|
4583
|
+
if (message.type === 'error') {
|
|
4511
4584
|
if (this.debug) {
|
|
4512
4585
|
console.log('[MarketDataWidget] Received no data message:', message.message);
|
|
4513
4586
|
}
|
|
@@ -4586,126 +4659,6 @@ class MarketDataWidget extends BaseWidget {
|
|
|
4586
4659
|
this.showConnectionQuality(); // Show cache indicator
|
|
4587
4660
|
}
|
|
4588
4661
|
}
|
|
4589
|
-
showNoDataState() {
|
|
4590
|
-
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
4591
|
-
if (this.isDestroyed) return;
|
|
4592
|
-
try {
|
|
4593
|
-
// Hide loading overlay
|
|
4594
|
-
this.hideLoading();
|
|
4595
|
-
|
|
4596
|
-
// Clear any existing errors
|
|
4597
|
-
this.clearError();
|
|
4598
|
-
|
|
4599
|
-
// Update header with dimmed styling and basic info from data
|
|
4600
|
-
const symbol = data.Symbol || this.symbol;
|
|
4601
|
-
|
|
4602
|
-
// Add null checks for all DOM elements
|
|
4603
|
-
const symbolElement = this.container.querySelector('.symbol');
|
|
4604
|
-
if (symbolElement) {
|
|
4605
|
-
symbolElement.textContent = symbol;
|
|
4606
|
-
}
|
|
4607
|
-
const companyNameElement = this.container.querySelector('.company-name');
|
|
4608
|
-
if (companyNameElement) {
|
|
4609
|
-
companyNameElement.textContent = `${symbol} Inc`;
|
|
4610
|
-
}
|
|
4611
|
-
|
|
4612
|
-
// Set price to $0.00 and add dimmed class
|
|
4613
|
-
const currentPriceElement = this.container.querySelector('.current-price');
|
|
4614
|
-
if (currentPriceElement) {
|
|
4615
|
-
currentPriceElement.textContent = '$0.00';
|
|
4616
|
-
}
|
|
4617
|
-
const changeElement = this.container.querySelector('.price-change');
|
|
4618
|
-
if (changeElement) {
|
|
4619
|
-
const changeValueElement = changeElement.querySelector('.change-value');
|
|
4620
|
-
const changePercentElement = changeElement.querySelector('.change-percent');
|
|
4621
|
-
if (changeValueElement) {
|
|
4622
|
-
changeValueElement.textContent = '+0.00';
|
|
4623
|
-
}
|
|
4624
|
-
if (changePercentElement) {
|
|
4625
|
-
changePercentElement.textContent = ' (0.00%)';
|
|
4626
|
-
}
|
|
4627
|
-
changeElement.classList.remove('positive', 'negative');
|
|
4628
|
-
changeElement.classList.add('neutral');
|
|
4629
|
-
}
|
|
4630
|
-
|
|
4631
|
-
// Add dimmed styling to header
|
|
4632
|
-
const widgetHeader = this.container.querySelector('.widget-header');
|
|
4633
|
-
if (widgetHeader) {
|
|
4634
|
-
widgetHeader.classList.add('dimmed');
|
|
4635
|
-
}
|
|
4636
|
-
|
|
4637
|
-
// Replace the data grid with no data message
|
|
4638
|
-
this.showNoDataMessage(symbol, data);
|
|
4639
|
-
|
|
4640
|
-
// Update footer with current timestamp
|
|
4641
|
-
const lastUpdateElement = this.container.querySelector('.last-update');
|
|
4642
|
-
if (lastUpdateElement) {
|
|
4643
|
-
const timestamp = formatTimestampET();
|
|
4644
|
-
lastUpdateElement.textContent = `Checked: ${timestamp}`;
|
|
4645
|
-
}
|
|
4646
|
-
const dataSourceElement = this.container.querySelector('.data-source');
|
|
4647
|
-
if (dataSourceElement) {
|
|
4648
|
-
dataSourceElement.textContent = 'Source: No data available';
|
|
4649
|
-
}
|
|
4650
|
-
} catch (error) {
|
|
4651
|
-
console.error('Error showing no data state:', error);
|
|
4652
|
-
this.showError('Error displaying no data state');
|
|
4653
|
-
}
|
|
4654
|
-
}
|
|
4655
|
-
showNoDataMessage(symbol) {
|
|
4656
|
-
const dataGrid = this.container.querySelector('.data-grid');
|
|
4657
|
-
|
|
4658
|
-
// Hide the data grid if it exists
|
|
4659
|
-
if (dataGrid) {
|
|
4660
|
-
dataGrid.style.display = 'none';
|
|
4661
|
-
}
|
|
4662
|
-
|
|
4663
|
-
// Remove existing no data message if present
|
|
4664
|
-
const existingNoData = this.container.querySelector('.no-data-state');
|
|
4665
|
-
if (existingNoData) {
|
|
4666
|
-
existingNoData.remove();
|
|
4667
|
-
}
|
|
4668
|
-
|
|
4669
|
-
// Create no data message element - safely without XSS risk
|
|
4670
|
-
const noDataElement = createElement('div', '', 'no-data-state');
|
|
4671
|
-
const noDataContent = createElement('div', '', 'no-data-content');
|
|
4672
|
-
|
|
4673
|
-
// Create icon
|
|
4674
|
-
const iconDiv = document.createElement('div');
|
|
4675
|
-
iconDiv.className = 'no-data-icon';
|
|
4676
|
-
iconDiv.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
4677
|
-
<circle cx="12" cy="12" r="10" stroke="#9ca3af" stroke-width="2"/>
|
|
4678
|
-
<path d="M12 8v4" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"/>
|
|
4679
|
-
<circle cx="12" cy="16" r="1" fill="#9ca3af"/>
|
|
4680
|
-
</svg>`;
|
|
4681
|
-
noDataContent.appendChild(iconDiv);
|
|
4682
|
-
|
|
4683
|
-
// Create title
|
|
4684
|
-
const title = createElement('h3', 'No Market Data', 'no-data-title');
|
|
4685
|
-
noDataContent.appendChild(title);
|
|
4686
|
-
|
|
4687
|
-
// Create description with sanitized symbol
|
|
4688
|
-
const description = createElement('p', '', 'no-data-description');
|
|
4689
|
-
description.appendChild(document.createTextNode('Market data for '));
|
|
4690
|
-
const symbolStrong = createElement('strong', sanitizeSymbol(symbol));
|
|
4691
|
-
description.appendChild(symbolStrong);
|
|
4692
|
-
description.appendChild(document.createTextNode(' was not found'));
|
|
4693
|
-
noDataContent.appendChild(description);
|
|
4694
|
-
|
|
4695
|
-
// Create guidance
|
|
4696
|
-
const guidance = createElement('p', 'Please check the symbol spelling or try a different symbol', 'no-data-guidance');
|
|
4697
|
-
noDataContent.appendChild(guidance);
|
|
4698
|
-
noDataElement.appendChild(noDataContent);
|
|
4699
|
-
|
|
4700
|
-
// Insert before footer, with null check
|
|
4701
|
-
const footer = this.container.querySelector('.widget-footer');
|
|
4702
|
-
if (footer && footer.parentNode) {
|
|
4703
|
-
footer.parentNode.insertBefore(noDataElement, footer);
|
|
4704
|
-
} else {
|
|
4705
|
-
// Fallback: append to container if footer not found
|
|
4706
|
-
this.container.appendChild(noDataElement);
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
4662
|
updateWidget(data) {
|
|
4710
4663
|
if (this.isDestroyed) return;
|
|
4711
4664
|
try {
|
|
@@ -5210,7 +5163,20 @@ class NightSessionWidget extends BaseWidget {
|
|
|
5210
5163
|
this.showLoading();
|
|
5211
5164
|
|
|
5212
5165
|
// Validate initial symbol via API to get company info
|
|
5213
|
-
|
|
5166
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
5167
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
5168
|
+
// Extract company info from first data item
|
|
5169
|
+
if (data && data[0]) {
|
|
5170
|
+
this.companyName = data[0].comp_name || '';
|
|
5171
|
+
this.exchangeName = data[0].market_name || '';
|
|
5172
|
+
this.mic = data[0].mic || '';
|
|
5173
|
+
}
|
|
5174
|
+
});
|
|
5175
|
+
|
|
5176
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
5177
|
+
if (validationSuccess === false) {
|
|
5178
|
+
return; // Error is already shown, don't continue
|
|
5179
|
+
}
|
|
5214
5180
|
|
|
5215
5181
|
// Set timeout to detect no data on initial load
|
|
5216
5182
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -5227,38 +5193,6 @@ class NightSessionWidget extends BaseWidget {
|
|
|
5227
5193
|
|
|
5228
5194
|
this.subscribeToData();
|
|
5229
5195
|
}
|
|
5230
|
-
async validateInitialSymbol() {
|
|
5231
|
-
try {
|
|
5232
|
-
const apiService = this.wsManager.getApiService();
|
|
5233
|
-
if (!apiService) {
|
|
5234
|
-
if (this.debug) {
|
|
5235
|
-
console.log('[NightSessionWidget] API service not available for initial validation');
|
|
5236
|
-
}
|
|
5237
|
-
return;
|
|
5238
|
-
}
|
|
5239
|
-
const result = await apiService.quotel1(this.symbol);
|
|
5240
|
-
if (result && result.data && result.data[0]) {
|
|
5241
|
-
const symbolData = result.data[0];
|
|
5242
|
-
// Extract company info
|
|
5243
|
-
this.companyName = symbolData.comp_name || '';
|
|
5244
|
-
this.exchangeName = symbolData.market_name || '';
|
|
5245
|
-
this.mic = symbolData.mic || '';
|
|
5246
|
-
if (this.debug) {
|
|
5247
|
-
console.log('[NightSessionWidget] Initial symbol validated:', {
|
|
5248
|
-
symbol: this.symbol,
|
|
5249
|
-
companyName: this.companyName,
|
|
5250
|
-
exchangeName: this.exchangeName,
|
|
5251
|
-
mic: this.mic
|
|
5252
|
-
});
|
|
5253
|
-
}
|
|
5254
|
-
}
|
|
5255
|
-
} catch (error) {
|
|
5256
|
-
if (this.debug) {
|
|
5257
|
-
console.warn('[NightSessionWidget] Initial symbol validation failed:', error);
|
|
5258
|
-
}
|
|
5259
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
5260
|
-
}
|
|
5261
|
-
}
|
|
5262
5196
|
subscribeToData() {
|
|
5263
5197
|
let subscriptionType;
|
|
5264
5198
|
if (this.source === 'bruce') {
|
|
@@ -5277,71 +5211,53 @@ class NightSessionWidget extends BaseWidget {
|
|
|
5277
5211
|
// to avoid duplicate timeouts
|
|
5278
5212
|
}
|
|
5279
5213
|
handleData(message) {
|
|
5214
|
+
console.log('DEBUG NIGHT', message);
|
|
5215
|
+
|
|
5216
|
+
//message = message.data || message.Data
|
|
5217
|
+
|
|
5280
5218
|
if (this.loadingTimeout) {
|
|
5281
5219
|
clearTimeout(this.loadingTimeout);
|
|
5282
5220
|
this.loadingTimeout = null;
|
|
5283
5221
|
}
|
|
5284
5222
|
|
|
5285
|
-
//
|
|
5286
|
-
if (message.
|
|
5223
|
+
// Check for error: false and display message if present
|
|
5224
|
+
if (message.error === true) {
|
|
5287
5225
|
if (this.debug) {
|
|
5288
|
-
console.log('[
|
|
5226
|
+
console.log('[Nigh] Received error response:', message.message);
|
|
5289
5227
|
}
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
NotFound: true,
|
|
5295
|
-
message: message.message
|
|
5296
|
-
});
|
|
5297
|
-
} else {
|
|
5228
|
+
const errorMsg = message.message || message.Message;
|
|
5229
|
+
|
|
5230
|
+
// If there's a message field, show it as an error
|
|
5231
|
+
if (errorMsg) {
|
|
5298
5232
|
this.hideLoading();
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
}
|
|
5233
|
+
this.showError(errorMsg);
|
|
5234
|
+
return;
|
|
5302
5235
|
}
|
|
5236
|
+
} else if (message.Data && typeof message.Data === 'string') {
|
|
5237
|
+
this.hideLoading();
|
|
5238
|
+
this.showError(message.Message);
|
|
5303
5239
|
return;
|
|
5304
5240
|
}
|
|
5305
5241
|
|
|
5306
|
-
// Handle
|
|
5242
|
+
// Handle error messages from server (plain text converted to structured format)
|
|
5307
5243
|
if (message.type === 'error') {
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
this.showNoDataState({
|
|
5315
|
-
Symbol: this.symbol,
|
|
5316
|
-
NotFound: true,
|
|
5317
|
-
isAccessError: true,
|
|
5318
|
-
message: errorMsg
|
|
5319
|
-
});
|
|
5320
|
-
} else {
|
|
5321
|
-
this.hideLoading();
|
|
5322
|
-
if (this.debug) {
|
|
5323
|
-
console.log('[NightSessionWidget] Access error but keeping cached data');
|
|
5324
|
-
}
|
|
5325
|
-
}
|
|
5244
|
+
if (this.debug) {
|
|
5245
|
+
console.log('[NightSessionWidget] Received no data message:', message.message);
|
|
5246
|
+
}
|
|
5247
|
+
// Only show no data state if we don't have cached data
|
|
5248
|
+
if (!this.data) {
|
|
5249
|
+
this.showError(errorMsg);
|
|
5326
5250
|
} else {
|
|
5327
|
-
|
|
5328
|
-
if (
|
|
5329
|
-
console.log('
|
|
5330
|
-
this.showError(errorMsg);
|
|
5331
|
-
} else {
|
|
5332
|
-
this.hideLoading();
|
|
5333
|
-
if (this.debug) {
|
|
5334
|
-
console.log('[NightSessionWidget] Error received but keeping cached data:', errorMsg);
|
|
5335
|
-
}
|
|
5251
|
+
this.hideLoading();
|
|
5252
|
+
if (this.debug) {
|
|
5253
|
+
console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
|
|
5336
5254
|
}
|
|
5337
5255
|
}
|
|
5338
5256
|
return;
|
|
5339
5257
|
}
|
|
5340
|
-
|
|
5341
|
-
// Filter for night session data
|
|
5342
|
-
if (Array.isArray(message)) {
|
|
5258
|
+
if (Array.isArray(message.data)) {
|
|
5343
5259
|
// First, try to find data matching our symbol regardless of MarketName
|
|
5344
|
-
const symbolData = message.find(item => item.Symbol === this.symbol);
|
|
5260
|
+
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
5345
5261
|
if (symbolData) {
|
|
5346
5262
|
if (this.debug) {
|
|
5347
5263
|
console.log('[NightSessionWidget] Found data for symbol:', symbolData);
|
|
@@ -5381,34 +5297,35 @@ class NightSessionWidget extends BaseWidget {
|
|
|
5381
5297
|
}
|
|
5382
5298
|
}
|
|
5383
5299
|
// Handle wrapped format
|
|
5384
|
-
else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1' || message.type === 'queryonbbol1') {
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5300
|
+
/* else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1' || message.type === 'queryonbbol1') {
|
|
5301
|
+
if (message.data['0']?.Symbol === this.symbol) {
|
|
5302
|
+
// For onbbo source, skip MarketName check
|
|
5303
|
+
const isOnbbo = message.type === 'queryonbbol1';
|
|
5304
|
+
const shouldShowNoData = message['0'].NotFound === true ||
|
|
5305
|
+
(!isOnbbo && (!message['0'].MarketName || message['0'].MarketName !== 'BLUE'));
|
|
5306
|
+
if (shouldShowNoData) {
|
|
5307
|
+
// Only show no data state if we don't have cached data
|
|
5308
|
+
if (!this.data) {
|
|
5309
|
+
this.showNoDataState(message['0']);
|
|
5310
|
+
} else {
|
|
5311
|
+
this.hideLoading();
|
|
5312
|
+
if (this.debug) {
|
|
5313
|
+
console.log('[NightSessionWidget] No new data, keeping cached data visible');
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
} else {
|
|
5317
|
+
const model = new NightSessionModel(message['0']);
|
|
5318
|
+
this.data = model; // Store for caching
|
|
5319
|
+
this.updateWidget(model);
|
|
5397
5320
|
}
|
|
5398
|
-
}
|
|
5399
5321
|
} else {
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
// No matching symbol - keep cached data if available
|
|
5406
|
-
if (this.debug) {
|
|
5407
|
-
console.log('[NightSessionWidget] No matching symbol in response, keeping cached data');
|
|
5322
|
+
// No matching symbol - keep cached data if available
|
|
5323
|
+
if (this.debug) {
|
|
5324
|
+
console.log('[NightSessionWidget] No matching symbol in response, keeping cached data');
|
|
5325
|
+
}
|
|
5326
|
+
this.hideLoading();
|
|
5408
5327
|
}
|
|
5409
|
-
|
|
5410
|
-
}
|
|
5411
|
-
}
|
|
5328
|
+
} */
|
|
5412
5329
|
if (message._cached) ;
|
|
5413
5330
|
}
|
|
5414
5331
|
updateWidget(data) {
|
|
@@ -6238,36 +6155,20 @@ class OptionsWidget extends BaseWidget {
|
|
|
6238
6155
|
this.showLoading();
|
|
6239
6156
|
|
|
6240
6157
|
// Validate initial symbol via API to get symbol info
|
|
6241
|
-
|
|
6242
|
-
this.
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
if (!apiService) {
|
|
6248
|
-
if (this.debug) {
|
|
6249
|
-
console.log('[OptionsWidget] API service not available for initial validation');
|
|
6250
|
-
}
|
|
6251
|
-
return;
|
|
6158
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting data
|
|
6159
|
+
const validationSuccess = await super.validateInitialSymbol('quoteOptionl1', this.symbol, data => {
|
|
6160
|
+
// Store for use in updateWidget
|
|
6161
|
+
// Note: quoteOptionl1 returns array directly, not wrapped in data field
|
|
6162
|
+
if (data && data[0] && !data[0].error && !data[0].not_found) {
|
|
6163
|
+
this.initialValidationData = data[0];
|
|
6252
6164
|
}
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
if (this.debug) {
|
|
6259
|
-
console.log('[OptionsWidget] Initial symbol validated:', {
|
|
6260
|
-
symbol: this.symbol,
|
|
6261
|
-
underlying: symbolData.Underlying || symbolData.RootSymbol
|
|
6262
|
-
});
|
|
6263
|
-
}
|
|
6264
|
-
}
|
|
6265
|
-
} catch (error) {
|
|
6266
|
-
if (this.debug) {
|
|
6267
|
-
console.warn('[OptionsWidget] Initial symbol validation failed:', error);
|
|
6268
|
-
}
|
|
6269
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
6165
|
+
});
|
|
6166
|
+
|
|
6167
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
6168
|
+
if (validationSuccess === false) {
|
|
6169
|
+
return; // Error is already shown, don't continue
|
|
6270
6170
|
}
|
|
6171
|
+
this.subscribeToData();
|
|
6271
6172
|
}
|
|
6272
6173
|
subscribeToData() {
|
|
6273
6174
|
// Subscribe with symbol for routing
|
|
@@ -38624,7 +38525,20 @@ class IntradayChartWidget extends BaseWidget {
|
|
|
38624
38525
|
}
|
|
38625
38526
|
async initialize() {
|
|
38626
38527
|
// Validate initial symbol via API to get company info
|
|
38627
|
-
|
|
38528
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
38529
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
38530
|
+
// Extract company info from first data item
|
|
38531
|
+
if (data && data[0]) {
|
|
38532
|
+
this.companyName = data[0].comp_name || '';
|
|
38533
|
+
this.exchangeName = data[0].market_name || '';
|
|
38534
|
+
this.mic = data[0].mic || '';
|
|
38535
|
+
}
|
|
38536
|
+
});
|
|
38537
|
+
|
|
38538
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
38539
|
+
if (validationSuccess === false) {
|
|
38540
|
+
return; // Error is already shown, don't continue
|
|
38541
|
+
}
|
|
38628
38542
|
|
|
38629
38543
|
// Update company name in the header
|
|
38630
38544
|
this.updateCompanyName();
|
|
@@ -38632,38 +38546,6 @@ class IntradayChartWidget extends BaseWidget {
|
|
|
38632
38546
|
// Load chart data
|
|
38633
38547
|
await this.loadChartData();
|
|
38634
38548
|
}
|
|
38635
|
-
async validateInitialSymbol() {
|
|
38636
|
-
try {
|
|
38637
|
-
const apiService = this.wsManager.getApiService();
|
|
38638
|
-
if (!apiService) {
|
|
38639
|
-
if (this.debug) {
|
|
38640
|
-
console.log('[IntradayChartWidget] API service not available for initial validation');
|
|
38641
|
-
}
|
|
38642
|
-
return;
|
|
38643
|
-
}
|
|
38644
|
-
const result = await apiService.quotel1(this.symbol);
|
|
38645
|
-
if (result && result.data && result.data[0]) {
|
|
38646
|
-
const symbolData = result.data[0];
|
|
38647
|
-
// Extract company info
|
|
38648
|
-
this.companyName = symbolData.comp_name || '';
|
|
38649
|
-
this.exchangeName = symbolData.market_name || '';
|
|
38650
|
-
this.mic = symbolData.mic || '';
|
|
38651
|
-
if (this.debug) {
|
|
38652
|
-
console.log('[IntradayChartWidget] Initial symbol validated:', {
|
|
38653
|
-
symbol: this.symbol,
|
|
38654
|
-
companyName: this.companyName,
|
|
38655
|
-
exchangeName: this.exchangeName,
|
|
38656
|
-
mic: this.mic
|
|
38657
|
-
});
|
|
38658
|
-
}
|
|
38659
|
-
}
|
|
38660
|
-
} catch (error) {
|
|
38661
|
-
if (this.debug) {
|
|
38662
|
-
console.warn('[IntradayChartWidget] Initial symbol validation failed:', error);
|
|
38663
|
-
}
|
|
38664
|
-
// Don't throw - let the widget continue with chart data
|
|
38665
|
-
}
|
|
38666
|
-
}
|
|
38667
38549
|
updateCompanyName() {
|
|
38668
38550
|
const companyNameElement = this.container.querySelector('.intraday-company-name');
|
|
38669
38551
|
if (companyNameElement) {
|
|
@@ -39085,21 +38967,14 @@ class IntradayChartWidget extends BaseWidget {
|
|
|
39085
38967
|
let priceData = null;
|
|
39086
38968
|
|
|
39087
38969
|
// Handle array format (standard night session format)
|
|
39088
|
-
if (Array.isArray(message)) {
|
|
38970
|
+
if (Array.isArray(message.data)) {
|
|
39089
38971
|
console.log('[IntradayChartWidget] Processing array format, length:', message.length);
|
|
39090
|
-
const symbolData = message.find(item => item.Symbol === this.symbol);
|
|
38972
|
+
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
39091
38973
|
console.log('[IntradayChartWidget] Found symbol data:', symbolData);
|
|
39092
38974
|
if (symbolData && !symbolData.NotFound) {
|
|
39093
38975
|
priceData = symbolData;
|
|
39094
38976
|
}
|
|
39095
38977
|
}
|
|
39096
|
-
// Handle wrapped format
|
|
39097
|
-
else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1') {
|
|
39098
|
-
console.log('[IntradayChartWidget] Processing wrapped format');
|
|
39099
|
-
if (message['0']?.Symbol === this.symbol && !message['0'].NotFound) {
|
|
39100
|
-
priceData = message['0'];
|
|
39101
|
-
}
|
|
39102
|
-
}
|
|
39103
38978
|
// Handle direct data format
|
|
39104
38979
|
else if (message.Symbol === this.symbol && !message.NotFound) {
|
|
39105
38980
|
console.log('[IntradayChartWidget] Processing direct format');
|
|
@@ -40056,8 +39931,10 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40056
39931
|
this.styled = options.styled !== undefined ? options.styled : true;
|
|
40057
39932
|
this.maxLevels = options.maxLevels || 10; // Number of levels to display
|
|
40058
39933
|
this.data = null;
|
|
39934
|
+
this.level1Data = null; // Store Level 1 data separately
|
|
40059
39935
|
this.isDestroyed = false;
|
|
40060
|
-
this.
|
|
39936
|
+
this.unsubscribeL2 = null;
|
|
39937
|
+
this.unsubscribeL1 = null;
|
|
40061
39938
|
this.symbolEditor = null;
|
|
40062
39939
|
this.loadingTimeout = null;
|
|
40063
39940
|
|
|
@@ -40158,13 +40035,22 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40158
40035
|
this.showError(`No data received for ${upperSymbol}. Please try again.`);
|
|
40159
40036
|
}, 10000);
|
|
40160
40037
|
|
|
40161
|
-
// Unsubscribe from old symbol
|
|
40162
|
-
if (this.
|
|
40038
|
+
// Unsubscribe from old symbol's L2 and L1 data
|
|
40039
|
+
if (this.unsubscribeL2) {
|
|
40163
40040
|
if (this.debug) {
|
|
40164
|
-
console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol}`);
|
|
40041
|
+
console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol} L2`);
|
|
40165
40042
|
}
|
|
40166
|
-
this.
|
|
40167
|
-
this.
|
|
40043
|
+
this.wsManager.sendUnsubscribe('queryonbbol2', this.symbol);
|
|
40044
|
+
this.unsubscribeL2();
|
|
40045
|
+
this.unsubscribeL2 = null;
|
|
40046
|
+
}
|
|
40047
|
+
if (this.unsubscribeL1) {
|
|
40048
|
+
if (this.debug) {
|
|
40049
|
+
console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol} L1`);
|
|
40050
|
+
}
|
|
40051
|
+
this.wsManager.sendUnsubscribe('queryonbbol1', this.symbol);
|
|
40052
|
+
this.unsubscribeL1();
|
|
40053
|
+
this.unsubscribeL1 = null;
|
|
40168
40054
|
}
|
|
40169
40055
|
|
|
40170
40056
|
// Update internal symbol
|
|
@@ -40191,7 +40077,19 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40191
40077
|
this.showLoading();
|
|
40192
40078
|
|
|
40193
40079
|
// Fetch company info for initial symbol
|
|
40194
|
-
|
|
40080
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
40081
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
40082
|
+
// Extract company info from first data item
|
|
40083
|
+
if (data && data[0]) {
|
|
40084
|
+
this.companyName = data[0].comp_name || '';
|
|
40085
|
+
this.exchangeName = data[0].market_name || '';
|
|
40086
|
+
}
|
|
40087
|
+
});
|
|
40088
|
+
|
|
40089
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
40090
|
+
if (validationSuccess === false) {
|
|
40091
|
+
return; // Error is already shown, don't continue
|
|
40092
|
+
}
|
|
40195
40093
|
|
|
40196
40094
|
// Set timeout to detect no data on initial load
|
|
40197
40095
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -40203,41 +40101,52 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40203
40101
|
}, 10000);
|
|
40204
40102
|
this.subscribeToData();
|
|
40205
40103
|
}
|
|
40206
|
-
|
|
40207
|
-
|
|
40208
|
-
|
|
40209
|
-
|
|
40210
|
-
|
|
40211
|
-
|
|
40212
|
-
|
|
40104
|
+
subscribeToData() {
|
|
40105
|
+
// Subscribe to ONBBO Level 2 data (order book)
|
|
40106
|
+
// Use separate widget IDs to avoid "already active" duplicate detection
|
|
40107
|
+
this.unsubscribeL2 = this.wsManager.subscribe(`${this.widgetId}-l2`, ['queryonbbol2'], messageWrapper => {
|
|
40108
|
+
const {
|
|
40109
|
+
event,
|
|
40110
|
+
data
|
|
40111
|
+
} = messageWrapper;
|
|
40112
|
+
|
|
40113
|
+
// Handle connection events
|
|
40114
|
+
if (event === 'connection') {
|
|
40115
|
+
this.handleConnectionStatus(data);
|
|
40213
40116
|
return;
|
|
40214
40117
|
}
|
|
40215
|
-
|
|
40216
|
-
|
|
40217
|
-
|
|
40218
|
-
|
|
40219
|
-
this.
|
|
40220
|
-
|
|
40221
|
-
|
|
40222
|
-
|
|
40223
|
-
symbol: this.symbol,
|
|
40224
|
-
companyName: this.companyName,
|
|
40225
|
-
exchangeName: this.exchangeName
|
|
40226
|
-
});
|
|
40227
|
-
}
|
|
40118
|
+
|
|
40119
|
+
// For data events, add type context
|
|
40120
|
+
if (event === 'data') {
|
|
40121
|
+
data._dataType = 'level2';
|
|
40122
|
+
this.handleMessage({
|
|
40123
|
+
event,
|
|
40124
|
+
data
|
|
40125
|
+
});
|
|
40228
40126
|
}
|
|
40229
|
-
}
|
|
40230
|
-
|
|
40231
|
-
|
|
40127
|
+
}, this.symbol);
|
|
40128
|
+
|
|
40129
|
+
// Subscribe to ONBBO Level 1 data (statistics: LastPx, LowPx, HighPx, Volume)
|
|
40130
|
+
this.unsubscribeL1 = this.wsManager.subscribe(`${this.widgetId}-l1`, ['queryonbbol1'], messageWrapper => {
|
|
40131
|
+
const {
|
|
40132
|
+
event,
|
|
40133
|
+
data
|
|
40134
|
+
} = messageWrapper;
|
|
40135
|
+
|
|
40136
|
+
// Connection already handled by L2 subscription
|
|
40137
|
+
if (event === 'connection') {
|
|
40138
|
+
return;
|
|
40232
40139
|
}
|
|
40233
|
-
|
|
40234
|
-
|
|
40235
|
-
|
|
40236
|
-
|
|
40237
|
-
|
|
40238
|
-
|
|
40239
|
-
|
|
40240
|
-
|
|
40140
|
+
|
|
40141
|
+
// For data events, add type context
|
|
40142
|
+
if (event === 'data') {
|
|
40143
|
+
data._dataType = 'level1';
|
|
40144
|
+
this.handleMessage({
|
|
40145
|
+
event,
|
|
40146
|
+
data
|
|
40147
|
+
});
|
|
40148
|
+
}
|
|
40149
|
+
}, this.symbol);
|
|
40241
40150
|
}
|
|
40242
40151
|
handleData(message) {
|
|
40243
40152
|
if (this.loadingTimeout) {
|
|
@@ -40245,8 +40154,14 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40245
40154
|
this.loadingTimeout = null;
|
|
40246
40155
|
}
|
|
40247
40156
|
|
|
40157
|
+
// Extract data type from metadata (added by subscription callback)
|
|
40158
|
+
const dataType = message._dataType;
|
|
40159
|
+
if (this.debug) {
|
|
40160
|
+
console.log(`[ONBBOLevel2Widget] handleData called with type: ${dataType}`, message);
|
|
40161
|
+
}
|
|
40162
|
+
|
|
40248
40163
|
// Handle error messages
|
|
40249
|
-
if (message.type === 'error') {
|
|
40164
|
+
if (message.type === 'error' || message.error == true) {
|
|
40250
40165
|
const errorMsg = message.message || 'Server error';
|
|
40251
40166
|
if (this.debug) {
|
|
40252
40167
|
console.log('[ONBBOLevel2Widget] Error:', errorMsg);
|
|
@@ -40255,16 +40170,54 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40255
40170
|
return;
|
|
40256
40171
|
}
|
|
40257
40172
|
|
|
40173
|
+
// Handle Level 1 data (statistics: LastPx, LowPx, HighPx, Volume)
|
|
40174
|
+
if (message.type === 'queryonbbol1') {
|
|
40175
|
+
if (this.debug) {
|
|
40176
|
+
console.log('[ONBBOLevel2Widget] Processing Level 1 data:', message);
|
|
40177
|
+
}
|
|
40178
|
+
|
|
40179
|
+
// Handle array format (new format with Data wrapper)
|
|
40180
|
+
if (message.data && Array.isArray(message.data)) {
|
|
40181
|
+
const l1Data = message.data.find(d => d.Symbol === this.symbol);
|
|
40182
|
+
if (l1Data) {
|
|
40183
|
+
this.level1Data = {
|
|
40184
|
+
lastPx: l1Data.LastPx || 0,
|
|
40185
|
+
lowPx: l1Data.LowPx || 0,
|
|
40186
|
+
highPx: l1Data.HighPx || 0,
|
|
40187
|
+
volume: l1Data.Volume || 0
|
|
40188
|
+
};
|
|
40189
|
+
this.updateLevel1Display();
|
|
40190
|
+
}
|
|
40191
|
+
}
|
|
40192
|
+
// Handle direct array format (standard format)
|
|
40193
|
+
else if (Array.isArray(message)) {
|
|
40194
|
+
const l1Data = message.find(d => d.Symbol === this.symbol);
|
|
40195
|
+
if (l1Data) {
|
|
40196
|
+
this.level1Data = {
|
|
40197
|
+
lastPx: l1Data.LastPx || 0,
|
|
40198
|
+
lowPx: l1Data.LowPx || 0,
|
|
40199
|
+
highPx: l1Data.HighPx || 0,
|
|
40200
|
+
volume: l1Data.Volume || 0
|
|
40201
|
+
};
|
|
40202
|
+
this.updateLevel1Display();
|
|
40203
|
+
}
|
|
40204
|
+
}
|
|
40205
|
+
|
|
40206
|
+
// Clean up metadata
|
|
40207
|
+
delete message._dataType;
|
|
40208
|
+
return;
|
|
40209
|
+
}
|
|
40210
|
+
|
|
40258
40211
|
// Handle Level 2 data - Array of MMID quotes
|
|
40259
|
-
if (Array.isArray(message)) {
|
|
40212
|
+
if (Array.isArray(message.data)) {
|
|
40260
40213
|
// Check if it's an array of MMID objects (ONBBO format)
|
|
40261
|
-
if (message.length > 0 && message[0].MMID) {
|
|
40214
|
+
if (message.data.length > 0 && message.data[0].MMID) {
|
|
40262
40215
|
if (this.debug) {
|
|
40263
40216
|
console.log('[ONBBOLevel2Widget] Received MMID array:', message);
|
|
40264
40217
|
}
|
|
40265
40218
|
|
|
40266
40219
|
// Create model from MMID array
|
|
40267
|
-
const model = new ONBBOLevel2Model(message);
|
|
40220
|
+
const model = new ONBBOLevel2Model(message.data);
|
|
40268
40221
|
model.symbol = this.symbol; // Set symbol from widget
|
|
40269
40222
|
this.data = model;
|
|
40270
40223
|
this.updateWidget(model);
|
|
@@ -40272,7 +40225,7 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40272
40225
|
}
|
|
40273
40226
|
|
|
40274
40227
|
// Try to find symbol in array (alternative format)
|
|
40275
|
-
const symbolData = message.find(item => item.Symbol === this.symbol);
|
|
40228
|
+
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
40276
40229
|
if (symbolData) {
|
|
40277
40230
|
if (symbolData.NotFound === true) {
|
|
40278
40231
|
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
@@ -40286,32 +40239,31 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40286
40239
|
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40287
40240
|
}
|
|
40288
40241
|
// Handle wrapped format
|
|
40289
|
-
else if (message.type === 'queryonbbol2') {
|
|
40290
|
-
|
|
40291
|
-
|
|
40292
|
-
|
|
40293
|
-
|
|
40242
|
+
/* else if (message.type === 'queryonbbol2') {
|
|
40243
|
+
// Check if wrapped data contains MMID array
|
|
40244
|
+
if (message['0'] && Array.isArray(message['0'])) {
|
|
40245
|
+
if (this.debug) {
|
|
40246
|
+
console.log('[ONBBOLevel2Widget] Received wrapped MMID array:', message['0']);
|
|
40247
|
+
}
|
|
40248
|
+
const model = new ONBBOLevel2Model(message['0']);
|
|
40249
|
+
model.symbol = this.symbol;
|
|
40250
|
+
this.data = model;
|
|
40251
|
+
this.updateWidget(model);
|
|
40252
|
+
return;
|
|
40294
40253
|
}
|
|
40295
|
-
|
|
40296
|
-
|
|
40297
|
-
|
|
40298
|
-
|
|
40299
|
-
|
|
40300
|
-
|
|
40301
|
-
|
|
40302
|
-
|
|
40303
|
-
|
|
40304
|
-
if (message['0'].NotFound === true) {
|
|
40305
|
-
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40254
|
+
// Check for symbol match in wrapped format
|
|
40255
|
+
if (message['0']?.Symbol === this.symbol) {
|
|
40256
|
+
if (message['0'].NotFound === true) {
|
|
40257
|
+
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40258
|
+
} else {
|
|
40259
|
+
const model = new ONBBOLevel2Model(message['0']);
|
|
40260
|
+
this.data = model;
|
|
40261
|
+
this.updateWidget(model);
|
|
40262
|
+
}
|
|
40306
40263
|
} else {
|
|
40307
|
-
|
|
40308
|
-
this.data = model;
|
|
40309
|
-
this.updateWidget(model);
|
|
40264
|
+
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40310
40265
|
}
|
|
40311
|
-
|
|
40312
|
-
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40313
|
-
}
|
|
40314
|
-
}
|
|
40266
|
+
} */
|
|
40315
40267
|
}
|
|
40316
40268
|
updateWidget(data) {
|
|
40317
40269
|
if (this.isDestroyed) return;
|
|
@@ -40409,6 +40361,43 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40409
40361
|
askBody.appendChild(row);
|
|
40410
40362
|
});
|
|
40411
40363
|
}
|
|
40364
|
+
updateLevel1Display() {
|
|
40365
|
+
if (!this.level1Data) return;
|
|
40366
|
+
const formatPrice = price => {
|
|
40367
|
+
return price ? price.toFixed(2) : '--';
|
|
40368
|
+
};
|
|
40369
|
+
const formatVolume = volume => {
|
|
40370
|
+
if (!volume) return '--';
|
|
40371
|
+
return volume.toLocaleString();
|
|
40372
|
+
};
|
|
40373
|
+
|
|
40374
|
+
// Update Last Price
|
|
40375
|
+
const lastPxElement = this.container.querySelector('.l1-last-px');
|
|
40376
|
+
if (lastPxElement) {
|
|
40377
|
+
lastPxElement.textContent = formatPrice(this.level1Data.lastPx);
|
|
40378
|
+
}
|
|
40379
|
+
|
|
40380
|
+
// Update Low Price
|
|
40381
|
+
const lowPxElement = this.container.querySelector('.l1-low-px');
|
|
40382
|
+
if (lowPxElement) {
|
|
40383
|
+
lowPxElement.textContent = formatPrice(this.level1Data.lowPx);
|
|
40384
|
+
}
|
|
40385
|
+
|
|
40386
|
+
// Update High Price
|
|
40387
|
+
const highPxElement = this.container.querySelector('.l1-high-px');
|
|
40388
|
+
if (highPxElement) {
|
|
40389
|
+
highPxElement.textContent = formatPrice(this.level1Data.highPx);
|
|
40390
|
+
}
|
|
40391
|
+
|
|
40392
|
+
// Update Volume
|
|
40393
|
+
const volumeElement = this.container.querySelector('.l1-volume');
|
|
40394
|
+
if (volumeElement) {
|
|
40395
|
+
volumeElement.textContent = formatVolume(this.level1Data.volume);
|
|
40396
|
+
}
|
|
40397
|
+
if (this.debug) {
|
|
40398
|
+
console.log('[ONBBOLevel2Widget] Updated Level 1 display:', this.level1Data);
|
|
40399
|
+
}
|
|
40400
|
+
}
|
|
40412
40401
|
showLoading() {
|
|
40413
40402
|
const loadingOverlay = this.container.querySelector('.widget-loading-overlay');
|
|
40414
40403
|
if (loadingOverlay) {
|
|
@@ -40455,9 +40444,17 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40455
40444
|
this.clearTimeout(this.loadingTimeout);
|
|
40456
40445
|
this.loadingTimeout = null;
|
|
40457
40446
|
}
|
|
40458
|
-
|
|
40459
|
-
|
|
40460
|
-
|
|
40447
|
+
|
|
40448
|
+
// Unsubscribe from L2 data
|
|
40449
|
+
if (this.unsubscribeL2) {
|
|
40450
|
+
this.unsubscribeL2();
|
|
40451
|
+
this.unsubscribeL2 = null;
|
|
40452
|
+
}
|
|
40453
|
+
|
|
40454
|
+
// Unsubscribe from L1 data
|
|
40455
|
+
if (this.unsubscribeL1) {
|
|
40456
|
+
this.unsubscribeL1();
|
|
40457
|
+
this.unsubscribeL1 = null;
|
|
40461
40458
|
}
|
|
40462
40459
|
|
|
40463
40460
|
// Destroy the symbol editor
|
|
@@ -40622,6 +40619,7 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40622
40619
|
this.styled = options.styled !== undefined ? options.styled : true;
|
|
40623
40620
|
this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
|
|
40624
40621
|
this.data = new TimeSalesModel([], this.maxTrades);
|
|
40622
|
+
this.data.symbol = this.symbol; // Set the symbol in the data model
|
|
40625
40623
|
this.isDestroyed = false;
|
|
40626
40624
|
this.unsubscribe = null;
|
|
40627
40625
|
this.symbolEditor = null;
|
|
@@ -40633,6 +40631,7 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40633
40631
|
|
|
40634
40632
|
// Create widget structure
|
|
40635
40633
|
this.createWidgetStructure();
|
|
40634
|
+
this.initializeSymbolEditor();
|
|
40636
40635
|
|
|
40637
40636
|
// Initialize the widget
|
|
40638
40637
|
this.initialize();
|
|
@@ -40649,7 +40648,26 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40649
40648
|
}
|
|
40650
40649
|
}
|
|
40651
40650
|
this.addStyles();
|
|
40652
|
-
this.setupSymbolEditor();
|
|
40651
|
+
//this.setupSymbolEditor();
|
|
40652
|
+
}
|
|
40653
|
+
initializeSymbolEditor() {
|
|
40654
|
+
// Initialize symbol editor with stock symbol validation
|
|
40655
|
+
this.symbolEditor = new SymbolEditor(this, {
|
|
40656
|
+
maxLength: 10,
|
|
40657
|
+
placeholder: 'Enter symbol...',
|
|
40658
|
+
//validator: this.validateStockSymbol.bind(this),
|
|
40659
|
+
onSymbolChange: this.handleSymbolChange.bind(this),
|
|
40660
|
+
debug: this.debug,
|
|
40661
|
+
autoUppercase: true,
|
|
40662
|
+
symbolType: 'quotel1'
|
|
40663
|
+
});
|
|
40664
|
+
|
|
40665
|
+
// Set initial symbol
|
|
40666
|
+
const symbolElement = this.container.querySelector('.symbol');
|
|
40667
|
+
if (symbolElement) {
|
|
40668
|
+
symbolElement.textContent = this.symbol;
|
|
40669
|
+
symbolElement.dataset.originalSymbol = this.symbol;
|
|
40670
|
+
}
|
|
40653
40671
|
}
|
|
40654
40672
|
addStyles() {
|
|
40655
40673
|
// Inject styles from the styles file into document head
|
|
@@ -40672,6 +40690,8 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40672
40690
|
// Keep editor open when clicking buttons
|
|
40673
40691
|
symbolType: 'quotel1' // Use standard quote validation
|
|
40674
40692
|
});
|
|
40693
|
+
|
|
40694
|
+
//console.log('SETUP COMPLETE')
|
|
40675
40695
|
}
|
|
40676
40696
|
async handleSymbolChange(newSymbol, oldSymbol) {
|
|
40677
40697
|
let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
@@ -40774,7 +40794,19 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40774
40794
|
this.showLoading();
|
|
40775
40795
|
|
|
40776
40796
|
// Validate initial symbol via API to get company info
|
|
40777
|
-
|
|
40797
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
40798
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
40799
|
+
// Extract company info from first data item
|
|
40800
|
+
if (data && data[0]) {
|
|
40801
|
+
this.companyName = data[0].comp_name || '';
|
|
40802
|
+
this.exchangeName = data[0].market_name || '';
|
|
40803
|
+
}
|
|
40804
|
+
});
|
|
40805
|
+
|
|
40806
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
40807
|
+
if (validationSuccess === false) {
|
|
40808
|
+
return; // Error is already shown, don't continue
|
|
40809
|
+
}
|
|
40778
40810
|
|
|
40779
40811
|
// Set timeout to detect no data on initial load
|
|
40780
40812
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -40787,36 +40819,6 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40787
40819
|
|
|
40788
40820
|
this.subscribeToData();
|
|
40789
40821
|
}
|
|
40790
|
-
async validateInitialSymbol() {
|
|
40791
|
-
try {
|
|
40792
|
-
const apiService = this.wsManager.getApiService();
|
|
40793
|
-
if (!apiService) {
|
|
40794
|
-
if (this.debug) {
|
|
40795
|
-
console.log('[TimeSalesWidget] API service not available for initial validation');
|
|
40796
|
-
}
|
|
40797
|
-
return;
|
|
40798
|
-
}
|
|
40799
|
-
const result = await apiService.quotel1(this.symbol);
|
|
40800
|
-
if (result && result.data && result.data[0]) {
|
|
40801
|
-
const symbolData = result.data[0];
|
|
40802
|
-
// Extract company info
|
|
40803
|
-
this.companyName = symbolData.comp_name || '';
|
|
40804
|
-
this.exchangeName = symbolData.market_name || '';
|
|
40805
|
-
if (this.debug) {
|
|
40806
|
-
console.log('[TimeSalesWidget] Initial symbol validated:', {
|
|
40807
|
-
symbol: this.symbol,
|
|
40808
|
-
companyName: this.companyName,
|
|
40809
|
-
exchangeName: this.exchangeName
|
|
40810
|
-
});
|
|
40811
|
-
}
|
|
40812
|
-
}
|
|
40813
|
-
} catch (error) {
|
|
40814
|
-
if (this.debug) {
|
|
40815
|
-
console.warn('[TimeSalesWidget] Initial symbol validation failed:', error);
|
|
40816
|
-
}
|
|
40817
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
40818
|
-
}
|
|
40819
|
-
}
|
|
40820
40822
|
subscribeToData() {
|
|
40821
40823
|
// Subscribe to Time & Sales data (using queryonbbotimesale)
|
|
40822
40824
|
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
|
|
@@ -40832,6 +40834,9 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40832
40834
|
clearTimeout(this.loadingTimeout);
|
|
40833
40835
|
this.loadingTimeout = null;
|
|
40834
40836
|
}
|
|
40837
|
+
if (message.type != 'queryonbbotimesale') {
|
|
40838
|
+
return;
|
|
40839
|
+
}
|
|
40835
40840
|
|
|
40836
40841
|
// Handle error messages
|
|
40837
40842
|
if (message.type === 'error') {
|
|
@@ -40844,30 +40849,16 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40844
40849
|
}
|
|
40845
40850
|
|
|
40846
40851
|
// Handle Time & Sales data - Array of trades
|
|
40847
|
-
if (Array.isArray(message)) {
|
|
40852
|
+
if (Array.isArray(message.data)) {
|
|
40848
40853
|
if (this.debug) {
|
|
40849
40854
|
console.log('[TimeSalesWidget] Received trades array:', message);
|
|
40850
40855
|
}
|
|
40851
40856
|
|
|
40852
40857
|
// Add new trades to existing data
|
|
40853
|
-
this.data.addTrades(message);
|
|
40858
|
+
this.data.addTrades(message.data);
|
|
40854
40859
|
this.updateWidget();
|
|
40855
40860
|
return;
|
|
40856
40861
|
}
|
|
40857
|
-
|
|
40858
|
-
// Handle wrapped format
|
|
40859
|
-
if ((message.type === 'queryonbbotimesale' || message.type === 'querytns') && message['0']) {
|
|
40860
|
-
if (Array.isArray(message['0'])) {
|
|
40861
|
-
if (this.debug) {
|
|
40862
|
-
console.log('[TimeSalesWidget] Received wrapped trades array:', message['0']);
|
|
40863
|
-
}
|
|
40864
|
-
|
|
40865
|
-
// Add new trades to existing data
|
|
40866
|
-
this.data.addTrades(message['0']);
|
|
40867
|
-
this.updateWidget();
|
|
40868
|
-
return;
|
|
40869
|
-
}
|
|
40870
|
-
}
|
|
40871
40862
|
if (this.debug) {
|
|
40872
40863
|
console.log('[TimeSalesWidget] Unexpected message format:', message);
|
|
40873
40864
|
}
|
|
@@ -41094,7 +41085,15 @@ class ApiService {
|
|
|
41094
41085
|
try {
|
|
41095
41086
|
const response = await fetch(url, config);
|
|
41096
41087
|
if (!response.ok) {
|
|
41097
|
-
|
|
41088
|
+
// Try to get response body for error details
|
|
41089
|
+
let errorBody = '';
|
|
41090
|
+
try {
|
|
41091
|
+
errorBody = await response.text();
|
|
41092
|
+
console.error(`[ApiService] HTTP ${response.status} - Response body:`, errorBody);
|
|
41093
|
+
} catch (e) {
|
|
41094
|
+
console.error(`[ApiService] Could not read error response body:`, e);
|
|
41095
|
+
}
|
|
41096
|
+
throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
|
|
41098
41097
|
}
|
|
41099
41098
|
return await response.json();
|
|
41100
41099
|
} catch (error) {
|
|
@@ -41583,6 +41582,7 @@ class WebSocketManager {
|
|
|
41583
41582
|
}
|
|
41584
41583
|
}
|
|
41585
41584
|
handleMessage(event) {
|
|
41585
|
+
//console.log('EVENT', event)
|
|
41586
41586
|
try {
|
|
41587
41587
|
// Update activity tracking - connection is alive!
|
|
41588
41588
|
this.lastMessageReceived = Date.now();
|
|
@@ -41607,22 +41607,20 @@ class WebSocketManager {
|
|
|
41607
41607
|
return;
|
|
41608
41608
|
}
|
|
41609
41609
|
|
|
41610
|
-
// Determine
|
|
41611
|
-
const
|
|
41610
|
+
// Determine type based on error content, default to 'error'
|
|
41611
|
+
const errorType = this._getTargetTypeFromErrorMessage(textMessage) || 'error';
|
|
41612
41612
|
if (textMessage.toLowerCase().includes('no night session') || textMessage.toLowerCase().includes('no data')) {
|
|
41613
41613
|
message = {
|
|
41614
|
-
type:
|
|
41614
|
+
type: errorType,
|
|
41615
41615
|
message: textMessage,
|
|
41616
|
-
error:
|
|
41617
|
-
noData: true
|
|
41618
|
-
targetType: targetType // Will be 'querynightsession' for night session errors
|
|
41616
|
+
error: true,
|
|
41617
|
+
noData: true
|
|
41619
41618
|
};
|
|
41620
41619
|
} else {
|
|
41621
41620
|
message = {
|
|
41622
|
-
type:
|
|
41621
|
+
type: errorType,
|
|
41623
41622
|
message: textMessage,
|
|
41624
|
-
error:
|
|
41625
|
-
targetType: targetType // Could be null for general errors
|
|
41623
|
+
error: true
|
|
41626
41624
|
};
|
|
41627
41625
|
}
|
|
41628
41626
|
}
|
|
@@ -41630,9 +41628,16 @@ class WebSocketManager {
|
|
|
41630
41628
|
console.log('[WebSocketManager] Processed message:', message);
|
|
41631
41629
|
}
|
|
41632
41630
|
|
|
41633
|
-
//
|
|
41634
|
-
|
|
41635
|
-
|
|
41631
|
+
// Extract data field for night session messages to avoid nested structure
|
|
41632
|
+
// Night session format: { type: 'queryonbbol1', error: false, data: [...] }
|
|
41633
|
+
// We want to send just the data array to widgets
|
|
41634
|
+
let dataToRoute = message;
|
|
41635
|
+
/* if (message.data !== undefined && message.type !== undefined) {
|
|
41636
|
+
// This is night session format - extract the data
|
|
41637
|
+
dataToRoute = message.data;
|
|
41638
|
+
} */
|
|
41639
|
+
|
|
41640
|
+
this._routeMessage(dataToRoute);
|
|
41636
41641
|
} catch (error) {
|
|
41637
41642
|
console.error('[WebSocketManager] Error handling message:', error);
|
|
41638
41643
|
this._notifyWidgets('error', {
|
|
@@ -41771,7 +41776,31 @@ class WebSocketManager {
|
|
|
41771
41776
|
}
|
|
41772
41777
|
} else {
|
|
41773
41778
|
if (this.config.debug) {
|
|
41774
|
-
console.log(`[WebSocketManager] Subscription ${subscriptionKey} already active, skipping`);
|
|
41779
|
+
console.log(`[WebSocketManager] Subscription ${subscriptionKey} already active, skipping server subscription`);
|
|
41780
|
+
}
|
|
41781
|
+
|
|
41782
|
+
// Subscription already active, but send cached data to newly subscribing widgets
|
|
41783
|
+
const cachedMessage = this.lastMessageCache.get(subscriptionKey);
|
|
41784
|
+
console.log('LAST', this.lastMessageCache);
|
|
41785
|
+
if (cachedMessage) {
|
|
41786
|
+
if (this.config.debug) {
|
|
41787
|
+
console.log(`[WebSocketManager] Sending cached data to newly subscribing widgets for ${subscriptionKey}`);
|
|
41788
|
+
}
|
|
41789
|
+
|
|
41790
|
+
// Find all widgets subscribed to this type:symbol combination
|
|
41791
|
+
this.subscriptions.forEach((subscription, widgetId) => {
|
|
41792
|
+
if (subscription.types.has(type) && subscription.symbol === symbol) {
|
|
41793
|
+
try {
|
|
41794
|
+
subscription.callback({
|
|
41795
|
+
event: 'data',
|
|
41796
|
+
data: cachedMessage,
|
|
41797
|
+
widgetId
|
|
41798
|
+
});
|
|
41799
|
+
} catch (error) {
|
|
41800
|
+
console.error(`[WebSocketManager] Error sending cached data to widget ${widgetId}:`, error);
|
|
41801
|
+
}
|
|
41802
|
+
}
|
|
41803
|
+
});
|
|
41775
41804
|
}
|
|
41776
41805
|
}
|
|
41777
41806
|
});
|
|
@@ -41823,14 +41852,9 @@ class WebSocketManager {
|
|
|
41823
41852
|
* The data field structure varies based on the type
|
|
41824
41853
|
*/
|
|
41825
41854
|
_normalizeMessage(message) {
|
|
41826
|
-
// If message already has targetType (error messages), return as is
|
|
41827
|
-
if (message.targetType) {
|
|
41828
|
-
return message;
|
|
41829
|
-
}
|
|
41830
|
-
|
|
41831
41855
|
// Check for error field (case-insensitive)
|
|
41832
|
-
const errorField = message.error
|
|
41833
|
-
const typeField = message.type
|
|
41856
|
+
const errorField = message.error;
|
|
41857
|
+
const typeField = message.type;
|
|
41834
41858
|
const messageField = message.message || message.Message;
|
|
41835
41859
|
|
|
41836
41860
|
// Handle explicit error messages - route based on type field
|
|
@@ -41838,13 +41862,12 @@ class WebSocketManager {
|
|
|
41838
41862
|
if (this.config.debug) {
|
|
41839
41863
|
console.log(`[WebSocketManager] Detected error message with type: ${typeField}`);
|
|
41840
41864
|
}
|
|
41841
|
-
|
|
41842
|
-
// Set targetType for routing to specific widget types
|
|
41843
41865
|
return {
|
|
41844
41866
|
...message,
|
|
41845
|
-
|
|
41846
|
-
type
|
|
41847
|
-
|
|
41867
|
+
// Only add type if message doesn't already have one
|
|
41868
|
+
...(!message.type && !message.Type && typeField ? {
|
|
41869
|
+
type: typeField
|
|
41870
|
+
} : {})
|
|
41848
41871
|
};
|
|
41849
41872
|
}
|
|
41850
41873
|
|
|
@@ -41862,92 +41885,30 @@ class WebSocketManager {
|
|
|
41862
41885
|
// Treat as error and route based on type
|
|
41863
41886
|
return {
|
|
41864
41887
|
...message,
|
|
41865
|
-
|
|
41866
|
-
type
|
|
41867
|
-
|
|
41868
|
-
|
|
41888
|
+
// Only add type if message doesn't already have one
|
|
41889
|
+
...(!message.type && !message.Type && typeField ? {
|
|
41890
|
+
type: typeField
|
|
41891
|
+
} : {}),
|
|
41892
|
+
error: true // Mark as error since it was detected as implicit error
|
|
41869
41893
|
};
|
|
41870
41894
|
}
|
|
41871
41895
|
}
|
|
41872
41896
|
|
|
41873
|
-
// Check for new format: has 'type'/'Type' AND 'data'/'Data' fields
|
|
41874
|
-
const dataField = message.data || message.Data;
|
|
41875
|
-
if (typeField && dataField !== undefined) {
|
|
41876
|
-
if (this.config.debug) {
|
|
41877
|
-
console.log(`[WebSocketManager] Detected new message format with type: ${typeField}`);
|
|
41878
|
-
}
|
|
41879
|
-
|
|
41880
|
-
// Extract the actual data and add the type to it for routing
|
|
41881
|
-
let normalizedData;
|
|
41882
|
-
if (Array.isArray(dataField)) {
|
|
41883
|
-
// If data is an array, process each item
|
|
41884
|
-
normalizedData = dataField;
|
|
41885
|
-
// Add type info to the structure for routing
|
|
41886
|
-
if (normalizedData.length > 0) {
|
|
41887
|
-
normalizedData._messageType = typeField;
|
|
41888
|
-
}
|
|
41889
|
-
} else if (typeof dataField === 'object' && dataField !== null) {
|
|
41890
|
-
// If data is an object, use it directly
|
|
41891
|
-
normalizedData = dataField;
|
|
41892
|
-
// Add type info for routing
|
|
41893
|
-
normalizedData._messageType = typeField;
|
|
41894
|
-
} else {
|
|
41895
|
-
// Primitive value, wrap it
|
|
41896
|
-
normalizedData = {
|
|
41897
|
-
value: dataField,
|
|
41898
|
-
_messageType: typeField
|
|
41899
|
-
};
|
|
41900
|
-
}
|
|
41901
|
-
|
|
41902
|
-
// Also preserve the original type at the root level for backward compatibility
|
|
41903
|
-
if (typeof normalizedData === 'object' && !normalizedData.type) {
|
|
41904
|
-
normalizedData.type = typeField;
|
|
41905
|
-
}
|
|
41906
|
-
return normalizedData;
|
|
41907
|
-
}
|
|
41908
|
-
|
|
41909
41897
|
// Old format or no type/data structure, return as is
|
|
41910
41898
|
return message;
|
|
41911
41899
|
}
|
|
41912
41900
|
_routeMessage(message) {
|
|
41913
41901
|
//console.log('message', message);
|
|
41914
41902
|
|
|
41915
|
-
|
|
41916
|
-
|
|
41917
|
-
|
|
41918
|
-
|
|
41919
|
-
return; // Don't route empty arrays
|
|
41920
|
-
}
|
|
41903
|
+
// IMPORTANT: Don't filter out empty arrays here - route them based on message.type
|
|
41904
|
+
// Widgets need to receive empty arrays to clear loading states and show "no data" messages
|
|
41905
|
+
// The message structure is: { type: 'queryoptionchain', data: [] }
|
|
41906
|
+
// We need to route based on the 'type' field, not the data content
|
|
41921
41907
|
|
|
41922
41908
|
// Cache the message for later use by new subscribers
|
|
41923
41909
|
this._cacheMessage(message);
|
|
41924
41910
|
|
|
41925
|
-
//
|
|
41926
|
-
if (message.targetType) {
|
|
41927
|
-
const targetWidgets = this.typeSubscriptions.get(message.targetType);
|
|
41928
|
-
if (targetWidgets && targetWidgets.size > 0) {
|
|
41929
|
-
if (this.config.debug) {
|
|
41930
|
-
console.log(`[WebSocketManager] Routing ${message.targetType} error to specific widgets:`, [...targetWidgets]);
|
|
41931
|
-
}
|
|
41932
|
-
targetWidgets.forEach(widgetId => {
|
|
41933
|
-
const subscription = this.subscriptions.get(widgetId);
|
|
41934
|
-
if (subscription) {
|
|
41935
|
-
try {
|
|
41936
|
-
subscription.callback({
|
|
41937
|
-
event: 'data',
|
|
41938
|
-
data: message,
|
|
41939
|
-
widgetId
|
|
41940
|
-
});
|
|
41941
|
-
} catch (error) {
|
|
41942
|
-
console.error(`[WebSocketManager] Error in widget ${widgetId} callback:`, error);
|
|
41943
|
-
}
|
|
41944
|
-
}
|
|
41945
|
-
});
|
|
41946
|
-
return; // Don't fall through to broadcast
|
|
41947
|
-
}
|
|
41948
|
-
}
|
|
41949
|
-
|
|
41950
|
-
//console.log('here');
|
|
41911
|
+
// Get relevant widgets based on message type and symbol
|
|
41951
41912
|
const relevantWidgets = this._getRelevantWidgets(message);
|
|
41952
41913
|
if (this.config.debug && relevantWidgets.size > 0) {
|
|
41953
41914
|
console.log(`[WebSocketManager] Routing message to ${relevantWidgets.size} relevant widgets`);
|
|
@@ -41990,42 +41951,49 @@ class WebSocketManager {
|
|
|
41990
41951
|
_cacheMessage(message) {
|
|
41991
41952
|
try {
|
|
41992
41953
|
// Extract symbol and message type to create cache key
|
|
41993
|
-
|
|
41994
|
-
|
|
41954
|
+
// This allows new widgets to get instant data when subscribing
|
|
41955
|
+
// check in message.data or message.Data
|
|
41956
|
+
let data;
|
|
41957
|
+
if (message.data) {
|
|
41958
|
+
data = message.data;
|
|
41959
|
+
} else if (message.Data) {
|
|
41960
|
+
data = message.Data;
|
|
41961
|
+
} else {
|
|
41962
|
+
data = message;
|
|
41963
|
+
}
|
|
41964
|
+
const symbol = this._extractSymbol(data);
|
|
41965
|
+
const messageType = message.type;
|
|
41966
|
+
if (this.config.debug) {
|
|
41967
|
+
console.log(`[WebSocketManager] _cacheMessage - extracted symbol: "${symbol}", messageType: "${messageType}"`);
|
|
41968
|
+
}
|
|
41969
|
+
|
|
41970
|
+
// Cache by type:symbol combination
|
|
41995
41971
|
if (messageType && symbol) {
|
|
41996
41972
|
const cacheKey = `${messageType}:${symbol}`;
|
|
41997
41973
|
this.lastMessageCache.set(cacheKey, message);
|
|
41998
41974
|
if (this.config.debug) {
|
|
41999
41975
|
console.log(`[WebSocketManager] Cached message for ${cacheKey}`);
|
|
42000
41976
|
}
|
|
42001
|
-
}
|
|
42002
|
-
|
|
42003
|
-
|
|
42004
|
-
|
|
42005
|
-
const firstItem = message[0];
|
|
42006
|
-
const symbol = firstItem.Symbol;
|
|
42007
|
-
const messageType = this._extractMessageType(message);
|
|
42008
|
-
if (messageType && symbol) {
|
|
42009
|
-
const cacheKey = `${messageType}:${symbol}`;
|
|
42010
|
-
this.lastMessageCache.set(cacheKey, message);
|
|
41977
|
+
} else {
|
|
41978
|
+
// If we can't extract symbol from message, try to cache by type only
|
|
41979
|
+
// This helps with data like ONBBO L2 which doesn't have symbol in the message
|
|
41980
|
+
if (messageType) {
|
|
42011
41981
|
if (this.config.debug) {
|
|
42012
|
-
console.log(`[WebSocketManager]
|
|
41982
|
+
console.log(`[WebSocketManager] No symbol found in message, attempting to infer from subscriptions for type: ${messageType}`);
|
|
42013
41983
|
}
|
|
42014
|
-
}
|
|
42015
|
-
}
|
|
42016
41984
|
|
|
42017
|
-
|
|
42018
|
-
|
|
42019
|
-
|
|
42020
|
-
|
|
42021
|
-
|
|
42022
|
-
|
|
42023
|
-
|
|
42024
|
-
|
|
42025
|
-
|
|
41985
|
+
// Find active subscriptions for this message type and cache for each
|
|
41986
|
+
this.activeSubscriptions.forEach(activeKey => {
|
|
41987
|
+
const [type, sym] = activeKey.split(':');
|
|
41988
|
+
if (type === messageType && sym) {
|
|
41989
|
+
const cacheKey = activeKey;
|
|
41990
|
+
this.lastMessageCache.set(cacheKey, message);
|
|
41991
|
+
if (this.config.debug) {
|
|
41992
|
+
console.log(`[WebSocketManager] Cached message for ${cacheKey} (inferred from active subscription)`);
|
|
41993
|
+
}
|
|
42026
41994
|
}
|
|
42027
|
-
}
|
|
42028
|
-
}
|
|
41995
|
+
});
|
|
41996
|
+
}
|
|
42029
41997
|
}
|
|
42030
41998
|
} catch (error) {
|
|
42031
41999
|
console.error('[WebSocketManager] Error caching message:', error);
|
|
@@ -42034,22 +42002,28 @@ class WebSocketManager {
|
|
|
42034
42002
|
_getRelevantWidgets(message) {
|
|
42035
42003
|
const relevantWidgets = new Set();
|
|
42036
42004
|
|
|
42005
|
+
// OPTIMIZATION: Extract message type once from the entire message, not from each data item
|
|
42006
|
+
// Message structure: { type: 'queryoptionchain', data: [] }
|
|
42007
|
+
let dataField = message.Data || message.data;
|
|
42008
|
+
const messageType = message.type || this._extractMessageType(dataField);
|
|
42009
|
+
|
|
42037
42010
|
// Handle array messages
|
|
42038
|
-
this._addRelevantWidgetsForItem(message, relevantWidgets);
|
|
42011
|
+
this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
|
|
42039
42012
|
return relevantWidgets;
|
|
42040
42013
|
}
|
|
42041
|
-
_addRelevantWidgetsForItem(item, relevantWidgets) {
|
|
42014
|
+
_addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
|
|
42042
42015
|
// If item has Data array, process it
|
|
42043
42016
|
if (item.Data && Array.isArray(item.Data)) {
|
|
42044
42017
|
//console.log('Data array found with length:', item.Data.length);
|
|
42045
42018
|
item.Data.forEach(dataItem => {
|
|
42046
|
-
|
|
42019
|
+
// Pass messageType from parent message, don't extract from each data item
|
|
42020
|
+
this._processDataItem(dataItem, relevantWidgets, messageType);
|
|
42047
42021
|
});
|
|
42048
42022
|
} else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
|
|
42049
42023
|
this._processOptionChainData(item, relevantWidgets);
|
|
42050
42024
|
} else {
|
|
42051
|
-
// Process single item
|
|
42052
|
-
this._processDataItem(item, relevantWidgets);
|
|
42025
|
+
// Process single item - messageType already extracted from message
|
|
42026
|
+
this._processDataItem(item, relevantWidgets, messageType);
|
|
42053
42027
|
}
|
|
42054
42028
|
}
|
|
42055
42029
|
|
|
@@ -42093,12 +42067,11 @@ class WebSocketManager {
|
|
|
42093
42067
|
}
|
|
42094
42068
|
}
|
|
42095
42069
|
}
|
|
42096
|
-
_processDataItem(dataItem, relevantWidgets) {
|
|
42070
|
+
_processDataItem(dataItem, relevantWidgets, messageType) {
|
|
42097
42071
|
// Process individual data items and route to appropriate widgets
|
|
42098
42072
|
// Option chain arrays are handled separately by _processOptionChainData
|
|
42099
42073
|
|
|
42100
42074
|
const symbol = this._extractSymbol(dataItem);
|
|
42101
|
-
const messageType = this._extractMessageType(dataItem);
|
|
42102
42075
|
if (this.config.debug) {
|
|
42103
42076
|
console.log('[WebSocketManager] Processing data item:', {
|
|
42104
42077
|
symbol,
|
|
@@ -42192,7 +42165,7 @@ class WebSocketManager {
|
|
|
42192
42165
|
_extractSymbol(item) {
|
|
42193
42166
|
// Handle new format where symbol might be in nested data structure
|
|
42194
42167
|
// Check if this is an array with data that has _messageType (normalized new format)
|
|
42195
|
-
if (Array.isArray(item)
|
|
42168
|
+
if (Array.isArray(item)) {
|
|
42196
42169
|
// The data is an array, check first item for symbol
|
|
42197
42170
|
if (item.length > 0 && item[0]) {
|
|
42198
42171
|
return this._extractSymbolFromItem(item[0]);
|
|
@@ -42258,25 +42231,15 @@ class WebSocketManager {
|
|
|
42258
42231
|
return symbolMappings[extracted] || extracted;
|
|
42259
42232
|
}
|
|
42260
42233
|
_extractMessageType(item) {
|
|
42261
|
-
//
|
|
42262
|
-
if (item._messageType) {
|
|
42263
|
-
return item._messageType;
|
|
42264
|
-
}
|
|
42265
|
-
|
|
42266
|
-
// Determine message type based on content
|
|
42267
|
-
if (item.type) return item.type;
|
|
42268
|
-
|
|
42269
|
-
// Infer type from data structure
|
|
42234
|
+
// FALLBACK: Infer type from data structure (for legacy/malformed messages)
|
|
42270
42235
|
|
|
42271
42236
|
// IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
|
|
42272
42237
|
// Level 2 ONBBO data - array of MMID objects or object with MMID field
|
|
42273
42238
|
if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
|
|
42274
42239
|
return 'queryonbbol2';
|
|
42275
42240
|
}
|
|
42276
|
-
|
|
42277
|
-
|
|
42278
|
-
if (item.MMID !== undefined) {
|
|
42279
|
-
return 'queryonbbol2';
|
|
42241
|
+
if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
|
|
42242
|
+
return 'queryoptionchain';
|
|
42280
42243
|
}
|
|
42281
42244
|
|
|
42282
42245
|
// Check for Source field AFTER MMID check
|