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.js
CHANGED
|
@@ -412,6 +412,85 @@
|
|
|
412
412
|
console.log(`[${this.constructor.name}] Connection failed after ${status.maxAttempts} attempts`);
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Validate initial symbol with the API and handle access/permission errors
|
|
418
|
+
* This is a reusable method for all widgets that need to validate symbols on initialization
|
|
419
|
+
*
|
|
420
|
+
* @param {string} apiMethod - The API method name to call (e.g., 'quotel1', 'quoteOptionl1')
|
|
421
|
+
* @param {string} symbol - The symbol to validate
|
|
422
|
+
* @param {Function} onSuccess - Callback when validation succeeds with data, receives (data, result)
|
|
423
|
+
* @returns {boolean} - Returns false if access denied (stops initialization), true otherwise
|
|
424
|
+
*/
|
|
425
|
+
async validateInitialSymbol(apiMethod, symbol) {
|
|
426
|
+
let onSuccess = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
427
|
+
try {
|
|
428
|
+
const apiService = this.wsManager?.getApiService();
|
|
429
|
+
if (!apiService) {
|
|
430
|
+
if (this.debug) {
|
|
431
|
+
console.log(`[${this.constructor.name}] API service not available for initial validation`);
|
|
432
|
+
}
|
|
433
|
+
return true; // Continue without validation
|
|
434
|
+
}
|
|
435
|
+
if (!apiService[apiMethod]) {
|
|
436
|
+
console.error(`[${this.constructor.name}] API method ${apiMethod} does not exist`);
|
|
437
|
+
return true; // Continue without validation
|
|
438
|
+
}
|
|
439
|
+
const result = await apiService[apiMethod](symbol);
|
|
440
|
+
if (result && result.data && result.data.length > 0) {
|
|
441
|
+
if (this.debug) {
|
|
442
|
+
console.log(`[${this.constructor.name}] Initial symbol validated:`, {
|
|
443
|
+
symbol,
|
|
444
|
+
method: apiMethod,
|
|
445
|
+
dataCount: result.data.length
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Call success callback if provided
|
|
450
|
+
if (onSuccess && typeof onSuccess === 'function') {
|
|
451
|
+
onSuccess(result.data, result);
|
|
452
|
+
}
|
|
453
|
+
return true; // Validation successful
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// No data returned - might be access issue, but continue anyway
|
|
457
|
+
if (this.debug) {
|
|
458
|
+
console.log(`[${this.constructor.name}] No data returned from ${apiMethod} for ${symbol}`);
|
|
459
|
+
}
|
|
460
|
+
return true; // Continue anyway, WebSocket might still work
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (this.debug) {
|
|
463
|
+
console.warn(`[${this.constructor.name}] Initial symbol validation failed:`, error);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Check if it's an access/permission error
|
|
467
|
+
const errorMessage = (error.message || '').toLowerCase();
|
|
468
|
+
const isAccessError = errorMessage.includes('400') ||
|
|
469
|
+
// Bad Request (often used for access issues)
|
|
470
|
+
errorMessage.includes('401') ||
|
|
471
|
+
// Unauthorized
|
|
472
|
+
errorMessage.includes('403') ||
|
|
473
|
+
// Forbidden
|
|
474
|
+
errorMessage.includes('forbidden') || errorMessage.includes('unauthorized') || errorMessage.includes('no access') || errorMessage.includes('no opra access') ||
|
|
475
|
+
// Specific OPRA access denial
|
|
476
|
+
errorMessage.includes('permission denied') || errorMessage.includes('access denied');
|
|
477
|
+
if (isAccessError) {
|
|
478
|
+
// Hide loading and show access error
|
|
479
|
+
this.hideLoading();
|
|
480
|
+
|
|
481
|
+
// Extract more specific error message if available
|
|
482
|
+
let userMessage = `Access denied: You don't have permission to view data for ${symbol}`;
|
|
483
|
+
if (errorMessage.includes('no opra access')) {
|
|
484
|
+
userMessage = `Access denied: You don't have OPRA access for option ${symbol}`;
|
|
485
|
+
}
|
|
486
|
+
this.showError(userMessage);
|
|
487
|
+
return false; // Stop initialization
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// For other errors, continue with WebSocket (might still work)
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
415
494
|
}
|
|
416
495
|
|
|
417
496
|
// src/models/MarketDataModel.js
|
|
@@ -1283,6 +1362,24 @@
|
|
|
1283
1362
|
<span class="l2-market-name"></span>
|
|
1284
1363
|
</div>
|
|
1285
1364
|
<span class="symbol editable-symbol">--</span>
|
|
1365
|
+
<div class="level1-info">
|
|
1366
|
+
<div class="l1-item">
|
|
1367
|
+
<span class="l1-label">Last:</span>
|
|
1368
|
+
<span class="l1-value l1-last-px">--</span>
|
|
1369
|
+
</div>
|
|
1370
|
+
<div class="l1-item">
|
|
1371
|
+
<span class="l1-label">Low:</span>
|
|
1372
|
+
<span class="l1-value l1-low-px">--</span>
|
|
1373
|
+
</div>
|
|
1374
|
+
<div class="l1-item">
|
|
1375
|
+
<span class="l1-label">High:</span>
|
|
1376
|
+
<span class="l1-value l1-high-px">--</span>
|
|
1377
|
+
</div>
|
|
1378
|
+
<div class="l1-item">
|
|
1379
|
+
<span class="l1-label">Volume:</span>
|
|
1380
|
+
<span class="l1-value l1-volume">--</span>
|
|
1381
|
+
</div>
|
|
1382
|
+
</div>
|
|
1286
1383
|
</div>
|
|
1287
1384
|
</div>
|
|
1288
1385
|
|
|
@@ -1957,6 +2054,38 @@
|
|
|
1957
2054
|
color: #111827;
|
|
1958
2055
|
}
|
|
1959
2056
|
|
|
2057
|
+
/* ========================================
|
|
2058
|
+
LEVEL 1 INFO (INSIDE HEADER)
|
|
2059
|
+
======================================== */
|
|
2060
|
+
|
|
2061
|
+
.onbbo-level2-widget .level1-info {
|
|
2062
|
+
display: grid;
|
|
2063
|
+
grid-template-columns: repeat(4, 1fr);
|
|
2064
|
+
gap: 12px;
|
|
2065
|
+
margin-top: 10px;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
.onbbo-level2-widget .l1-item {
|
|
2069
|
+
display: flex;
|
|
2070
|
+
flex-direction: column;
|
|
2071
|
+
gap: 3px;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
.onbbo-level2-widget .l1-label {
|
|
2075
|
+
font-size: 11px;
|
|
2076
|
+
font-weight: 600;
|
|
2077
|
+
color: #6b7280;
|
|
2078
|
+
text-transform: uppercase;
|
|
2079
|
+
letter-spacing: 0.5px;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
.onbbo-level2-widget .l1-value {
|
|
2083
|
+
font-size: 15px;
|
|
2084
|
+
font-weight: 700;
|
|
2085
|
+
color: #111827;
|
|
2086
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1960
2089
|
/* ========================================
|
|
1961
2090
|
ORDER BOOK CONTAINER
|
|
1962
2091
|
======================================== */
|
|
@@ -2341,6 +2470,11 @@
|
|
|
2341
2470
|
.onbbo-level2-widget .orderbook-panel:last-child {
|
|
2342
2471
|
border-bottom: none;
|
|
2343
2472
|
}
|
|
2473
|
+
|
|
2474
|
+
.onbbo-level2-widget .level1-info {
|
|
2475
|
+
grid-template-columns: repeat(2, 1fr);
|
|
2476
|
+
gap: 6px;
|
|
2477
|
+
}
|
|
2344
2478
|
}
|
|
2345
2479
|
|
|
2346
2480
|
@media (max-width: 480px) {
|
|
@@ -2359,6 +2493,19 @@
|
|
|
2359
2493
|
text-align: left;
|
|
2360
2494
|
}
|
|
2361
2495
|
|
|
2496
|
+
.onbbo-level2-widget .level1-info {
|
|
2497
|
+
grid-template-columns: repeat(2, 1fr);
|
|
2498
|
+
gap: 4px;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
.onbbo-level2-widget .l1-label {
|
|
2502
|
+
font-size: 9px;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
.onbbo-level2-widget .l1-value {
|
|
2506
|
+
font-size: 11px;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2362
2509
|
.onbbo-level2-widget .panel-header {
|
|
2363
2510
|
font-size: 10px;
|
|
2364
2511
|
padding: 6px 2px;
|
|
@@ -2442,28 +2589,6 @@
|
|
|
2442
2589
|
font-size: 1.71em;
|
|
2443
2590
|
font-weight: 700;
|
|
2444
2591
|
color: #111827;
|
|
2445
|
-
cursor: pointer;
|
|
2446
|
-
transition: all 0.2s ease;
|
|
2447
|
-
position: relative;
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
.time-sales-widget .symbol.editable-symbol:hover {
|
|
2451
|
-
opacity: 0.8;
|
|
2452
|
-
transform: scale(1.02);
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
.time-sales-widget .symbol.editable-symbol:hover::after {
|
|
2456
|
-
content: "✎";
|
|
2457
|
-
position: absolute;
|
|
2458
|
-
top: -8px;
|
|
2459
|
-
right: -8px;
|
|
2460
|
-
background: #3b82f6;
|
|
2461
|
-
color: white;
|
|
2462
|
-
font-size: 10px;
|
|
2463
|
-
padding: 2px 4px;
|
|
2464
|
-
border-radius: 4px;
|
|
2465
|
-
opacity: 0.8;
|
|
2466
|
-
pointer-events: none;
|
|
2467
2592
|
}
|
|
2468
2593
|
|
|
2469
2594
|
/* ========================================
|
|
@@ -4415,105 +4540,53 @@
|
|
|
4415
4540
|
this.showLoading();
|
|
4416
4541
|
|
|
4417
4542
|
// Validate initial symbol via API to get company info
|
|
4418
|
-
|
|
4419
|
-
this.
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
const apiService = this.wsManager.getApiService();
|
|
4424
|
-
if (!apiService) {
|
|
4425
|
-
if (this.debug) {
|
|
4426
|
-
console.log('[MarketDataWidget] API service not available for initial validation');
|
|
4427
|
-
}
|
|
4428
|
-
return;
|
|
4543
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting data
|
|
4544
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
4545
|
+
// Store for use in updateWidget
|
|
4546
|
+
if (data && data[0]) {
|
|
4547
|
+
this.initialValidationData = data[0];
|
|
4429
4548
|
}
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
if (this.debug) {
|
|
4436
|
-
console.log('[MarketDataWidget] Initial symbol validated:', {
|
|
4437
|
-
symbol: this.symbol,
|
|
4438
|
-
companyName: symbolData.comp_name,
|
|
4439
|
-
exchangeName: symbolData.market_name
|
|
4440
|
-
});
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
|
-
} catch (error) {
|
|
4444
|
-
if (this.debug) {
|
|
4445
|
-
console.warn('[MarketDataWidget] Initial symbol validation failed:', error);
|
|
4446
|
-
}
|
|
4447
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
4549
|
+
});
|
|
4550
|
+
|
|
4551
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
4552
|
+
if (validationSuccess === false) {
|
|
4553
|
+
return; // Error is already shown, don't continue
|
|
4448
4554
|
}
|
|
4555
|
+
this.subscribeToData();
|
|
4449
4556
|
}
|
|
4450
4557
|
subscribeToData() {
|
|
4451
4558
|
// Subscribe with symbol for routing
|
|
4452
4559
|
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
|
|
4453
4560
|
);
|
|
4454
|
-
|
|
4455
|
-
// Send subscription message
|
|
4456
|
-
/* this.wsManager.send({
|
|
4457
|
-
type: 'queryl1',
|
|
4458
|
-
symbol: this.symbol
|
|
4459
|
-
}); */
|
|
4460
4561
|
}
|
|
4562
|
+
handleData(message) {
|
|
4563
|
+
//console.log('DEBUG', message)
|
|
4461
4564
|
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
if (this.debug) {
|
|
4467
|
-
console.log('[MarketDataWidget] Received:', event, data);
|
|
4468
|
-
}
|
|
4469
|
-
if (event === 'connection') {
|
|
4470
|
-
this.handleConnectionStatus(data);
|
|
4471
|
-
return;
|
|
4472
|
-
}
|
|
4473
|
-
if (event === 'data') {
|
|
4474
|
-
this.handleData(data);
|
|
4475
|
-
}
|
|
4476
|
-
if (event === 'session_revoked') {
|
|
4477
|
-
if (data.status === 'attempting_relogin') {
|
|
4478
|
-
this.showLoading();
|
|
4479
|
-
} else if (data.status === 'relogin_failed') {
|
|
4480
|
-
this.showError(data.error);
|
|
4481
|
-
} else if (data.status === 'relogin_successful') {
|
|
4482
|
-
this.hideLoading();
|
|
4483
|
-
}
|
|
4484
|
-
return;
|
|
4485
|
-
}
|
|
4486
|
-
} catch (error) {
|
|
4487
|
-
console.error('[MarketDataWidget] Error handling message:', error);
|
|
4488
|
-
this.showError('Error processing data');
|
|
4565
|
+
// Check for error: false and display message if present
|
|
4566
|
+
if (message.error === true) {
|
|
4567
|
+
if (this.debug) {
|
|
4568
|
+
console.log('[MarketDataWidget] Received error response:', message.message);
|
|
4489
4569
|
}
|
|
4490
|
-
|
|
4570
|
+
const errorMsg = message.message || message.Message;
|
|
4491
4571
|
|
|
4492
|
-
|
|
4493
|
-
if (
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
// Re-send subscription when reconnected
|
|
4498
|
-
this.wsManager.send({
|
|
4499
|
-
type: 'queryl1',
|
|
4500
|
-
symbol: this.symbol
|
|
4501
|
-
});
|
|
4502
|
-
} else if (status.status === 'disconnected') {
|
|
4503
|
-
this.showError('Disconnected from data service');
|
|
4504
|
-
} else if (status.status === 'error') {
|
|
4505
|
-
this.showError(status.error || 'Connection error');
|
|
4572
|
+
// If there's a message field, show it as an error
|
|
4573
|
+
if (errorMsg) {
|
|
4574
|
+
this.hideLoading();
|
|
4575
|
+
this.showError(errorMsg);
|
|
4576
|
+
return;
|
|
4506
4577
|
}
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4578
|
+
} else if (message.Data && typeof message.Data === 'string') {
|
|
4579
|
+
this.hideLoading();
|
|
4580
|
+
this.showError(message.Message);
|
|
4581
|
+
return;
|
|
4582
|
+
}
|
|
4510
4583
|
if (this.loadingTimeout) {
|
|
4511
4584
|
clearTimeout(this.loadingTimeout);
|
|
4512
4585
|
this.loadingTimeout = null;
|
|
4513
4586
|
}
|
|
4514
4587
|
|
|
4515
4588
|
// Handle error messages from server (plain text converted to structured format)
|
|
4516
|
-
if (message.type === 'error'
|
|
4589
|
+
if (message.type === 'error') {
|
|
4517
4590
|
if (this.debug) {
|
|
4518
4591
|
console.log('[MarketDataWidget] Received no data message:', message.message);
|
|
4519
4592
|
}
|
|
@@ -4592,126 +4665,6 @@
|
|
|
4592
4665
|
this.showConnectionQuality(); // Show cache indicator
|
|
4593
4666
|
}
|
|
4594
4667
|
}
|
|
4595
|
-
showNoDataState() {
|
|
4596
|
-
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
4597
|
-
if (this.isDestroyed) return;
|
|
4598
|
-
try {
|
|
4599
|
-
// Hide loading overlay
|
|
4600
|
-
this.hideLoading();
|
|
4601
|
-
|
|
4602
|
-
// Clear any existing errors
|
|
4603
|
-
this.clearError();
|
|
4604
|
-
|
|
4605
|
-
// Update header with dimmed styling and basic info from data
|
|
4606
|
-
const symbol = data.Symbol || this.symbol;
|
|
4607
|
-
|
|
4608
|
-
// Add null checks for all DOM elements
|
|
4609
|
-
const symbolElement = this.container.querySelector('.symbol');
|
|
4610
|
-
if (symbolElement) {
|
|
4611
|
-
symbolElement.textContent = symbol;
|
|
4612
|
-
}
|
|
4613
|
-
const companyNameElement = this.container.querySelector('.company-name');
|
|
4614
|
-
if (companyNameElement) {
|
|
4615
|
-
companyNameElement.textContent = `${symbol} Inc`;
|
|
4616
|
-
}
|
|
4617
|
-
|
|
4618
|
-
// Set price to $0.00 and add dimmed class
|
|
4619
|
-
const currentPriceElement = this.container.querySelector('.current-price');
|
|
4620
|
-
if (currentPriceElement) {
|
|
4621
|
-
currentPriceElement.textContent = '$0.00';
|
|
4622
|
-
}
|
|
4623
|
-
const changeElement = this.container.querySelector('.price-change');
|
|
4624
|
-
if (changeElement) {
|
|
4625
|
-
const changeValueElement = changeElement.querySelector('.change-value');
|
|
4626
|
-
const changePercentElement = changeElement.querySelector('.change-percent');
|
|
4627
|
-
if (changeValueElement) {
|
|
4628
|
-
changeValueElement.textContent = '+0.00';
|
|
4629
|
-
}
|
|
4630
|
-
if (changePercentElement) {
|
|
4631
|
-
changePercentElement.textContent = ' (0.00%)';
|
|
4632
|
-
}
|
|
4633
|
-
changeElement.classList.remove('positive', 'negative');
|
|
4634
|
-
changeElement.classList.add('neutral');
|
|
4635
|
-
}
|
|
4636
|
-
|
|
4637
|
-
// Add dimmed styling to header
|
|
4638
|
-
const widgetHeader = this.container.querySelector('.widget-header');
|
|
4639
|
-
if (widgetHeader) {
|
|
4640
|
-
widgetHeader.classList.add('dimmed');
|
|
4641
|
-
}
|
|
4642
|
-
|
|
4643
|
-
// Replace the data grid with no data message
|
|
4644
|
-
this.showNoDataMessage(symbol, data);
|
|
4645
|
-
|
|
4646
|
-
// Update footer with current timestamp
|
|
4647
|
-
const lastUpdateElement = this.container.querySelector('.last-update');
|
|
4648
|
-
if (lastUpdateElement) {
|
|
4649
|
-
const timestamp = formatTimestampET();
|
|
4650
|
-
lastUpdateElement.textContent = `Checked: ${timestamp}`;
|
|
4651
|
-
}
|
|
4652
|
-
const dataSourceElement = this.container.querySelector('.data-source');
|
|
4653
|
-
if (dataSourceElement) {
|
|
4654
|
-
dataSourceElement.textContent = 'Source: No data available';
|
|
4655
|
-
}
|
|
4656
|
-
} catch (error) {
|
|
4657
|
-
console.error('Error showing no data state:', error);
|
|
4658
|
-
this.showError('Error displaying no data state');
|
|
4659
|
-
}
|
|
4660
|
-
}
|
|
4661
|
-
showNoDataMessage(symbol) {
|
|
4662
|
-
const dataGrid = this.container.querySelector('.data-grid');
|
|
4663
|
-
|
|
4664
|
-
// Hide the data grid if it exists
|
|
4665
|
-
if (dataGrid) {
|
|
4666
|
-
dataGrid.style.display = 'none';
|
|
4667
|
-
}
|
|
4668
|
-
|
|
4669
|
-
// Remove existing no data message if present
|
|
4670
|
-
const existingNoData = this.container.querySelector('.no-data-state');
|
|
4671
|
-
if (existingNoData) {
|
|
4672
|
-
existingNoData.remove();
|
|
4673
|
-
}
|
|
4674
|
-
|
|
4675
|
-
// Create no data message element - safely without XSS risk
|
|
4676
|
-
const noDataElement = createElement('div', '', 'no-data-state');
|
|
4677
|
-
const noDataContent = createElement('div', '', 'no-data-content');
|
|
4678
|
-
|
|
4679
|
-
// Create icon
|
|
4680
|
-
const iconDiv = document.createElement('div');
|
|
4681
|
-
iconDiv.className = 'no-data-icon';
|
|
4682
|
-
iconDiv.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
4683
|
-
<circle cx="12" cy="12" r="10" stroke="#9ca3af" stroke-width="2"/>
|
|
4684
|
-
<path d="M12 8v4" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"/>
|
|
4685
|
-
<circle cx="12" cy="16" r="1" fill="#9ca3af"/>
|
|
4686
|
-
</svg>`;
|
|
4687
|
-
noDataContent.appendChild(iconDiv);
|
|
4688
|
-
|
|
4689
|
-
// Create title
|
|
4690
|
-
const title = createElement('h3', 'No Market Data', 'no-data-title');
|
|
4691
|
-
noDataContent.appendChild(title);
|
|
4692
|
-
|
|
4693
|
-
// Create description with sanitized symbol
|
|
4694
|
-
const description = createElement('p', '', 'no-data-description');
|
|
4695
|
-
description.appendChild(document.createTextNode('Market data for '));
|
|
4696
|
-
const symbolStrong = createElement('strong', sanitizeSymbol(symbol));
|
|
4697
|
-
description.appendChild(symbolStrong);
|
|
4698
|
-
description.appendChild(document.createTextNode(' was not found'));
|
|
4699
|
-
noDataContent.appendChild(description);
|
|
4700
|
-
|
|
4701
|
-
// Create guidance
|
|
4702
|
-
const guidance = createElement('p', 'Please check the symbol spelling or try a different symbol', 'no-data-guidance');
|
|
4703
|
-
noDataContent.appendChild(guidance);
|
|
4704
|
-
noDataElement.appendChild(noDataContent);
|
|
4705
|
-
|
|
4706
|
-
// Insert before footer, with null check
|
|
4707
|
-
const footer = this.container.querySelector('.widget-footer');
|
|
4708
|
-
if (footer && footer.parentNode) {
|
|
4709
|
-
footer.parentNode.insertBefore(noDataElement, footer);
|
|
4710
|
-
} else {
|
|
4711
|
-
// Fallback: append to container if footer not found
|
|
4712
|
-
this.container.appendChild(noDataElement);
|
|
4713
|
-
}
|
|
4714
|
-
}
|
|
4715
4668
|
updateWidget(data) {
|
|
4716
4669
|
if (this.isDestroyed) return;
|
|
4717
4670
|
try {
|
|
@@ -5216,7 +5169,20 @@
|
|
|
5216
5169
|
this.showLoading();
|
|
5217
5170
|
|
|
5218
5171
|
// Validate initial symbol via API to get company info
|
|
5219
|
-
|
|
5172
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
5173
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
5174
|
+
// Extract company info from first data item
|
|
5175
|
+
if (data && data[0]) {
|
|
5176
|
+
this.companyName = data[0].comp_name || '';
|
|
5177
|
+
this.exchangeName = data[0].market_name || '';
|
|
5178
|
+
this.mic = data[0].mic || '';
|
|
5179
|
+
}
|
|
5180
|
+
});
|
|
5181
|
+
|
|
5182
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
5183
|
+
if (validationSuccess === false) {
|
|
5184
|
+
return; // Error is already shown, don't continue
|
|
5185
|
+
}
|
|
5220
5186
|
|
|
5221
5187
|
// Set timeout to detect no data on initial load
|
|
5222
5188
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -5233,38 +5199,6 @@
|
|
|
5233
5199
|
|
|
5234
5200
|
this.subscribeToData();
|
|
5235
5201
|
}
|
|
5236
|
-
async validateInitialSymbol() {
|
|
5237
|
-
try {
|
|
5238
|
-
const apiService = this.wsManager.getApiService();
|
|
5239
|
-
if (!apiService) {
|
|
5240
|
-
if (this.debug) {
|
|
5241
|
-
console.log('[NightSessionWidget] API service not available for initial validation');
|
|
5242
|
-
}
|
|
5243
|
-
return;
|
|
5244
|
-
}
|
|
5245
|
-
const result = await apiService.quotel1(this.symbol);
|
|
5246
|
-
if (result && result.data && result.data[0]) {
|
|
5247
|
-
const symbolData = result.data[0];
|
|
5248
|
-
// Extract company info
|
|
5249
|
-
this.companyName = symbolData.comp_name || '';
|
|
5250
|
-
this.exchangeName = symbolData.market_name || '';
|
|
5251
|
-
this.mic = symbolData.mic || '';
|
|
5252
|
-
if (this.debug) {
|
|
5253
|
-
console.log('[NightSessionWidget] Initial symbol validated:', {
|
|
5254
|
-
symbol: this.symbol,
|
|
5255
|
-
companyName: this.companyName,
|
|
5256
|
-
exchangeName: this.exchangeName,
|
|
5257
|
-
mic: this.mic
|
|
5258
|
-
});
|
|
5259
|
-
}
|
|
5260
|
-
}
|
|
5261
|
-
} catch (error) {
|
|
5262
|
-
if (this.debug) {
|
|
5263
|
-
console.warn('[NightSessionWidget] Initial symbol validation failed:', error);
|
|
5264
|
-
}
|
|
5265
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
5202
|
subscribeToData() {
|
|
5269
5203
|
let subscriptionType;
|
|
5270
5204
|
if (this.source === 'bruce') {
|
|
@@ -5283,71 +5217,53 @@
|
|
|
5283
5217
|
// to avoid duplicate timeouts
|
|
5284
5218
|
}
|
|
5285
5219
|
handleData(message) {
|
|
5220
|
+
console.log('DEBUG NIGHT', message);
|
|
5221
|
+
|
|
5222
|
+
//message = message.data || message.Data
|
|
5223
|
+
|
|
5286
5224
|
if (this.loadingTimeout) {
|
|
5287
5225
|
clearTimeout(this.loadingTimeout);
|
|
5288
5226
|
this.loadingTimeout = null;
|
|
5289
5227
|
}
|
|
5290
5228
|
|
|
5291
|
-
//
|
|
5292
|
-
if (message.
|
|
5229
|
+
// Check for error: false and display message if present
|
|
5230
|
+
if (message.error === true) {
|
|
5293
5231
|
if (this.debug) {
|
|
5294
|
-
console.log('[
|
|
5232
|
+
console.log('[Nigh] Received error response:', message.message);
|
|
5295
5233
|
}
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
NotFound: true,
|
|
5301
|
-
message: message.message
|
|
5302
|
-
});
|
|
5303
|
-
} else {
|
|
5234
|
+
const errorMsg = message.message || message.Message;
|
|
5235
|
+
|
|
5236
|
+
// If there's a message field, show it as an error
|
|
5237
|
+
if (errorMsg) {
|
|
5304
5238
|
this.hideLoading();
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
}
|
|
5239
|
+
this.showError(errorMsg);
|
|
5240
|
+
return;
|
|
5308
5241
|
}
|
|
5242
|
+
} else if (message.Data && typeof message.Data === 'string') {
|
|
5243
|
+
this.hideLoading();
|
|
5244
|
+
this.showError(message.Message);
|
|
5309
5245
|
return;
|
|
5310
5246
|
}
|
|
5311
5247
|
|
|
5312
|
-
// Handle
|
|
5248
|
+
// Handle error messages from server (plain text converted to structured format)
|
|
5313
5249
|
if (message.type === 'error') {
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
this.showNoDataState({
|
|
5321
|
-
Symbol: this.symbol,
|
|
5322
|
-
NotFound: true,
|
|
5323
|
-
isAccessError: true,
|
|
5324
|
-
message: errorMsg
|
|
5325
|
-
});
|
|
5326
|
-
} else {
|
|
5327
|
-
this.hideLoading();
|
|
5328
|
-
if (this.debug) {
|
|
5329
|
-
console.log('[NightSessionWidget] Access error but keeping cached data');
|
|
5330
|
-
}
|
|
5331
|
-
}
|
|
5250
|
+
if (this.debug) {
|
|
5251
|
+
console.log('[NightSessionWidget] Received no data message:', message.message);
|
|
5252
|
+
}
|
|
5253
|
+
// Only show no data state if we don't have cached data
|
|
5254
|
+
if (!this.data) {
|
|
5255
|
+
this.showError(errorMsg);
|
|
5332
5256
|
} else {
|
|
5333
|
-
|
|
5334
|
-
if (
|
|
5335
|
-
console.log('
|
|
5336
|
-
this.showError(errorMsg);
|
|
5337
|
-
} else {
|
|
5338
|
-
this.hideLoading();
|
|
5339
|
-
if (this.debug) {
|
|
5340
|
-
console.log('[NightSessionWidget] Error received but keeping cached data:', errorMsg);
|
|
5341
|
-
}
|
|
5257
|
+
this.hideLoading();
|
|
5258
|
+
if (this.debug) {
|
|
5259
|
+
console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
|
|
5342
5260
|
}
|
|
5343
5261
|
}
|
|
5344
5262
|
return;
|
|
5345
5263
|
}
|
|
5346
|
-
|
|
5347
|
-
// Filter for night session data
|
|
5348
|
-
if (Array.isArray(message)) {
|
|
5264
|
+
if (Array.isArray(message.data)) {
|
|
5349
5265
|
// First, try to find data matching our symbol regardless of MarketName
|
|
5350
|
-
const symbolData = message.find(item => item.Symbol === this.symbol);
|
|
5266
|
+
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
5351
5267
|
if (symbolData) {
|
|
5352
5268
|
if (this.debug) {
|
|
5353
5269
|
console.log('[NightSessionWidget] Found data for symbol:', symbolData);
|
|
@@ -5387,34 +5303,35 @@
|
|
|
5387
5303
|
}
|
|
5388
5304
|
}
|
|
5389
5305
|
// Handle wrapped format
|
|
5390
|
-
else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1' || message.type === 'queryonbbol1') {
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5306
|
+
/* else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1' || message.type === 'queryonbbol1') {
|
|
5307
|
+
if (message.data['0']?.Symbol === this.symbol) {
|
|
5308
|
+
// For onbbo source, skip MarketName check
|
|
5309
|
+
const isOnbbo = message.type === 'queryonbbol1';
|
|
5310
|
+
const shouldShowNoData = message['0'].NotFound === true ||
|
|
5311
|
+
(!isOnbbo && (!message['0'].MarketName || message['0'].MarketName !== 'BLUE'));
|
|
5312
|
+
if (shouldShowNoData) {
|
|
5313
|
+
// Only show no data state if we don't have cached data
|
|
5314
|
+
if (!this.data) {
|
|
5315
|
+
this.showNoDataState(message['0']);
|
|
5316
|
+
} else {
|
|
5317
|
+
this.hideLoading();
|
|
5318
|
+
if (this.debug) {
|
|
5319
|
+
console.log('[NightSessionWidget] No new data, keeping cached data visible');
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
} else {
|
|
5323
|
+
const model = new NightSessionModel(message['0']);
|
|
5324
|
+
this.data = model; // Store for caching
|
|
5325
|
+
this.updateWidget(model);
|
|
5403
5326
|
}
|
|
5404
|
-
}
|
|
5405
5327
|
} else {
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
// No matching symbol - keep cached data if available
|
|
5412
|
-
if (this.debug) {
|
|
5413
|
-
console.log('[NightSessionWidget] No matching symbol in response, keeping cached data');
|
|
5328
|
+
// No matching symbol - keep cached data if available
|
|
5329
|
+
if (this.debug) {
|
|
5330
|
+
console.log('[NightSessionWidget] No matching symbol in response, keeping cached data');
|
|
5331
|
+
}
|
|
5332
|
+
this.hideLoading();
|
|
5414
5333
|
}
|
|
5415
|
-
|
|
5416
|
-
}
|
|
5417
|
-
}
|
|
5334
|
+
} */
|
|
5418
5335
|
if (message._cached) ;
|
|
5419
5336
|
}
|
|
5420
5337
|
updateWidget(data) {
|
|
@@ -6244,36 +6161,20 @@
|
|
|
6244
6161
|
this.showLoading();
|
|
6245
6162
|
|
|
6246
6163
|
// Validate initial symbol via API to get symbol info
|
|
6247
|
-
|
|
6248
|
-
this.
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
if (!apiService) {
|
|
6254
|
-
if (this.debug) {
|
|
6255
|
-
console.log('[OptionsWidget] API service not available for initial validation');
|
|
6256
|
-
}
|
|
6257
|
-
return;
|
|
6164
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting data
|
|
6165
|
+
const validationSuccess = await super.validateInitialSymbol('quoteOptionl1', this.symbol, data => {
|
|
6166
|
+
// Store for use in updateWidget
|
|
6167
|
+
// Note: quoteOptionl1 returns array directly, not wrapped in data field
|
|
6168
|
+
if (data && data[0] && !data[0].error && !data[0].not_found) {
|
|
6169
|
+
this.initialValidationData = data[0];
|
|
6258
6170
|
}
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
if (this.debug) {
|
|
6265
|
-
console.log('[OptionsWidget] Initial symbol validated:', {
|
|
6266
|
-
symbol: this.symbol,
|
|
6267
|
-
underlying: symbolData.Underlying || symbolData.RootSymbol
|
|
6268
|
-
});
|
|
6269
|
-
}
|
|
6270
|
-
}
|
|
6271
|
-
} catch (error) {
|
|
6272
|
-
if (this.debug) {
|
|
6273
|
-
console.warn('[OptionsWidget] Initial symbol validation failed:', error);
|
|
6274
|
-
}
|
|
6275
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
6171
|
+
});
|
|
6172
|
+
|
|
6173
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
6174
|
+
if (validationSuccess === false) {
|
|
6175
|
+
return; // Error is already shown, don't continue
|
|
6276
6176
|
}
|
|
6177
|
+
this.subscribeToData();
|
|
6277
6178
|
}
|
|
6278
6179
|
subscribeToData() {
|
|
6279
6180
|
// Subscribe with symbol for routing
|
|
@@ -38630,7 +38531,20 @@ ${SharedStyles}
|
|
|
38630
38531
|
}
|
|
38631
38532
|
async initialize() {
|
|
38632
38533
|
// Validate initial symbol via API to get company info
|
|
38633
|
-
|
|
38534
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
38535
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
38536
|
+
// Extract company info from first data item
|
|
38537
|
+
if (data && data[0]) {
|
|
38538
|
+
this.companyName = data[0].comp_name || '';
|
|
38539
|
+
this.exchangeName = data[0].market_name || '';
|
|
38540
|
+
this.mic = data[0].mic || '';
|
|
38541
|
+
}
|
|
38542
|
+
});
|
|
38543
|
+
|
|
38544
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
38545
|
+
if (validationSuccess === false) {
|
|
38546
|
+
return; // Error is already shown, don't continue
|
|
38547
|
+
}
|
|
38634
38548
|
|
|
38635
38549
|
// Update company name in the header
|
|
38636
38550
|
this.updateCompanyName();
|
|
@@ -38638,38 +38552,6 @@ ${SharedStyles}
|
|
|
38638
38552
|
// Load chart data
|
|
38639
38553
|
await this.loadChartData();
|
|
38640
38554
|
}
|
|
38641
|
-
async validateInitialSymbol() {
|
|
38642
|
-
try {
|
|
38643
|
-
const apiService = this.wsManager.getApiService();
|
|
38644
|
-
if (!apiService) {
|
|
38645
|
-
if (this.debug) {
|
|
38646
|
-
console.log('[IntradayChartWidget] API service not available for initial validation');
|
|
38647
|
-
}
|
|
38648
|
-
return;
|
|
38649
|
-
}
|
|
38650
|
-
const result = await apiService.quotel1(this.symbol);
|
|
38651
|
-
if (result && result.data && result.data[0]) {
|
|
38652
|
-
const symbolData = result.data[0];
|
|
38653
|
-
// Extract company info
|
|
38654
|
-
this.companyName = symbolData.comp_name || '';
|
|
38655
|
-
this.exchangeName = symbolData.market_name || '';
|
|
38656
|
-
this.mic = symbolData.mic || '';
|
|
38657
|
-
if (this.debug) {
|
|
38658
|
-
console.log('[IntradayChartWidget] Initial symbol validated:', {
|
|
38659
|
-
symbol: this.symbol,
|
|
38660
|
-
companyName: this.companyName,
|
|
38661
|
-
exchangeName: this.exchangeName,
|
|
38662
|
-
mic: this.mic
|
|
38663
|
-
});
|
|
38664
|
-
}
|
|
38665
|
-
}
|
|
38666
|
-
} catch (error) {
|
|
38667
|
-
if (this.debug) {
|
|
38668
|
-
console.warn('[IntradayChartWidget] Initial symbol validation failed:', error);
|
|
38669
|
-
}
|
|
38670
|
-
// Don't throw - let the widget continue with chart data
|
|
38671
|
-
}
|
|
38672
|
-
}
|
|
38673
38555
|
updateCompanyName() {
|
|
38674
38556
|
const companyNameElement = this.container.querySelector('.intraday-company-name');
|
|
38675
38557
|
if (companyNameElement) {
|
|
@@ -39091,21 +38973,14 @@ ${SharedStyles}
|
|
|
39091
38973
|
let priceData = null;
|
|
39092
38974
|
|
|
39093
38975
|
// Handle array format (standard night session format)
|
|
39094
|
-
if (Array.isArray(message)) {
|
|
38976
|
+
if (Array.isArray(message.data)) {
|
|
39095
38977
|
console.log('[IntradayChartWidget] Processing array format, length:', message.length);
|
|
39096
|
-
const symbolData = message.find(item => item.Symbol === this.symbol);
|
|
38978
|
+
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
39097
38979
|
console.log('[IntradayChartWidget] Found symbol data:', symbolData);
|
|
39098
38980
|
if (symbolData && !symbolData.NotFound) {
|
|
39099
38981
|
priceData = symbolData;
|
|
39100
38982
|
}
|
|
39101
38983
|
}
|
|
39102
|
-
// Handle wrapped format
|
|
39103
|
-
else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1') {
|
|
39104
|
-
console.log('[IntradayChartWidget] Processing wrapped format');
|
|
39105
|
-
if (message['0']?.Symbol === this.symbol && !message['0'].NotFound) {
|
|
39106
|
-
priceData = message['0'];
|
|
39107
|
-
}
|
|
39108
|
-
}
|
|
39109
38984
|
// Handle direct data format
|
|
39110
38985
|
else if (message.Symbol === this.symbol && !message.NotFound) {
|
|
39111
38986
|
console.log('[IntradayChartWidget] Processing direct format');
|
|
@@ -40062,8 +39937,10 @@ ${SharedStyles}
|
|
|
40062
39937
|
this.styled = options.styled !== undefined ? options.styled : true;
|
|
40063
39938
|
this.maxLevels = options.maxLevels || 10; // Number of levels to display
|
|
40064
39939
|
this.data = null;
|
|
39940
|
+
this.level1Data = null; // Store Level 1 data separately
|
|
40065
39941
|
this.isDestroyed = false;
|
|
40066
|
-
this.
|
|
39942
|
+
this.unsubscribeL2 = null;
|
|
39943
|
+
this.unsubscribeL1 = null;
|
|
40067
39944
|
this.symbolEditor = null;
|
|
40068
39945
|
this.loadingTimeout = null;
|
|
40069
39946
|
|
|
@@ -40164,13 +40041,22 @@ ${SharedStyles}
|
|
|
40164
40041
|
this.showError(`No data received for ${upperSymbol}. Please try again.`);
|
|
40165
40042
|
}, 10000);
|
|
40166
40043
|
|
|
40167
|
-
// Unsubscribe from old symbol
|
|
40168
|
-
if (this.
|
|
40044
|
+
// Unsubscribe from old symbol's L2 and L1 data
|
|
40045
|
+
if (this.unsubscribeL2) {
|
|
40169
40046
|
if (this.debug) {
|
|
40170
|
-
console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol}`);
|
|
40047
|
+
console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol} L2`);
|
|
40171
40048
|
}
|
|
40172
|
-
this.
|
|
40173
|
-
this.
|
|
40049
|
+
this.wsManager.sendUnsubscribe('queryonbbol2', this.symbol);
|
|
40050
|
+
this.unsubscribeL2();
|
|
40051
|
+
this.unsubscribeL2 = null;
|
|
40052
|
+
}
|
|
40053
|
+
if (this.unsubscribeL1) {
|
|
40054
|
+
if (this.debug) {
|
|
40055
|
+
console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol} L1`);
|
|
40056
|
+
}
|
|
40057
|
+
this.wsManager.sendUnsubscribe('queryonbbol1', this.symbol);
|
|
40058
|
+
this.unsubscribeL1();
|
|
40059
|
+
this.unsubscribeL1 = null;
|
|
40174
40060
|
}
|
|
40175
40061
|
|
|
40176
40062
|
// Update internal symbol
|
|
@@ -40197,7 +40083,19 @@ ${SharedStyles}
|
|
|
40197
40083
|
this.showLoading();
|
|
40198
40084
|
|
|
40199
40085
|
// Fetch company info for initial symbol
|
|
40200
|
-
|
|
40086
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
40087
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
40088
|
+
// Extract company info from first data item
|
|
40089
|
+
if (data && data[0]) {
|
|
40090
|
+
this.companyName = data[0].comp_name || '';
|
|
40091
|
+
this.exchangeName = data[0].market_name || '';
|
|
40092
|
+
}
|
|
40093
|
+
});
|
|
40094
|
+
|
|
40095
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
40096
|
+
if (validationSuccess === false) {
|
|
40097
|
+
return; // Error is already shown, don't continue
|
|
40098
|
+
}
|
|
40201
40099
|
|
|
40202
40100
|
// Set timeout to detect no data on initial load
|
|
40203
40101
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -40209,41 +40107,52 @@ ${SharedStyles}
|
|
|
40209
40107
|
}, 10000);
|
|
40210
40108
|
this.subscribeToData();
|
|
40211
40109
|
}
|
|
40212
|
-
|
|
40213
|
-
|
|
40214
|
-
|
|
40215
|
-
|
|
40216
|
-
|
|
40217
|
-
|
|
40218
|
-
|
|
40110
|
+
subscribeToData() {
|
|
40111
|
+
// Subscribe to ONBBO Level 2 data (order book)
|
|
40112
|
+
// Use separate widget IDs to avoid "already active" duplicate detection
|
|
40113
|
+
this.unsubscribeL2 = this.wsManager.subscribe(`${this.widgetId}-l2`, ['queryonbbol2'], messageWrapper => {
|
|
40114
|
+
const {
|
|
40115
|
+
event,
|
|
40116
|
+
data
|
|
40117
|
+
} = messageWrapper;
|
|
40118
|
+
|
|
40119
|
+
// Handle connection events
|
|
40120
|
+
if (event === 'connection') {
|
|
40121
|
+
this.handleConnectionStatus(data);
|
|
40219
40122
|
return;
|
|
40220
40123
|
}
|
|
40221
|
-
|
|
40222
|
-
|
|
40223
|
-
|
|
40224
|
-
|
|
40225
|
-
this.
|
|
40226
|
-
|
|
40227
|
-
|
|
40228
|
-
|
|
40229
|
-
symbol: this.symbol,
|
|
40230
|
-
companyName: this.companyName,
|
|
40231
|
-
exchangeName: this.exchangeName
|
|
40232
|
-
});
|
|
40233
|
-
}
|
|
40124
|
+
|
|
40125
|
+
// For data events, add type context
|
|
40126
|
+
if (event === 'data') {
|
|
40127
|
+
data._dataType = 'level2';
|
|
40128
|
+
this.handleMessage({
|
|
40129
|
+
event,
|
|
40130
|
+
data
|
|
40131
|
+
});
|
|
40234
40132
|
}
|
|
40235
|
-
}
|
|
40236
|
-
|
|
40237
|
-
|
|
40133
|
+
}, this.symbol);
|
|
40134
|
+
|
|
40135
|
+
// Subscribe to ONBBO Level 1 data (statistics: LastPx, LowPx, HighPx, Volume)
|
|
40136
|
+
this.unsubscribeL1 = this.wsManager.subscribe(`${this.widgetId}-l1`, ['queryonbbol1'], messageWrapper => {
|
|
40137
|
+
const {
|
|
40138
|
+
event,
|
|
40139
|
+
data
|
|
40140
|
+
} = messageWrapper;
|
|
40141
|
+
|
|
40142
|
+
// Connection already handled by L2 subscription
|
|
40143
|
+
if (event === 'connection') {
|
|
40144
|
+
return;
|
|
40238
40145
|
}
|
|
40239
|
-
|
|
40240
|
-
|
|
40241
|
-
|
|
40242
|
-
|
|
40243
|
-
|
|
40244
|
-
|
|
40245
|
-
|
|
40246
|
-
|
|
40146
|
+
|
|
40147
|
+
// For data events, add type context
|
|
40148
|
+
if (event === 'data') {
|
|
40149
|
+
data._dataType = 'level1';
|
|
40150
|
+
this.handleMessage({
|
|
40151
|
+
event,
|
|
40152
|
+
data
|
|
40153
|
+
});
|
|
40154
|
+
}
|
|
40155
|
+
}, this.symbol);
|
|
40247
40156
|
}
|
|
40248
40157
|
handleData(message) {
|
|
40249
40158
|
if (this.loadingTimeout) {
|
|
@@ -40251,8 +40160,14 @@ ${SharedStyles}
|
|
|
40251
40160
|
this.loadingTimeout = null;
|
|
40252
40161
|
}
|
|
40253
40162
|
|
|
40163
|
+
// Extract data type from metadata (added by subscription callback)
|
|
40164
|
+
const dataType = message._dataType;
|
|
40165
|
+
if (this.debug) {
|
|
40166
|
+
console.log(`[ONBBOLevel2Widget] handleData called with type: ${dataType}`, message);
|
|
40167
|
+
}
|
|
40168
|
+
|
|
40254
40169
|
// Handle error messages
|
|
40255
|
-
if (message.type === 'error') {
|
|
40170
|
+
if (message.type === 'error' || message.error == true) {
|
|
40256
40171
|
const errorMsg = message.message || 'Server error';
|
|
40257
40172
|
if (this.debug) {
|
|
40258
40173
|
console.log('[ONBBOLevel2Widget] Error:', errorMsg);
|
|
@@ -40261,16 +40176,54 @@ ${SharedStyles}
|
|
|
40261
40176
|
return;
|
|
40262
40177
|
}
|
|
40263
40178
|
|
|
40179
|
+
// Handle Level 1 data (statistics: LastPx, LowPx, HighPx, Volume)
|
|
40180
|
+
if (message.type === 'queryonbbol1') {
|
|
40181
|
+
if (this.debug) {
|
|
40182
|
+
console.log('[ONBBOLevel2Widget] Processing Level 1 data:', message);
|
|
40183
|
+
}
|
|
40184
|
+
|
|
40185
|
+
// Handle array format (new format with Data wrapper)
|
|
40186
|
+
if (message.data && Array.isArray(message.data)) {
|
|
40187
|
+
const l1Data = message.data.find(d => d.Symbol === this.symbol);
|
|
40188
|
+
if (l1Data) {
|
|
40189
|
+
this.level1Data = {
|
|
40190
|
+
lastPx: l1Data.LastPx || 0,
|
|
40191
|
+
lowPx: l1Data.LowPx || 0,
|
|
40192
|
+
highPx: l1Data.HighPx || 0,
|
|
40193
|
+
volume: l1Data.Volume || 0
|
|
40194
|
+
};
|
|
40195
|
+
this.updateLevel1Display();
|
|
40196
|
+
}
|
|
40197
|
+
}
|
|
40198
|
+
// Handle direct array format (standard format)
|
|
40199
|
+
else if (Array.isArray(message)) {
|
|
40200
|
+
const l1Data = message.find(d => d.Symbol === this.symbol);
|
|
40201
|
+
if (l1Data) {
|
|
40202
|
+
this.level1Data = {
|
|
40203
|
+
lastPx: l1Data.LastPx || 0,
|
|
40204
|
+
lowPx: l1Data.LowPx || 0,
|
|
40205
|
+
highPx: l1Data.HighPx || 0,
|
|
40206
|
+
volume: l1Data.Volume || 0
|
|
40207
|
+
};
|
|
40208
|
+
this.updateLevel1Display();
|
|
40209
|
+
}
|
|
40210
|
+
}
|
|
40211
|
+
|
|
40212
|
+
// Clean up metadata
|
|
40213
|
+
delete message._dataType;
|
|
40214
|
+
return;
|
|
40215
|
+
}
|
|
40216
|
+
|
|
40264
40217
|
// Handle Level 2 data - Array of MMID quotes
|
|
40265
|
-
if (Array.isArray(message)) {
|
|
40218
|
+
if (Array.isArray(message.data)) {
|
|
40266
40219
|
// Check if it's an array of MMID objects (ONBBO format)
|
|
40267
|
-
if (message.length > 0 && message[0].MMID) {
|
|
40220
|
+
if (message.data.length > 0 && message.data[0].MMID) {
|
|
40268
40221
|
if (this.debug) {
|
|
40269
40222
|
console.log('[ONBBOLevel2Widget] Received MMID array:', message);
|
|
40270
40223
|
}
|
|
40271
40224
|
|
|
40272
40225
|
// Create model from MMID array
|
|
40273
|
-
const model = new ONBBOLevel2Model(message);
|
|
40226
|
+
const model = new ONBBOLevel2Model(message.data);
|
|
40274
40227
|
model.symbol = this.symbol; // Set symbol from widget
|
|
40275
40228
|
this.data = model;
|
|
40276
40229
|
this.updateWidget(model);
|
|
@@ -40278,7 +40231,7 @@ ${SharedStyles}
|
|
|
40278
40231
|
}
|
|
40279
40232
|
|
|
40280
40233
|
// Try to find symbol in array (alternative format)
|
|
40281
|
-
const symbolData = message.find(item => item.Symbol === this.symbol);
|
|
40234
|
+
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
40282
40235
|
if (symbolData) {
|
|
40283
40236
|
if (symbolData.NotFound === true) {
|
|
40284
40237
|
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
@@ -40292,32 +40245,31 @@ ${SharedStyles}
|
|
|
40292
40245
|
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40293
40246
|
}
|
|
40294
40247
|
// Handle wrapped format
|
|
40295
|
-
else if (message.type === 'queryonbbol2') {
|
|
40296
|
-
|
|
40297
|
-
|
|
40298
|
-
|
|
40299
|
-
|
|
40248
|
+
/* else if (message.type === 'queryonbbol2') {
|
|
40249
|
+
// Check if wrapped data contains MMID array
|
|
40250
|
+
if (message['0'] && Array.isArray(message['0'])) {
|
|
40251
|
+
if (this.debug) {
|
|
40252
|
+
console.log('[ONBBOLevel2Widget] Received wrapped MMID array:', message['0']);
|
|
40253
|
+
}
|
|
40254
|
+
const model = new ONBBOLevel2Model(message['0']);
|
|
40255
|
+
model.symbol = this.symbol;
|
|
40256
|
+
this.data = model;
|
|
40257
|
+
this.updateWidget(model);
|
|
40258
|
+
return;
|
|
40300
40259
|
}
|
|
40301
|
-
|
|
40302
|
-
|
|
40303
|
-
|
|
40304
|
-
|
|
40305
|
-
|
|
40306
|
-
|
|
40307
|
-
|
|
40308
|
-
|
|
40309
|
-
|
|
40310
|
-
if (message['0'].NotFound === true) {
|
|
40311
|
-
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40260
|
+
// Check for symbol match in wrapped format
|
|
40261
|
+
if (message['0']?.Symbol === this.symbol) {
|
|
40262
|
+
if (message['0'].NotFound === true) {
|
|
40263
|
+
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40264
|
+
} else {
|
|
40265
|
+
const model = new ONBBOLevel2Model(message['0']);
|
|
40266
|
+
this.data = model;
|
|
40267
|
+
this.updateWidget(model);
|
|
40268
|
+
}
|
|
40312
40269
|
} else {
|
|
40313
|
-
|
|
40314
|
-
this.data = model;
|
|
40315
|
-
this.updateWidget(model);
|
|
40270
|
+
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40316
40271
|
}
|
|
40317
|
-
|
|
40318
|
-
this.showError(`No Level 2 data available for ${this.symbol}`);
|
|
40319
|
-
}
|
|
40320
|
-
}
|
|
40272
|
+
} */
|
|
40321
40273
|
}
|
|
40322
40274
|
updateWidget(data) {
|
|
40323
40275
|
if (this.isDestroyed) return;
|
|
@@ -40415,6 +40367,43 @@ ${SharedStyles}
|
|
|
40415
40367
|
askBody.appendChild(row);
|
|
40416
40368
|
});
|
|
40417
40369
|
}
|
|
40370
|
+
updateLevel1Display() {
|
|
40371
|
+
if (!this.level1Data) return;
|
|
40372
|
+
const formatPrice = price => {
|
|
40373
|
+
return price ? price.toFixed(2) : '--';
|
|
40374
|
+
};
|
|
40375
|
+
const formatVolume = volume => {
|
|
40376
|
+
if (!volume) return '--';
|
|
40377
|
+
return volume.toLocaleString();
|
|
40378
|
+
};
|
|
40379
|
+
|
|
40380
|
+
// Update Last Price
|
|
40381
|
+
const lastPxElement = this.container.querySelector('.l1-last-px');
|
|
40382
|
+
if (lastPxElement) {
|
|
40383
|
+
lastPxElement.textContent = formatPrice(this.level1Data.lastPx);
|
|
40384
|
+
}
|
|
40385
|
+
|
|
40386
|
+
// Update Low Price
|
|
40387
|
+
const lowPxElement = this.container.querySelector('.l1-low-px');
|
|
40388
|
+
if (lowPxElement) {
|
|
40389
|
+
lowPxElement.textContent = formatPrice(this.level1Data.lowPx);
|
|
40390
|
+
}
|
|
40391
|
+
|
|
40392
|
+
// Update High Price
|
|
40393
|
+
const highPxElement = this.container.querySelector('.l1-high-px');
|
|
40394
|
+
if (highPxElement) {
|
|
40395
|
+
highPxElement.textContent = formatPrice(this.level1Data.highPx);
|
|
40396
|
+
}
|
|
40397
|
+
|
|
40398
|
+
// Update Volume
|
|
40399
|
+
const volumeElement = this.container.querySelector('.l1-volume');
|
|
40400
|
+
if (volumeElement) {
|
|
40401
|
+
volumeElement.textContent = formatVolume(this.level1Data.volume);
|
|
40402
|
+
}
|
|
40403
|
+
if (this.debug) {
|
|
40404
|
+
console.log('[ONBBOLevel2Widget] Updated Level 1 display:', this.level1Data);
|
|
40405
|
+
}
|
|
40406
|
+
}
|
|
40418
40407
|
showLoading() {
|
|
40419
40408
|
const loadingOverlay = this.container.querySelector('.widget-loading-overlay');
|
|
40420
40409
|
if (loadingOverlay) {
|
|
@@ -40461,9 +40450,17 @@ ${SharedStyles}
|
|
|
40461
40450
|
this.clearTimeout(this.loadingTimeout);
|
|
40462
40451
|
this.loadingTimeout = null;
|
|
40463
40452
|
}
|
|
40464
|
-
|
|
40465
|
-
|
|
40466
|
-
|
|
40453
|
+
|
|
40454
|
+
// Unsubscribe from L2 data
|
|
40455
|
+
if (this.unsubscribeL2) {
|
|
40456
|
+
this.unsubscribeL2();
|
|
40457
|
+
this.unsubscribeL2 = null;
|
|
40458
|
+
}
|
|
40459
|
+
|
|
40460
|
+
// Unsubscribe from L1 data
|
|
40461
|
+
if (this.unsubscribeL1) {
|
|
40462
|
+
this.unsubscribeL1();
|
|
40463
|
+
this.unsubscribeL1 = null;
|
|
40467
40464
|
}
|
|
40468
40465
|
|
|
40469
40466
|
// Destroy the symbol editor
|
|
@@ -40628,6 +40625,7 @@ ${SharedStyles}
|
|
|
40628
40625
|
this.styled = options.styled !== undefined ? options.styled : true;
|
|
40629
40626
|
this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
|
|
40630
40627
|
this.data = new TimeSalesModel([], this.maxTrades);
|
|
40628
|
+
this.data.symbol = this.symbol; // Set the symbol in the data model
|
|
40631
40629
|
this.isDestroyed = false;
|
|
40632
40630
|
this.unsubscribe = null;
|
|
40633
40631
|
this.symbolEditor = null;
|
|
@@ -40639,6 +40637,7 @@ ${SharedStyles}
|
|
|
40639
40637
|
|
|
40640
40638
|
// Create widget structure
|
|
40641
40639
|
this.createWidgetStructure();
|
|
40640
|
+
this.initializeSymbolEditor();
|
|
40642
40641
|
|
|
40643
40642
|
// Initialize the widget
|
|
40644
40643
|
this.initialize();
|
|
@@ -40655,7 +40654,26 @@ ${SharedStyles}
|
|
|
40655
40654
|
}
|
|
40656
40655
|
}
|
|
40657
40656
|
this.addStyles();
|
|
40658
|
-
this.setupSymbolEditor();
|
|
40657
|
+
//this.setupSymbolEditor();
|
|
40658
|
+
}
|
|
40659
|
+
initializeSymbolEditor() {
|
|
40660
|
+
// Initialize symbol editor with stock symbol validation
|
|
40661
|
+
this.symbolEditor = new SymbolEditor(this, {
|
|
40662
|
+
maxLength: 10,
|
|
40663
|
+
placeholder: 'Enter symbol...',
|
|
40664
|
+
//validator: this.validateStockSymbol.bind(this),
|
|
40665
|
+
onSymbolChange: this.handleSymbolChange.bind(this),
|
|
40666
|
+
debug: this.debug,
|
|
40667
|
+
autoUppercase: true,
|
|
40668
|
+
symbolType: 'quotel1'
|
|
40669
|
+
});
|
|
40670
|
+
|
|
40671
|
+
// Set initial symbol
|
|
40672
|
+
const symbolElement = this.container.querySelector('.symbol');
|
|
40673
|
+
if (symbolElement) {
|
|
40674
|
+
symbolElement.textContent = this.symbol;
|
|
40675
|
+
symbolElement.dataset.originalSymbol = this.symbol;
|
|
40676
|
+
}
|
|
40659
40677
|
}
|
|
40660
40678
|
addStyles() {
|
|
40661
40679
|
// Inject styles from the styles file into document head
|
|
@@ -40678,6 +40696,8 @@ ${SharedStyles}
|
|
|
40678
40696
|
// Keep editor open when clicking buttons
|
|
40679
40697
|
symbolType: 'quotel1' // Use standard quote validation
|
|
40680
40698
|
});
|
|
40699
|
+
|
|
40700
|
+
//console.log('SETUP COMPLETE')
|
|
40681
40701
|
}
|
|
40682
40702
|
async handleSymbolChange(newSymbol, oldSymbol) {
|
|
40683
40703
|
let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
@@ -40780,7 +40800,19 @@ ${SharedStyles}
|
|
|
40780
40800
|
this.showLoading();
|
|
40781
40801
|
|
|
40782
40802
|
// Validate initial symbol via API to get company info
|
|
40783
|
-
|
|
40803
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
40804
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
40805
|
+
// Extract company info from first data item
|
|
40806
|
+
if (data && data[0]) {
|
|
40807
|
+
this.companyName = data[0].comp_name || '';
|
|
40808
|
+
this.exchangeName = data[0].market_name || '';
|
|
40809
|
+
}
|
|
40810
|
+
});
|
|
40811
|
+
|
|
40812
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
40813
|
+
if (validationSuccess === false) {
|
|
40814
|
+
return; // Error is already shown, don't continue
|
|
40815
|
+
}
|
|
40784
40816
|
|
|
40785
40817
|
// Set timeout to detect no data on initial load
|
|
40786
40818
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -40793,36 +40825,6 @@ ${SharedStyles}
|
|
|
40793
40825
|
|
|
40794
40826
|
this.subscribeToData();
|
|
40795
40827
|
}
|
|
40796
|
-
async validateInitialSymbol() {
|
|
40797
|
-
try {
|
|
40798
|
-
const apiService = this.wsManager.getApiService();
|
|
40799
|
-
if (!apiService) {
|
|
40800
|
-
if (this.debug) {
|
|
40801
|
-
console.log('[TimeSalesWidget] API service not available for initial validation');
|
|
40802
|
-
}
|
|
40803
|
-
return;
|
|
40804
|
-
}
|
|
40805
|
-
const result = await apiService.quotel1(this.symbol);
|
|
40806
|
-
if (result && result.data && result.data[0]) {
|
|
40807
|
-
const symbolData = result.data[0];
|
|
40808
|
-
// Extract company info
|
|
40809
|
-
this.companyName = symbolData.comp_name || '';
|
|
40810
|
-
this.exchangeName = symbolData.market_name || '';
|
|
40811
|
-
if (this.debug) {
|
|
40812
|
-
console.log('[TimeSalesWidget] Initial symbol validated:', {
|
|
40813
|
-
symbol: this.symbol,
|
|
40814
|
-
companyName: this.companyName,
|
|
40815
|
-
exchangeName: this.exchangeName
|
|
40816
|
-
});
|
|
40817
|
-
}
|
|
40818
|
-
}
|
|
40819
|
-
} catch (error) {
|
|
40820
|
-
if (this.debug) {
|
|
40821
|
-
console.warn('[TimeSalesWidget] Initial symbol validation failed:', error);
|
|
40822
|
-
}
|
|
40823
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
40824
|
-
}
|
|
40825
|
-
}
|
|
40826
40828
|
subscribeToData() {
|
|
40827
40829
|
// Subscribe to Time & Sales data (using queryonbbotimesale)
|
|
40828
40830
|
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
|
|
@@ -40838,6 +40840,9 @@ ${SharedStyles}
|
|
|
40838
40840
|
clearTimeout(this.loadingTimeout);
|
|
40839
40841
|
this.loadingTimeout = null;
|
|
40840
40842
|
}
|
|
40843
|
+
if (message.type != 'queryonbbotimesale') {
|
|
40844
|
+
return;
|
|
40845
|
+
}
|
|
40841
40846
|
|
|
40842
40847
|
// Handle error messages
|
|
40843
40848
|
if (message.type === 'error') {
|
|
@@ -40850,30 +40855,16 @@ ${SharedStyles}
|
|
|
40850
40855
|
}
|
|
40851
40856
|
|
|
40852
40857
|
// Handle Time & Sales data - Array of trades
|
|
40853
|
-
if (Array.isArray(message)) {
|
|
40858
|
+
if (Array.isArray(message.data)) {
|
|
40854
40859
|
if (this.debug) {
|
|
40855
40860
|
console.log('[TimeSalesWidget] Received trades array:', message);
|
|
40856
40861
|
}
|
|
40857
40862
|
|
|
40858
40863
|
// Add new trades to existing data
|
|
40859
|
-
this.data.addTrades(message);
|
|
40864
|
+
this.data.addTrades(message.data);
|
|
40860
40865
|
this.updateWidget();
|
|
40861
40866
|
return;
|
|
40862
40867
|
}
|
|
40863
|
-
|
|
40864
|
-
// Handle wrapped format
|
|
40865
|
-
if ((message.type === 'queryonbbotimesale' || message.type === 'querytns') && message['0']) {
|
|
40866
|
-
if (Array.isArray(message['0'])) {
|
|
40867
|
-
if (this.debug) {
|
|
40868
|
-
console.log('[TimeSalesWidget] Received wrapped trades array:', message['0']);
|
|
40869
|
-
}
|
|
40870
|
-
|
|
40871
|
-
// Add new trades to existing data
|
|
40872
|
-
this.data.addTrades(message['0']);
|
|
40873
|
-
this.updateWidget();
|
|
40874
|
-
return;
|
|
40875
|
-
}
|
|
40876
|
-
}
|
|
40877
40868
|
if (this.debug) {
|
|
40878
40869
|
console.log('[TimeSalesWidget] Unexpected message format:', message);
|
|
40879
40870
|
}
|
|
@@ -41100,7 +41091,15 @@ ${SharedStyles}
|
|
|
41100
41091
|
try {
|
|
41101
41092
|
const response = await fetch(url, config);
|
|
41102
41093
|
if (!response.ok) {
|
|
41103
|
-
|
|
41094
|
+
// Try to get response body for error details
|
|
41095
|
+
let errorBody = '';
|
|
41096
|
+
try {
|
|
41097
|
+
errorBody = await response.text();
|
|
41098
|
+
console.error(`[ApiService] HTTP ${response.status} - Response body:`, errorBody);
|
|
41099
|
+
} catch (e) {
|
|
41100
|
+
console.error(`[ApiService] Could not read error response body:`, e);
|
|
41101
|
+
}
|
|
41102
|
+
throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
|
|
41104
41103
|
}
|
|
41105
41104
|
return await response.json();
|
|
41106
41105
|
} catch (error) {
|
|
@@ -41589,6 +41588,7 @@ ${SharedStyles}
|
|
|
41589
41588
|
}
|
|
41590
41589
|
}
|
|
41591
41590
|
handleMessage(event) {
|
|
41591
|
+
//console.log('EVENT', event)
|
|
41592
41592
|
try {
|
|
41593
41593
|
// Update activity tracking - connection is alive!
|
|
41594
41594
|
this.lastMessageReceived = Date.now();
|
|
@@ -41613,22 +41613,20 @@ ${SharedStyles}
|
|
|
41613
41613
|
return;
|
|
41614
41614
|
}
|
|
41615
41615
|
|
|
41616
|
-
// Determine
|
|
41617
|
-
const
|
|
41616
|
+
// Determine type based on error content, default to 'error'
|
|
41617
|
+
const errorType = this._getTargetTypeFromErrorMessage(textMessage) || 'error';
|
|
41618
41618
|
if (textMessage.toLowerCase().includes('no night session') || textMessage.toLowerCase().includes('no data')) {
|
|
41619
41619
|
message = {
|
|
41620
|
-
type:
|
|
41620
|
+
type: errorType,
|
|
41621
41621
|
message: textMessage,
|
|
41622
|
-
error:
|
|
41623
|
-
noData: true
|
|
41624
|
-
targetType: targetType // Will be 'querynightsession' for night session errors
|
|
41622
|
+
error: true,
|
|
41623
|
+
noData: true
|
|
41625
41624
|
};
|
|
41626
41625
|
} else {
|
|
41627
41626
|
message = {
|
|
41628
|
-
type:
|
|
41627
|
+
type: errorType,
|
|
41629
41628
|
message: textMessage,
|
|
41630
|
-
error:
|
|
41631
|
-
targetType: targetType // Could be null for general errors
|
|
41629
|
+
error: true
|
|
41632
41630
|
};
|
|
41633
41631
|
}
|
|
41634
41632
|
}
|
|
@@ -41636,9 +41634,16 @@ ${SharedStyles}
|
|
|
41636
41634
|
console.log('[WebSocketManager] Processed message:', message);
|
|
41637
41635
|
}
|
|
41638
41636
|
|
|
41639
|
-
//
|
|
41640
|
-
|
|
41641
|
-
|
|
41637
|
+
// Extract data field for night session messages to avoid nested structure
|
|
41638
|
+
// Night session format: { type: 'queryonbbol1', error: false, data: [...] }
|
|
41639
|
+
// We want to send just the data array to widgets
|
|
41640
|
+
let dataToRoute = message;
|
|
41641
|
+
/* if (message.data !== undefined && message.type !== undefined) {
|
|
41642
|
+
// This is night session format - extract the data
|
|
41643
|
+
dataToRoute = message.data;
|
|
41644
|
+
} */
|
|
41645
|
+
|
|
41646
|
+
this._routeMessage(dataToRoute);
|
|
41642
41647
|
} catch (error) {
|
|
41643
41648
|
console.error('[WebSocketManager] Error handling message:', error);
|
|
41644
41649
|
this._notifyWidgets('error', {
|
|
@@ -41777,7 +41782,31 @@ ${SharedStyles}
|
|
|
41777
41782
|
}
|
|
41778
41783
|
} else {
|
|
41779
41784
|
if (this.config.debug) {
|
|
41780
|
-
console.log(`[WebSocketManager] Subscription ${subscriptionKey} already active, skipping`);
|
|
41785
|
+
console.log(`[WebSocketManager] Subscription ${subscriptionKey} already active, skipping server subscription`);
|
|
41786
|
+
}
|
|
41787
|
+
|
|
41788
|
+
// Subscription already active, but send cached data to newly subscribing widgets
|
|
41789
|
+
const cachedMessage = this.lastMessageCache.get(subscriptionKey);
|
|
41790
|
+
console.log('LAST', this.lastMessageCache);
|
|
41791
|
+
if (cachedMessage) {
|
|
41792
|
+
if (this.config.debug) {
|
|
41793
|
+
console.log(`[WebSocketManager] Sending cached data to newly subscribing widgets for ${subscriptionKey}`);
|
|
41794
|
+
}
|
|
41795
|
+
|
|
41796
|
+
// Find all widgets subscribed to this type:symbol combination
|
|
41797
|
+
this.subscriptions.forEach((subscription, widgetId) => {
|
|
41798
|
+
if (subscription.types.has(type) && subscription.symbol === symbol) {
|
|
41799
|
+
try {
|
|
41800
|
+
subscription.callback({
|
|
41801
|
+
event: 'data',
|
|
41802
|
+
data: cachedMessage,
|
|
41803
|
+
widgetId
|
|
41804
|
+
});
|
|
41805
|
+
} catch (error) {
|
|
41806
|
+
console.error(`[WebSocketManager] Error sending cached data to widget ${widgetId}:`, error);
|
|
41807
|
+
}
|
|
41808
|
+
}
|
|
41809
|
+
});
|
|
41781
41810
|
}
|
|
41782
41811
|
}
|
|
41783
41812
|
});
|
|
@@ -41829,14 +41858,9 @@ ${SharedStyles}
|
|
|
41829
41858
|
* The data field structure varies based on the type
|
|
41830
41859
|
*/
|
|
41831
41860
|
_normalizeMessage(message) {
|
|
41832
|
-
// If message already has targetType (error messages), return as is
|
|
41833
|
-
if (message.targetType) {
|
|
41834
|
-
return message;
|
|
41835
|
-
}
|
|
41836
|
-
|
|
41837
41861
|
// Check for error field (case-insensitive)
|
|
41838
|
-
const errorField = message.error
|
|
41839
|
-
const typeField = message.type
|
|
41862
|
+
const errorField = message.error;
|
|
41863
|
+
const typeField = message.type;
|
|
41840
41864
|
const messageField = message.message || message.Message;
|
|
41841
41865
|
|
|
41842
41866
|
// Handle explicit error messages - route based on type field
|
|
@@ -41844,13 +41868,12 @@ ${SharedStyles}
|
|
|
41844
41868
|
if (this.config.debug) {
|
|
41845
41869
|
console.log(`[WebSocketManager] Detected error message with type: ${typeField}`);
|
|
41846
41870
|
}
|
|
41847
|
-
|
|
41848
|
-
// Set targetType for routing to specific widget types
|
|
41849
41871
|
return {
|
|
41850
41872
|
...message,
|
|
41851
|
-
|
|
41852
|
-
type
|
|
41853
|
-
|
|
41873
|
+
// Only add type if message doesn't already have one
|
|
41874
|
+
...(!message.type && !message.Type && typeField ? {
|
|
41875
|
+
type: typeField
|
|
41876
|
+
} : {})
|
|
41854
41877
|
};
|
|
41855
41878
|
}
|
|
41856
41879
|
|
|
@@ -41868,92 +41891,30 @@ ${SharedStyles}
|
|
|
41868
41891
|
// Treat as error and route based on type
|
|
41869
41892
|
return {
|
|
41870
41893
|
...message,
|
|
41871
|
-
|
|
41872
|
-
type
|
|
41873
|
-
|
|
41874
|
-
|
|
41894
|
+
// Only add type if message doesn't already have one
|
|
41895
|
+
...(!message.type && !message.Type && typeField ? {
|
|
41896
|
+
type: typeField
|
|
41897
|
+
} : {}),
|
|
41898
|
+
error: true // Mark as error since it was detected as implicit error
|
|
41875
41899
|
};
|
|
41876
41900
|
}
|
|
41877
41901
|
}
|
|
41878
41902
|
|
|
41879
|
-
// Check for new format: has 'type'/'Type' AND 'data'/'Data' fields
|
|
41880
|
-
const dataField = message.data || message.Data;
|
|
41881
|
-
if (typeField && dataField !== undefined) {
|
|
41882
|
-
if (this.config.debug) {
|
|
41883
|
-
console.log(`[WebSocketManager] Detected new message format with type: ${typeField}`);
|
|
41884
|
-
}
|
|
41885
|
-
|
|
41886
|
-
// Extract the actual data and add the type to it for routing
|
|
41887
|
-
let normalizedData;
|
|
41888
|
-
if (Array.isArray(dataField)) {
|
|
41889
|
-
// If data is an array, process each item
|
|
41890
|
-
normalizedData = dataField;
|
|
41891
|
-
// Add type info to the structure for routing
|
|
41892
|
-
if (normalizedData.length > 0) {
|
|
41893
|
-
normalizedData._messageType = typeField;
|
|
41894
|
-
}
|
|
41895
|
-
} else if (typeof dataField === 'object' && dataField !== null) {
|
|
41896
|
-
// If data is an object, use it directly
|
|
41897
|
-
normalizedData = dataField;
|
|
41898
|
-
// Add type info for routing
|
|
41899
|
-
normalizedData._messageType = typeField;
|
|
41900
|
-
} else {
|
|
41901
|
-
// Primitive value, wrap it
|
|
41902
|
-
normalizedData = {
|
|
41903
|
-
value: dataField,
|
|
41904
|
-
_messageType: typeField
|
|
41905
|
-
};
|
|
41906
|
-
}
|
|
41907
|
-
|
|
41908
|
-
// Also preserve the original type at the root level for backward compatibility
|
|
41909
|
-
if (typeof normalizedData === 'object' && !normalizedData.type) {
|
|
41910
|
-
normalizedData.type = typeField;
|
|
41911
|
-
}
|
|
41912
|
-
return normalizedData;
|
|
41913
|
-
}
|
|
41914
|
-
|
|
41915
41903
|
// Old format or no type/data structure, return as is
|
|
41916
41904
|
return message;
|
|
41917
41905
|
}
|
|
41918
41906
|
_routeMessage(message) {
|
|
41919
41907
|
//console.log('message', message);
|
|
41920
41908
|
|
|
41921
|
-
|
|
41922
|
-
|
|
41923
|
-
|
|
41924
|
-
|
|
41925
|
-
return; // Don't route empty arrays
|
|
41926
|
-
}
|
|
41909
|
+
// IMPORTANT: Don't filter out empty arrays here - route them based on message.type
|
|
41910
|
+
// Widgets need to receive empty arrays to clear loading states and show "no data" messages
|
|
41911
|
+
// The message structure is: { type: 'queryoptionchain', data: [] }
|
|
41912
|
+
// We need to route based on the 'type' field, not the data content
|
|
41927
41913
|
|
|
41928
41914
|
// Cache the message for later use by new subscribers
|
|
41929
41915
|
this._cacheMessage(message);
|
|
41930
41916
|
|
|
41931
|
-
//
|
|
41932
|
-
if (message.targetType) {
|
|
41933
|
-
const targetWidgets = this.typeSubscriptions.get(message.targetType);
|
|
41934
|
-
if (targetWidgets && targetWidgets.size > 0) {
|
|
41935
|
-
if (this.config.debug) {
|
|
41936
|
-
console.log(`[WebSocketManager] Routing ${message.targetType} error to specific widgets:`, [...targetWidgets]);
|
|
41937
|
-
}
|
|
41938
|
-
targetWidgets.forEach(widgetId => {
|
|
41939
|
-
const subscription = this.subscriptions.get(widgetId);
|
|
41940
|
-
if (subscription) {
|
|
41941
|
-
try {
|
|
41942
|
-
subscription.callback({
|
|
41943
|
-
event: 'data',
|
|
41944
|
-
data: message,
|
|
41945
|
-
widgetId
|
|
41946
|
-
});
|
|
41947
|
-
} catch (error) {
|
|
41948
|
-
console.error(`[WebSocketManager] Error in widget ${widgetId} callback:`, error);
|
|
41949
|
-
}
|
|
41950
|
-
}
|
|
41951
|
-
});
|
|
41952
|
-
return; // Don't fall through to broadcast
|
|
41953
|
-
}
|
|
41954
|
-
}
|
|
41955
|
-
|
|
41956
|
-
//console.log('here');
|
|
41917
|
+
// Get relevant widgets based on message type and symbol
|
|
41957
41918
|
const relevantWidgets = this._getRelevantWidgets(message);
|
|
41958
41919
|
if (this.config.debug && relevantWidgets.size > 0) {
|
|
41959
41920
|
console.log(`[WebSocketManager] Routing message to ${relevantWidgets.size} relevant widgets`);
|
|
@@ -41996,42 +41957,49 @@ ${SharedStyles}
|
|
|
41996
41957
|
_cacheMessage(message) {
|
|
41997
41958
|
try {
|
|
41998
41959
|
// Extract symbol and message type to create cache key
|
|
41999
|
-
|
|
42000
|
-
|
|
41960
|
+
// This allows new widgets to get instant data when subscribing
|
|
41961
|
+
// check in message.data or message.Data
|
|
41962
|
+
let data;
|
|
41963
|
+
if (message.data) {
|
|
41964
|
+
data = message.data;
|
|
41965
|
+
} else if (message.Data) {
|
|
41966
|
+
data = message.Data;
|
|
41967
|
+
} else {
|
|
41968
|
+
data = message;
|
|
41969
|
+
}
|
|
41970
|
+
const symbol = this._extractSymbol(data);
|
|
41971
|
+
const messageType = message.type;
|
|
41972
|
+
if (this.config.debug) {
|
|
41973
|
+
console.log(`[WebSocketManager] _cacheMessage - extracted symbol: "${symbol}", messageType: "${messageType}"`);
|
|
41974
|
+
}
|
|
41975
|
+
|
|
41976
|
+
// Cache by type:symbol combination
|
|
42001
41977
|
if (messageType && symbol) {
|
|
42002
41978
|
const cacheKey = `${messageType}:${symbol}`;
|
|
42003
41979
|
this.lastMessageCache.set(cacheKey, message);
|
|
42004
41980
|
if (this.config.debug) {
|
|
42005
41981
|
console.log(`[WebSocketManager] Cached message for ${cacheKey}`);
|
|
42006
41982
|
}
|
|
42007
|
-
}
|
|
42008
|
-
|
|
42009
|
-
|
|
42010
|
-
|
|
42011
|
-
const firstItem = message[0];
|
|
42012
|
-
const symbol = firstItem.Symbol;
|
|
42013
|
-
const messageType = this._extractMessageType(message);
|
|
42014
|
-
if (messageType && symbol) {
|
|
42015
|
-
const cacheKey = `${messageType}:${symbol}`;
|
|
42016
|
-
this.lastMessageCache.set(cacheKey, message);
|
|
41983
|
+
} else {
|
|
41984
|
+
// If we can't extract symbol from message, try to cache by type only
|
|
41985
|
+
// This helps with data like ONBBO L2 which doesn't have symbol in the message
|
|
41986
|
+
if (messageType) {
|
|
42017
41987
|
if (this.config.debug) {
|
|
42018
|
-
console.log(`[WebSocketManager]
|
|
41988
|
+
console.log(`[WebSocketManager] No symbol found in message, attempting to infer from subscriptions for type: ${messageType}`);
|
|
42019
41989
|
}
|
|
42020
|
-
}
|
|
42021
|
-
}
|
|
42022
41990
|
|
|
42023
|
-
|
|
42024
|
-
|
|
42025
|
-
|
|
42026
|
-
|
|
42027
|
-
|
|
42028
|
-
|
|
42029
|
-
|
|
42030
|
-
|
|
42031
|
-
|
|
41991
|
+
// Find active subscriptions for this message type and cache for each
|
|
41992
|
+
this.activeSubscriptions.forEach(activeKey => {
|
|
41993
|
+
const [type, sym] = activeKey.split(':');
|
|
41994
|
+
if (type === messageType && sym) {
|
|
41995
|
+
const cacheKey = activeKey;
|
|
41996
|
+
this.lastMessageCache.set(cacheKey, message);
|
|
41997
|
+
if (this.config.debug) {
|
|
41998
|
+
console.log(`[WebSocketManager] Cached message for ${cacheKey} (inferred from active subscription)`);
|
|
41999
|
+
}
|
|
42032
42000
|
}
|
|
42033
|
-
}
|
|
42034
|
-
}
|
|
42001
|
+
});
|
|
42002
|
+
}
|
|
42035
42003
|
}
|
|
42036
42004
|
} catch (error) {
|
|
42037
42005
|
console.error('[WebSocketManager] Error caching message:', error);
|
|
@@ -42040,22 +42008,28 @@ ${SharedStyles}
|
|
|
42040
42008
|
_getRelevantWidgets(message) {
|
|
42041
42009
|
const relevantWidgets = new Set();
|
|
42042
42010
|
|
|
42011
|
+
// OPTIMIZATION: Extract message type once from the entire message, not from each data item
|
|
42012
|
+
// Message structure: { type: 'queryoptionchain', data: [] }
|
|
42013
|
+
let dataField = message.Data || message.data;
|
|
42014
|
+
const messageType = message.type || this._extractMessageType(dataField);
|
|
42015
|
+
|
|
42043
42016
|
// Handle array messages
|
|
42044
|
-
this._addRelevantWidgetsForItem(message, relevantWidgets);
|
|
42017
|
+
this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
|
|
42045
42018
|
return relevantWidgets;
|
|
42046
42019
|
}
|
|
42047
|
-
_addRelevantWidgetsForItem(item, relevantWidgets) {
|
|
42020
|
+
_addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
|
|
42048
42021
|
// If item has Data array, process it
|
|
42049
42022
|
if (item.Data && Array.isArray(item.Data)) {
|
|
42050
42023
|
//console.log('Data array found with length:', item.Data.length);
|
|
42051
42024
|
item.Data.forEach(dataItem => {
|
|
42052
|
-
|
|
42025
|
+
// Pass messageType from parent message, don't extract from each data item
|
|
42026
|
+
this._processDataItem(dataItem, relevantWidgets, messageType);
|
|
42053
42027
|
});
|
|
42054
42028
|
} else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
|
|
42055
42029
|
this._processOptionChainData(item, relevantWidgets);
|
|
42056
42030
|
} else {
|
|
42057
|
-
// Process single item
|
|
42058
|
-
this._processDataItem(item, relevantWidgets);
|
|
42031
|
+
// Process single item - messageType already extracted from message
|
|
42032
|
+
this._processDataItem(item, relevantWidgets, messageType);
|
|
42059
42033
|
}
|
|
42060
42034
|
}
|
|
42061
42035
|
|
|
@@ -42099,12 +42073,11 @@ ${SharedStyles}
|
|
|
42099
42073
|
}
|
|
42100
42074
|
}
|
|
42101
42075
|
}
|
|
42102
|
-
_processDataItem(dataItem, relevantWidgets) {
|
|
42076
|
+
_processDataItem(dataItem, relevantWidgets, messageType) {
|
|
42103
42077
|
// Process individual data items and route to appropriate widgets
|
|
42104
42078
|
// Option chain arrays are handled separately by _processOptionChainData
|
|
42105
42079
|
|
|
42106
42080
|
const symbol = this._extractSymbol(dataItem);
|
|
42107
|
-
const messageType = this._extractMessageType(dataItem);
|
|
42108
42081
|
if (this.config.debug) {
|
|
42109
42082
|
console.log('[WebSocketManager] Processing data item:', {
|
|
42110
42083
|
symbol,
|
|
@@ -42198,7 +42171,7 @@ ${SharedStyles}
|
|
|
42198
42171
|
_extractSymbol(item) {
|
|
42199
42172
|
// Handle new format where symbol might be in nested data structure
|
|
42200
42173
|
// Check if this is an array with data that has _messageType (normalized new format)
|
|
42201
|
-
if (Array.isArray(item)
|
|
42174
|
+
if (Array.isArray(item)) {
|
|
42202
42175
|
// The data is an array, check first item for symbol
|
|
42203
42176
|
if (item.length > 0 && item[0]) {
|
|
42204
42177
|
return this._extractSymbolFromItem(item[0]);
|
|
@@ -42264,25 +42237,15 @@ ${SharedStyles}
|
|
|
42264
42237
|
return symbolMappings[extracted] || extracted;
|
|
42265
42238
|
}
|
|
42266
42239
|
_extractMessageType(item) {
|
|
42267
|
-
//
|
|
42268
|
-
if (item._messageType) {
|
|
42269
|
-
return item._messageType;
|
|
42270
|
-
}
|
|
42271
|
-
|
|
42272
|
-
// Determine message type based on content
|
|
42273
|
-
if (item.type) return item.type;
|
|
42274
|
-
|
|
42275
|
-
// Infer type from data structure
|
|
42240
|
+
// FALLBACK: Infer type from data structure (for legacy/malformed messages)
|
|
42276
42241
|
|
|
42277
42242
|
// IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
|
|
42278
42243
|
// Level 2 ONBBO data - array of MMID objects or object with MMID field
|
|
42279
42244
|
if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
|
|
42280
42245
|
return 'queryonbbol2';
|
|
42281
42246
|
}
|
|
42282
|
-
|
|
42283
|
-
|
|
42284
|
-
if (item.MMID !== undefined) {
|
|
42285
|
-
return 'queryonbbol2';
|
|
42247
|
+
if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
|
|
42248
|
+
return 'queryoptionchain';
|
|
42286
42249
|
}
|
|
42287
42250
|
|
|
42288
42251
|
// Check for Source field AFTER MMID check
|