diff --git a/AGENTS.md b/AGENTS.md index 5228e96..a4bc5dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,7 @@ | Trading-data, CoinMarketCap-data, auto-refresh, or cross-script validation | docs/userscript-validation.md | | Behavioral tests, test lint, affected selection, or source coverage | docs/test-policy.md | | Read-only review | skills/userscript-review/SKILL.md | +| Chart mirror storage and IndexedDB | docs/binance-chart-storage-development.md | | Release or publish | skills/userscript-release/SKILL.md | | Codex/browser/proxy/helper/connection timeout | global timeout rule; if available, ~/.dotfiles/knowledge/shared/CODEX_TOOL_TIMEOUT_TRIAGE.md | diff --git a/README.md b/README.md index 26f1b7d..6e0ad3c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ This repository is the source of truth and distribution point for the scripts. G - Infers direction from the current open/close tab. - Provides a quantity multiplier panel. +- Includes chart mirror storage optimization without a separate installation. - Handles orderbook display precision, available quantity, current orders, and ladder order boundaries. - Designed for manual traders who frequently inspect the orderbook and submit limit orders. @@ -108,6 +109,7 @@ npm run build:binance-userscripts ## Documentation +- [Binance chart storage optimizer](docs/binance-chart-storage-development.md) - [Signal gateway architecture and usage (Chinese)](docs/signal-gateway-architecture.zh-CN.md) - [Binance orderbook trade development](docs/binance-orderbook-trade-development.md) - [Binance Strategy 29 Bollinger signals and migration](docs/binance-strategy29-bollinger-development.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 1a207fe..a9dd74f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -51,6 +51,7 @@ - 根据当前开仓/平仓 tab 自动识别操作方向。 - 支持数量倍率面板,减少重复输入。 +- 内置图表镜像存储优化,无需额外安装脚本。 - 处理订单簿价格精度、可用数量、当前委托和阶梯挂单相关边界。 - 适合高频查看盘口、需要快速提交限价单的手动交易场景。 diff --git a/docs/binance-chart-storage-development.md b/docs/binance-chart-storage-development.md new file mode 100644 index 0000000..754dcab --- /dev/null +++ b/docs/binance-chart-storage-development.md @@ -0,0 +1,99 @@ +# Orderbook Chart Storage Optimization + +`src/binance-orderbook-trade/chart-storage/` owns the production implementation. Build +the existing `scripts/binance-orderbook-trade.user.js` with +`npm run build:binance-orderbook-trade`. There is no separate install entry. +The earlier `experiments/binance-chart-storage/` files are historical research +and independent baselines; production imports none of them. + +## Behavior and scope + +The module installs before the orderbook business initializer at document-start +in the page's MAIN_WORLD. It only admits +top-level Binance perpetual trading routes accepted by the shared route parser, +including USDT and USDC symbols. Futures home, landing and other subpages receive +no storage hook or UI. The orderbook retains its own existing frame and SPA +behavior. Landing-to-trading SPA navigation does not re-run the storage installer; +only a fresh matching page starts early interception. It never places orders, changes leverage, reads credentials, changes +the proxy, clears databases or replaces global storage methods. + +Only the pinned TradingView module `70940` mirror destination expression changes. +The complete original factory must match before first execution. Late injection, +source drift and a missing capture after a 30-second timer retire interception +and leave native registration intact. Metadata does not guarantee early injection +on every extension configuration; rejection remains a supported outcome. + +Each accepted mirror uses one IndexedDB read/write transaction and compares the +destination values before writing. Equal JSON trees must retain signed zero, +property order and dense array structure. Missing keys remain distinct from stored +null. Ordinary saves, source reads, Basic chart settings and `clear()` retain +their native implementations. No record cache persists between batches. + +Input admission defaults to 512 entries, 4 MiB per batch and 16 pending batches +with 64 MiB of copied input. Unsupported values, capacity limits, initialization +or driver/configuration rejection and synchronous transaction-creation failure +execute the original native expression once, before any optimized transaction +exists. Native handling may exceed the optimizer's limits; these limits bound +optimization, not Binance's original work. Shared references, cycles and non-JSON +input use that native path instead of silently changing their stored meaning. + +Within a transaction, unsupported old values and exhausted comparison budgets +cause direct writes. They do not reject otherwise valid replacements. A genuine +transaction failure aborts the entire batch and stops future optimization; it +never replays the failed batch. The original caller still receives the failure +through its Promise boundary. Subsequent calls resume native handling. + +## Lifecycle and diagnostics + +`self.__BINANCE_CHART_STORAGE__.snapshot()` returns fixed aggregate counters and +startup status. `stop()` drains the current instance's accepted work, including +every member of an early-rejecting native Promise array. Calls arriving during +drain wait for that finite fence, then run their native expression. After stop, +native arrays and errors pass through synchronously and statistics freeze. +The public interface exposes no stored keys, values or storage methods. + +`acceptedBatches` counts created optimized transactions. `committedTransactions` +counts real completion events, while `committedWrites` and `skippedWrites` count +only committed work. `rejectedBatches` and `nativePassthroughBatches` describe +native routing before transaction creation, not failed saves. `failedBatches` +counts failed optimized transactions. The separate native-only experiment's +`completed` and `dispatches` fields must not be interpreted as commit counters. + +Stopping does not close shared connections, undo committed data, drain other tabs +or guarantee persistence before page destruction. Leaving a supported route +retires optimization when a retained callback runs; pagehide also requests stop. + +## Compatibility limits + +The native per-key mirror can partially commit; an optimized destination batch +commits or aborts as a whole. Its interleaving with native clear and other tabs +therefore differs. Source reads remain non-atomic, and two databases cannot share +this transaction. No newest-revision ordering, cross-tab coordination, repair of +already queued work or universal blank-page recovery is promised. The change +addresses repeated destination transactions and unchanged writes specifically. + +The retained upstream factory is generated from the exact public response and +verified with its manifest and inverse replacement. To update that source pin, +first inspect and regenerate the fixture provenance; never broaden the matcher +to accept an unknown module. The generation command accepts an explicit output: + +```sh +node experiments/binance-chart-storage/generate-mirror-module.js /path/to/verified/TradingView.99bc5074.js src/binance-orderbook-trade/chart-storage/mirror-module.js +``` + +## Validation and release + +- Runtime unit scenarios execute the real bootstrap and exact native factory. +- Production writer scenarios use real IndexedDB and the captured localForage, + covering original-value semantics, admission, reconnect, abort and drain. +- Multi-tab scenarios use competing different revisions, native clear, reload + and actual transaction events; no fixed sleep determines business completion. +- The anonymous smoke runner executes the generated install artifact in a fresh + Chrome context. Tampermonkey installation requires separate exact installed + and loaded-source checks in the user's authorized test tab. + +Run the affected Node and browser tests, `npm run lint:tests`, the build/check, +one passing full `npm test`, and `git diff --check`. Publish through a reviewed +GitHub PR; verify the main raw artifact before synchronizing Tampermonkey. A +successful chart and committed test batch do not establish long-running stability +or prove that every historic white-screen incident had the same cause. diff --git a/docs/binance-futures-blank-page-diagnosis.md b/docs/binance-futures-blank-page-diagnosis.md index cbca2a4..e7899fc 100644 --- a/docs/binance-futures-blank-page-diagnosis.md +++ b/docs/binance-futures-blank-page-diagnosis.md @@ -109,3 +109,824 @@ Do not patch Binance's router or call its continuation from a userscript. The separate userscript bug was broad route recognition (`home` became `HOME`) and Strategy31's non-trading status lifecycle. Fixing those defects does not establish that the host startup stall is fixed. + +## 2026-10-06: native lock-queue hotspot and verified session recovery + +This follow-up identified a native browser bottleneck, beyond the earlier +application-level cache gate. It did not identify the initial transaction +producer or establish a permanent fix. + +In the normal Chrome profile, USUSDT rendered while 1000LUNCUSDT, PHAUSDT and +ALICEUSDT retained the Futures skeleton. Their theme-start marks had no matching +end. Existing ALICE requests included pending native IndexedDB opens for +`theme-config-futures-trade-ui` and `i18n-data-zh-CN`. The user reported that +unsigned-in DIAUSDT rendered both the orderbook and TradingView in incognito. +This comparison alone did not isolate extensions, authentication and storage. + +The user also supplied a screenshot of `chrome://indexeddb-internals/` showing +only its heading and filter input, without database rows or a Force close button. +The exact-version [frontend source](https://github.com/chromium/chromium/blob/154.0.8037.98/content/browser/resources/indexed_db/indexeddb_internals.ts#L188) +registers the input listener only after metadata arrives. The +[backend](https://github.com/chromium/chromium/blob/154.0.8037.98/content/browser/indexed_db/indexed_db_context_impl.cc#L596) +waits for bucket metadata callbacks, including work posted to bucket contexts; +the UI also waits for all loaded storage partitions. One stalled branch can +therefore prevent the entire list from appearing. + +A three-second native sample of Chrome 154.0.8037.98 ARM64 captured all 233 +samples of one busy worker inside `Connection::RemoveTransaction`; 232 of them +continued through the blocking-check path below: + +```text +BucketContext::RunTasks +Database::RunTasks +Connection::RemoveTransaction +Transaction::IsTransactionBlockingOtherClients +PartitionedLockManager::IsBlockingAnyRequest +``` + +The deepest frames repeatedly included `SupportsUserData::GetUserData` through +the lock-request filter. Offline symbolication used the +[official dSYM](https://dl.google.com/chrome/mac/stable/dsym/googlechrome-154.0.8037.98-arm64-dsym.tar.bz2), +whose UUID exactly matched the running Framework: +`4C4C4433-5555-3144-A154-EC6770D35233`. `atos` reported an unsupported DWARF +attribute but returned function names; no source-line resolution is claimed. + +The matching source shows that +[`RemoveTransaction`](https://github.com/chromium/chromium/blob/154.0.8037.98/content/browser/indexed_db/instance/connection.cc#L238) +checks remaining started transactions, while +[`IsBlockingAnyRequest`](https://github.com/chromium/chromium/blob/154.0.8037.98/components/services/storage/indexed_db/locks/partitioned_lock_manager.cc#L151) +walks held locks and their waiting queues. These loops explain the sampled work; +the sample does not reveal queue lengths, database names or the originating +JavaScript calls. Do not label it a proven deadlock, corruption, or userscript +defect. + +Recovery observations, with explicit permission to close the four named tabs: + +- Gracefully closing only USUSDT's existing theme-cache connection did not + restore the failed pages. A successful `close()` call did not prove all work + on that connection had finished. +- Closing all four normal-profile Binance tabs reduced the sampled Chrome + process CPU from about 104.5% to 4.6%. The same worker waited in 254 of 255 + subsequent samples. +- Without restarting Chrome or clearing site data, the user observed that the + existing IndexedDB internal page automatically displayed its list. +- A newly opened, sole DIAUSDT tab rendered the orderbook and TradingView, + verified in a screenshot. Its initial theme-cache gate took about 287 ms. +- One ordinary reload also rendered both components; the initial theme gate + took about 103 ms. The page remained mounted at a later four-minute check. + +The initial culprit tab, transaction producer, and the contribution of native +Binance code versus extensions remain unknown. Incognito success and this +recovery do not prove either side innocent. Keeping one trading tab is the +verified session workaround; its long-term stability and reopening multiple +trading tabs were not validated. No proxy, extension, or persistent-cache change +was made in this follow-up, and Force close was not performed. + +Upstream [CL 6133080](https://chromium-review.googlesource.com/c/chromium/src/+/6133080) +optimized this blocking check, including a single-client shortcut; its core +behavior is already present in this Chrome version. +[CL 7252092](https://chromium-review.googlesource.com/c/chromium/src/+/7252092) +adds diagnostic connection counts rather than a performance fix. Both public +CL message/comment threads and final diffs were inspected. The linked +[issue 384476946](https://issues.chromium.org/issues/384476946) was not readable +anonymously, so its full discussion, status and applicability remain unverified. +No confirmed new repair version or applicable official switch was found. + +Local incident artifacts are under `/tmp/binance-multi-startup-o2R1Oy/`, including +`symbolicated-idb-worker.json`, `recovery-result.json`, and the before/after +screenshots. The large downloaded symbol files are disposable and are not +required to read those retained results. + +## 2026-10-06: recurrence and chart-save transaction amplification + +After additional trading tabs were opened, the user supplied two IndexedDB +internal-page screenshots. One explicitly showed `chart_delivery` / +`keyvaluepairs`, with a running readwrite transaction followed by many blocked +readwrite transactions, aged about 127 seconds. The other showed blocked writes +to a `usage` store, but omitted its database heading. Transaction identifiers +are not queue lengths. These screenshots are snapshots, not continuous traces. + +A bounded transaction observer on USUSDT subsequently counted 13,050 newly +created `chart_delivery` readwrite transactions, 1,460 `chart_futures` readonly +transactions and 591 `chart_futures` readwrite transactions. The observation +was shorter than 105 seconds; its precise stop timestamp was not retained, so +it does not establish a 60-second rate. The first through 7,000th sampled +delivery writes spanned about 510 ms. A 2,000-listener limit meant most delivery +transactions were untracked after creation; zero observed chart completions +does not prove that all 13,050 transactions remained blocked. The observer did +not read records or initiate transactions, and the native method was restored. + +The loaded [TradingView chunk](https://bin.bnbstatic.com/static/chunks/TradingView.99bc5074.js) +was byte-for-byte identical to the inspected public source. Module `70940`: + +1. Reads all current-namespace keys and selects `#TV_SYMBOL-*` and + `myTradingView*` keys. +2. Reads every selected value concurrently. +3. Unconditionally writes every selected key to the other chart namespace, + again concurrently. The namespaces include `chart_futures` and + `chart_delivery`; a USDT page therefore writes the delivery database without + a COIN-M page being open. + +This synchronization runs after native saves and directly on active-chart +changes. Its entry count must not be labeled a save count. Native save callbacks +also write the current layout and symbol data before invoking synchronization. + +A second observation combined transaction counts, drawing-event subscriptions +and conditional breakpoints that always returned false. Breakpoints were bound +to verified executable locations in the loaded source; they did not replace +save callbacks. During 45.115 seconds it observed: + +| Observation | Count | +| --- | ---: | +| Native scheduled-save executions | 21 | +| Drawing `properties_changed` / `remove` events | 15 / 5 | +| `onAutoSaveNeeded` events | 1 | +| New synchronization starts | 3 | +| Key-read batches / total selected keys | 27 / 7,830 | +| Synchronization batches reaching destination writes | 0 | +| New current-chart readonly / readwrite transactions | 7,836 / 24 | + +The read batches averaged 290 keys. There were more resumed read batches than +new synchronization starts, demonstrating that this window included earlier +asynchronous work. The prior 13,050 writes equal 45 times 290, but that arithmetic +alone does not prove the prior batch count or a constant historical key count. +Debugger instrumentation can affect scheduling and performance; these counts +are not an uninstrumented performance benchmark. + +A separate 40.393-second drawing observer identified all 11 property-change +events as `LineToolOrder`; six removals originated in the native shape update +loop in [chunk 66202](https://bin.bnbstatic.com/static/chunks/66202.233a75ac.js). +The user confirmed an automatic task was running during this observation. +Native `Gr` schedules a complete save after every drawing event except `click` +and `move`, using independent 100-ms timers. The precise automatic task was not +inspected or stopped. + +At the same time, a screenshot confirmed MUBARAKUSDT had a populated orderbook +but an empty TradingView region, with no chart iframe. This recurrence was not +fixed by the earlier session recovery. The evidence identifies active native +save amplification; it does not prove a corrupted database or exclude every +other producer. The separate `pika-i18n-usage` producer was also observed. + +All conditional breakpoints and drawing observers were removed, and the native +transaction method was verified restored. No records were read or deleted by +these probes. Evidence files are in `/tmp/binance-idb-producer-nfcyS5/`, including +`us-transaction-observation.json`, `combined-save-mirror-count.json` and +`drawing-origin-count.json`. + +### Order-line visibility control + +The current [TradingView library](https://www.binance.com/static/chart/tradingview/en/trading-platform-30/bundles/library.50c4aba0e2bfdfef5b8b.js) +defines `LineToolTrading.state()` as returning `null`. +[LineToolOrder](https://www.binance.com/static/chart/tradingview/en/trading-platform-30/bundles/line-tool-order.bd1676028df5fdf272ee.js) +inherits that implementation. The pane serializer excludes null source states: +these order lines are not saved in the chart snapshot. Their property and remove +events nevertheless reach Binance's drawing-save listener. This identifies +unnecessary whole-chart persistence triggered by transient order graphics. + +With explicit permission, the native USUSDT chart menu's checked `当前委托` +(`Open Orders`) option was temporarily unchecked, then restored. The automatic +task was not stopped or controlled by the diagnostic. One initial toggle and +one initial restore attempt encountered a closed tooltip and failed their +pre-action assertions; neither attempt clicked anything. The actual checkbox +transitions were separately verified as false and then true. + +- A 60.001-second observation beginning about 27 seconds after hiding the lines + recorded zero drawing events, zero scheduled native saves and zero chart + writes. Only ten current-chart readonly transactions were created. +- The browser process still showed about 92.2% CPU, and MUBARAKUSDT still had no + TradingView iframe. Stopping this producer did not immediately drain or abort + previously queued work. +- Restoring the original checked state produced thirteen property-change + events and thirteen native saves within the remaining approximately + eleven-second observation segment. The setting was verified checked again. + +This on/off/on comparison establishes that the native order-display path was +an active save producer. It does not by itself prove every failed page was +blocked exclusively by that producer. Hiding these graphics is a validated +way to stop this source while leaving the order list available; permanent +application and another all-tab recovery require separate authorization. + +Simply serializing callbacks is not a complete repair: active-chart changes +bypass that entry point, some callbacks discard their completion Promise, and +the native mirror catches failures before all remaining writes necessarily +finish. It also retains the same total work and can accumulate a JS queue. +A repair preserving order graphics would need to filter the native save +listener specifically, including reliable identification of removed order +graphics, while preserving manual drawings and other subscribers. Such a +userscript implementation has not been validated or deployed. + +The original display state was restored, all diagnostic globals and breakpoints +were removed, and `IDBDatabase.prototype.transaction` was verified native. +Additional evidence: `orders-hidden-count.json`, `orders-restored-count.json` +and `before-hide-save-count.json` in the same incident directory. + +### Contribution of the userscript save coalescer + +The user questioned whether the earlier batch-save optimization was involved. +The USUSDT page's loaded orderbook source was version `2.7.217` and contained the +complete current generated install artifact byte-for-byte. This establishes +loaded-source identity for that tab, not just installed metadata. + +The October 5 change `a846922` replaced each burst/round's last `pendingSave` +with an array of accepted callbacks. `deliverSaveCallbacks` serializes once, +then synchronously invokes every callback with a separate copy of the final +snapshot. It does not await callback Promises. For N native persistence +callbacks, the old intercepted batch started one native persistence chain; +the current batch starts N chains. Each successful chain can subsequently +mirror K historical keys. Thus `fullSaveCount: 1` describes serialization, +not one database save or one transaction. Current tests explicitly require one +serialization and N callback deliveries; they do not establish fewer native +database writes. + +A 45.009-second live observation directly counted one coalescer batch containing +eight accepted callbacks and eight callback invocations. The same window +counted eighteen native scheduled-save executions. These window counts need not +represent identical sets of requests, but they prove the current coalescer was +actively replaying multiple persistence callbacks in one batch. Evidence is in +`coalescer-attribution.json`; the conditional breakpoints were removed afterward. + +This is a verified contribution and a performance-contract gap, not proof that +the change caused every earlier blank page. Native saves outside the coalescer +and active-chart synchronization also remain. Returning to unconditional +last-callback dropping would lose other callers' completion behavior. A repair +must distinguish redundant native persistence from callbacks that must be +delivered, or remove the unnecessary native broker-drawing save trigger at a +reliably identified boundary. No such durable repair is claimed here. + +### Historical global-facade prototype: first stage + +The following three stages preserve the original global-facade research. That +design changes native mixed-operation ordering and is no longer the production +candidate. The current module-scoped mirror experiment is described afterwards. + +With authorization for an isolated experiment, a prototype was added under +[`experiments/binance-chart-storage/`](../experiments/binance-chart-storage/README.md). +It uses an exact public Binance Rspack runtime and localForage module `43917` +fixture, a fresh Playwright Chromium context, synthetic chart records and real +IndexedDB. All external requests in these lab tests are blocked. The two initial +entrypoints (`adapter.js` and `capture.js`) reject origins other than +`https://chart-storage.test`; no adapter was installed in a +real Binance document, and no production database or trading action was touched. + +The tested intervention is the library's actual `createInstance` boundary after +its first factory execution. A synchronous capture hook runs before the host's +`require` returns. A separate facade survives the library's asynchronous driver +initialization, queues calls across instances in issue order and combines them +into bounded native transactions. Each write compares against the stored value +inside that transaction and skips only an unchanged existing record. Every +accepted optimized-batch Promise and callback completes after commit or abort. It does +not reuse one transaction behind multiple unmodified localForage handlers. + +For 290 prepopulated synthetic records and eight sequential unchanged-save +rounds, native localForage produced 4,640 transactions and 2,320 puts. The same +workflow through the prototype produced 16 transactions and zero puts. Eight +simultaneous save rounds produced ten transactions and zero puts. Both prototype +workloads delivered 4,640 distinct callbacks, returned matching results, retained +matching persisted data, and ended with no pending requests. Comparison increased +native gets from 2,320 to 4,640 in the sequential workload; this is evidence of +reduced transaction/write amplification, not reduced total reads or a measured +CPU/latency improvement. + +The browser tests also cover initialization, mixed-operation ordering, +null-versus-missing keys, native barriers, capacity errors, abort rollback, +callback exceptions, connection close/version change, stop/drain, persistence +after reload and simultaneous two-page writes. A held native transaction ensures +the two-page test has both writes queued before releasing the lock; only one +physical put is needed when both request the same new value. + +All 31 Chromium scenarios and three Node origin-guard tests passed, including +real quota exhaustion configured before the first database write. That quota +case rejected every operation in the batch with `QuotaExceededError`, completed +all callbacks and preserved prior values. Test-policy lint and experiment syntax +checks passed. The rendered lab report was inspected. Results and the remaining +production boundary are recorded in the experiment README. + +This remains a restricted prototype. The test browser is Chromium +`151.0.7922.34`, not the affected Chrome `154.0.8037.98` profile. Module identity, +early injection, real chart values and all native callers require validation +before an actual page adapter can be designed. Batched rollback and read +completion timing differ from native localForage, and native barriers retain +their own one-time connection recovery. The prototype cannot make two databases +atomic, repair separate application read/modify/write races or drain previously +queued transactions. No permanent blank-page repair is claimed by these tests. + +### Historical storage integration follow-up: 2026-10-06 + +Further source inspection and regression tests identified two integration +obstacles before any production storage interception: + +1. Installing the original chunk `push` wrapper before the Rspack runtime + missed first execution. The runtime retains that wrapper as a parent append + callback, reached after registration and synchronous host execution. The + new `bootstrap.js` observes the runtime's first `push` assignment and captures + the export before the host uses it, for both module/runtime script orders. + Diagnostic failure rejects observation without poisoning the host's cached + module. Cleanup also preserves a newly created queue during an active append. +2. TradingView's save callback returns JSON parsed inside its iframe. Comparing + its object prototype with the parent's `Object.prototype` rejected valid + snapshots. The adapter now recognizes native plain JSON objects across realms; + real iframe regressions verify nested data and own `__proto__` properties, + while Date, class and forged-prototype values remain rejected. + +The current user USUSDT page's module factory was read without requiring the +module, reading records or refreshing the page. Its complete module-property +SHA-256 was `f33d0f78c185ff592f1f0781e21baab63e7db937054866c37afdfda028e9e647`, +matching the fixture. This verifies one current build, not a stable public API. + +The added passive `preflight.js` briefly observes the real export's +`createInstance` and returns each original instance. It records fixed chart +database/default-store metadata and creation times, with at most sixteen +timestamp records. It does not wrap instance methods, initialize storage, +record keys/values, or enable the adapter. Only this preflight and bootstrap +additionally allow the exact top-level USUSDT incident path. The storage adapter +and original capture remain restricted to the lab origin. Manual stop and a +30-second deadline restore the native factory; dedicated tests prove retained +original instances still work and passive observation adds no IndexedDB opens, +transactions, gets or puts. Other cleanup branches have not each been separately +tested. + +The expanded validation passed all 45 Chromium scenarios (31 prototype and +14 integration) and eight Node origin/path/frame rejection cases. Test-policy +lint and syntax checks also passed. These fixture-based scenarios use real +IndexedDB; they do not exercise the user's installed extension or account. + +Anonymous Chrome `154.0.8037.98` observations then used separate temporary +profiles, no extensions and no login, with synchronous `addInitScript` +installation before navigation. The initial run established capture before the +first chart instance. A later screenshot showed the default Basic chart and +orderbook working, so waiting for a TradingView iframe in that mode was an +incorrect test premise. One mode-selection attempt also timed out because the +actual native label is `Trading View`, including the space. + +Using the verified label produced an iframe and seven canvases, but screenshot +inspection still showed the loading animation. That run's raw `ui_ready` result +is too weak to establish a rendered chart. The final observation therefore +waited for native `activeChart().dataReady()` and subsequent paint frames, then +inspected a screenshot showing USUSDT candles, moving averages, volume bars and +the populated orderbook. The captured library's no-callback `dataReady()` checks +for nonempty main-series data and does not initiate storage operations. + +In this final run the module was captured at 2,184.1 ms, before the first chart +instance at 3,008.7 ms. At 7,429.2 ms there was one TradingView iframe, twelve +canvases and `dataReady() === true`; no page errors were observed. Twenty futures +instances and zero delivery instances were observed before stop. This establishes +the capture ordering and one successfully rendered anonymous page. All temporary +browsers were closed, and the final observer reported `active=false` with no +failure. Results and the inspected screenshot are retained in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-storage-preflight-chart-ready-1nRAMB/`. + +At that stage, production blockers remained. The actual source creates and retains localForage +instances during React rendering, and Basic charts share these databases with +TradingView. The adapter's then-current stop drained accepted work but rejected later +calls through retained facades, so restoring the factory alone cannot safely +disable it in an already mounted page. Real changed snapshots, Basic settings +and clear operations, capacity and connection behavior, native layout +read/modify/write and cross-database mirroring need integration coverage. Batch +rollback and read completion timing still differ from native behavior. + +The anonymous injection does not verify Tampermonkey document-start/page-world +timing, an affected logged-in profile, sustained trading load or blank-page +recovery. No production userscript source or install artifact changed; no adapter +was installed in Binance and no existing trading tab was refreshed by this +follow-up. The storage-layer experiment demonstrates reduced transaction and +unchanged-write work, not a deployed repair. + +### Historical facade lifecycle and original host workflows: 2026-10-06 + +The next implementation stage addressed the specific stop incompatibility above. +Six failing browser regressions first reproduced rejected retained-facade calls, +premature native-factory restoration and incomplete descriptor cleanup. The +adapter now closes optimized admission, drains the finite accepted queue and +only then restores the original factory descriptor. Calls through retained or +newly created managed instances during drain wait for that boundary and resume +through the original native methods afterwards. Full arguments, native receivers +and method-specific callback signatures are preserved. Reinstallation on the +same export is prohibited until a fresh page lifecycle, preventing old facades +from bypassing a second installation's queue. + +The nine lifecycle scenarios include a real write lock, a version-one connection +blocking the library's version-two database initialization, abort completion, +callback reentry, native Blob and API use after stop, ownership changes and +descriptor restoration. A review also identified value-inspection reentry: +a transparent Proxy could call stop during JSON validation before admission. +That failing regression is fixed by rechecking the active phase immediately +before enqueue. An ownership change during drain rejects stop and preserves +the external factory instead of overwriting it. No queued operation is retried. + +An independent native-library comparison established that simultaneous native +`setItem` and `getItem` calls can read the previous value: the captured set method +creates its transaction one Promise stage later. Post-stop tests now compare +against that actual native baseline, while separately proving final persistence +and the inability to overtake the accepted optimization queue. A stopped adapter +does not impose its stronger issue-order semantics on later native calls. + +Compatibility tests also now execute the original host persistence functions, +rather than only a synthetic get/set workflow. The +[public host fixture](../test/fixtures/binance-chart-storage/host-persistence.md) +contains 273 exact original source fragments and 203 original dependency factories +from seven public bundles. An independent reviewer verified full source hashes, +byte counts and every original UTF-16 range. The fixture retains the original +TradingView save/load functions, save callback and cross-database mirror, together +with Basic storage, annotation conversion and old-key migration. It supplies only +the documented storage/widget/reference context and runs through the captured +Rspack runtime and real localForage. Upstream Basic React subscriptions, debounce, +JSON serialization and actual chart rendering remain outside these fixtures. + +Fourteen host-workflow scenarios run the same workflows against native storage +and the adapter. They verify drawing removal without resurrection, layout +retention, both databases' mirror contents, all six Basic storage categories, +old-key migration and shared-store clear. Four saves with revisions `[1,1,2,2]` +used 64 transactions and 36 puts natively, versus 28 transactions and 11 puts +through the adapter. The expected data matched in both databases. + +A bounded load then seeded 290 identical historical records in each database +and executed eight rounds of four concurrent original save/mirror chains. +Each round changed the current snapshot and drawing. All 32 chains completed; +both databases' 293 keys, all 580 historical values and final snapshot, layout, +drawing and native load results matched expectations. +The four chains within a round use the same revision, and rounds are awaited +sequentially; this does not test competing different revisions. + +| Isolated 32-chain workload | Transactions | Puts | Gets | +| --- | ---: | ---: | ---: | +| Native | 18,912 | 9,472 | 9,408 | +| Adapter | 112 | 32 | 18,880 | + +The adapter skipped 9,440 unchanged writes; its 112 transactions include 32 native +barriers. Failure, rejection, abort and pending-operation counts were zero. +Setup and final verification I/O are excluded. The measured installation-lifetime +peak was 1,172 operations and 181,767 bytes; workload counters and lifetime peaks +are labeled separately in the attachments. Individual lab elapsed-time samples +are not evidence of sustained production latency or CPU improvement. +The complete load report and extracted measurements are retained as +`/tmp/chart-storage-load-retained-20261006.json` and +`/tmp/chart-storage-load-measurements-20261006.json`. + +The tests deliberately retain a further compatibility difference. Concurrent +native sets can create transactions after a later-invoked Basic clear, leaving +those new records present; the adapter enforces issue order and removes them. +The normal shared-clear path awaits completed saves first and matches in both +implementations. Separate assertions expose the concurrent difference instead +of hiding it behind an equivalence claim. + +All 68 Chromium scenarios (31 prototype, 14 startup integration, nine lifecycle +and 14 original host workflows) and eight origin/path/frame Node cases passed. +Full test-policy lint, syntax checks and independent code review passed. This +stage corrected the experiment's stop behavior and broadened its compatibility +evidence. It still does not establish Tampermonkey page-world timing, real saved +data compatibility, long-running stability, mounted live UI behavior or recovery +of the affected profile. No production userscript, installed script, user database +or trading action was modified by the experiment. + +### Current module-scoped mirror candidate: 2026-10-06 + +The global facade above is historical research, not the production candidate. +Its issue-order queue changes the captured native driver's ordering between +`setItem` and `getItem`, `removeItem` or `clear`. In particular, native set-value +preparation can defer its transaction until after a later Basic clear; the facade +orders that set before clear. Preserving each implementation's own expected +result does not make them equivalent. Native storage methods must remain native. + +The new experiment replaces only the destination-write `Wr=fr.map(...)` RHS in +public module `70940` of `TradingView.99bc5074.js`. Acorn identifies a unique +expression at `[33783,34025)` within the complete method at `[6665,36819)`. +The method's SHA-256 is +`37d249d1ceec4a41b54a959c1ab2c4b52b3061e504cf8cd81a824962af8c6c88`. +Generation pins the full public source and method, rejects a colliding service +identifier and verifies that undoing the one insertion reproduces the whole +original method byte for byte. It emits static JavaScript without runtime +`eval` or `new Function`: + +```js +Wr = __dispatchChartMirror(Sr, fr, () => ORIGINAL_FR_MAP_EXPRESSION) +``` + +Registration compares the exact full `Function.prototype.toString` result before +replacing the factory. The bootstrap selects `70940` only with the explicit +replacement callback; default `43917` observation is unchanged. Mismatch rejects +observation while preserving native module execution and cache behavior. A +review found that a replacement callback could synchronously stop its observer +yet still return a candidate that was installed. Both runtime orders reproduced +that failure before a post-callback active-state check fixed it. Stop now retains +the original registration in that case; ordinary cleanup restores queue and +factory slots while preserving already executed cached exports. + +Source `keys()` and every source `getItem()` remain unchanged, as do the original +mirror guards, catch and target exclusion. The ordinary TradingView save, Basic +settings and reset paths are outside the replacement. The dedicated writer never +hooks `createInstance`, `setItem`, `getItem`, `removeItem` or `clear`. It uses one +real destination transaction to read current values and write changed or missing +records; null and missing are distinguished. The tests execute the original +callback fragments and dependencies rather than rewriting the host save logic. +See the [scoped fixture](../test/fixtures/binance-chart-storage/mirror-scoped.md) +and [current experiment contract](../experiments/binance-chart-storage/README.md). + +The writer defaults to `scope: 'lab'`, restricted to `https://chart-storage.test`. +An explicit `isolated-ususdt-probe` scope permits only the exact top-level +`https://www.binance.com/zh-CN/futures/USUSDT` page. Origin/path/frame checks cannot +prove anonymity; a dedicated harness must own a fresh Chrome process/profile +without login or extensions. Default production-origin refusal is unchanged. +Both scopes require the native IndexedDB `keyvaluepairs` stores in `chart_futures` +and `chart_delivery`. The writer +validates/copies JSON and bounds entries, input bytes, stored-read bytes and +pending work. Empty input performs no initialization or transaction. A failed +batch rolls back completely and is never replayed. Stop drains only the finite +accepted set; later mirror calls wait for that fence and evaluate the retained +original async per-key map, while ordinary native operations remain independent. + +This narrowing preserves ordinary API behavior but deliberately changes mirror +atomicity and interleaving. Native per-key mirror writes can partly commit; +the new destination batch commits or aborts as a whole and can interleave +differently with native clear or another tab. Limits add explicit rejection +conditions. The original outer catch still owns reporting. Source reads remain +non-atomic, separate databases remain independent, and application read/modify/ +write races remain outside the fix. + +The new suites contain 19 scoped-integration scenarios and 18 writer scenarios. +They cover exact source identity, native set/get/remove/clear results and callback +order, method identity, original guards and save chains, null/empty data, +registration/cache cleanup, reentrant stop, real abort/quota rollback, limits, +blocked initialization, clear interleaving and another tab's changed data. +Before the explicit page-probe scope, the combined experiment run passed 105 Chromium scenarios, eight Node +origin/path/frame cases and the full test-policy lint. These include the retained +historical suites; that count does not turn the facade into the current candidate. +After the explicit scope was added, three new origin/scope regressions first +failed, then the expanded Node suite passed all 18 cases. The 37 scoped/writer +browser scenarios and full test-policy lint also passed. Scope permission and +anonymous-profile isolation are distinct contracts. + +The current bounded workload again starts with 290 historical keys per database +and completes eight rounds of four concurrent original save/mirror chains. Each +round uses one changed revision, and later rounds await the preceding round. +Every historical value, both databases' 293 keys and final saved drawing data +match. Setup and final verification I/O are excluded. + +| Current 32-chain workload | Transactions | Puts | Gets | +| --- | ---: | ---: | ---: | +| Native | 18,912 | 9,472 | 9,408 | +| Scoped mirror writer | 9,568 | 112 | 18,784 | + +The 112 puts comprise 96 ordinary source writes and 16 changed +destination writes. All native source reads remain; comparing 293 destination +keys in each of 32 mirrors adds 9,376 gets. Both workloads finish with zero +pending or aborted transactions. Counts are verified in +`/tmp/chart-storage-final-20261006.json` and the extracted +`/tmp/chart-storage-mirror-measurements-20261006.json`. They demonstrate less +transaction/write work, not fewer reads, sustained CPU/latency improvement or +recovery of the affected profile. + +A separate anonymous Chrome `154.0.8037.98` run used a temporary profile without +extensions or login and injected the full static `70940` replacement before +registration. Its service would execute only the exact original native thunk; +the lab writer was not installed. Strict pin validation attempted once, matched +once, and the replaced full module executed once, with capture at 5,975.4 ms. +At 10,015.6 ms the native chart reported `dataReady() === true`. The inspected +screenshot showed USUSDT candles, moving averages, volume and a populated +orderbook. There were no page errors; observation stopped and the browser closed. +Results and the screenshot are retained in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-native-preflight-HeCAar/`. + +That run recorded **zero mirror dispatches**. It verifies complete module startup +and rendered readiness with a native-only service, but neither invokes the +replaced mirror branch nor proves real batched writes. + +A subsequent independent anonymous Chrome `154.0.8037.98` shape probe exercised +the original native mirror. During the explicitly bounded native interval change +from `60` to `15`, its completed count advanced from 6 to 7. The earlier mirrors +are not all attributed to one click. Final totals were seven native completions, +all targeting `chart_delivery` with IndexedDB, zero failures and zero JSON-shape +failures. The observed maximum was four entries, 49,003 batch bytes and 24,504 +bytes for one record. This sample fits the current JSON and size contracts; it +does not establish compatibility for every user's saved layouts or drawings. +The final chart remained data-ready at resolution `15`; the inspected screenshot +showed the chart and populated orderbook. Observation stopped and the temporary +browser closed. Evidence is retained in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-shape-probe-yDfadK/`. +This is real native mirror invocation and data-shape evidence, not real-page +bulk-writer validation. + +### Anonymous bulk and native-only preflight: 2026-10-07 (Beijing) + +A later independent anonymous Chrome `154.0.8037.98` run enabled the actual batch +writer through the explicit USUSDT probe scope. Its report begins at +`2026-10-06T16:24:24.572Z`, October 7 in Beijing. The pinned factory matched and +executed once. Five accepted batches committed five transactions, with eight +committed record writes and eleven unchanged writes skipped. Failed, rejected, aborted +and pending counts were zero; peaks were two pending batches and 98,006 bytes. +The one-hour chart stage observed four completed mirrors; the bounded `60` to +`15` transition then advanced completions from 4 to 5. The first four are not +all attributed to one click. + +After `writer.stop()`, selecting the four-hour interval advanced completed +mirrors from 5 to 6 and `nativeCalls` from zero to one. The complete writer +statistics remained unchanged. Each of the three stages read back four target +records and matched them to its captured dispatch snapshot. The runner required +native chart data readiness; the inspected final screenshot showed four-hour +USUSDT candles, moving averages, volume and the populated orderbook. There were +no page errors, observation stopped, and the temporary browser closed. The report +and screenshot are retained in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-bulk-probe-3YcdXL/`. +This proves bounded real-page batch execution and native continuation through +the mounted callback in an independent anonymous browser. It does not enable +the writer in the user's logged-in profile or establish blank-page recovery. + +Three earlier attempts stopped at the anonymous-page UI check before matching +the factory or dispatching a mirror. Registration text selected both a button +and a link; selecting the unique accessible button corrected that check. Those +attempts provide no evidence of a storage failure. The checked-in anonymous +runner, entry and contract now make the workflow independently repeatable, +with a fresh Chrome context, fixed-URL navigation/runtime guards, explicit failure +accounting and browser cleanup. The standalone command is documented in the +[experiment README](../experiments/binance-chart-storage/README.md). + +The origin/scope suite has 18 Node cases, the anonymous-probe contract adds four, +and native-only preflight adds 18: all 40 passed. The 37 mirror browser scenarios +also passed. These counts identify the later validation scope separately from +the earlier 105-browser/eight-Node experiment run. + +Tampermonkey delivery needs its own native-only acceptance on an authorized test page. +The official [Content Script API documentation](https://www.tampermonkey.net/documentation.php?q=content_script_api) +states that default **Content Script** mode has no real `document-start` support; +on Chrome, ordinary **UserScripts API** has the same restriction. **UserScripts +API Dynamic** injects both wrapper and script immediately and supports it. The +official [`@sandbox` documentation](https://www.tampermonkey.net/documentation.php?q=sandbox) +describes `raw` as requesting `MAIN_WORLD`, but permits another enabled sandbox +when injection there is unavailable, such as under CSP. Metadata alone therefore +cannot prove either document-start timing or MAIN_WORLD access to the native +queue. The extension must independently demonstrate both, strict pin before +first factory execution, native mirror completion and clean stop before an +optimized entrypoint can be considered. Missing evidence must reject replacement. + +The independently reviewed `native-preflight-core.js`, userscript entry and +build now generate `native-preflight.user.js` as a self-contained review artifact. +Its build asserts a four-source dependency graph with no writer or runtime loader. +For 30 seconds the core records bounded in-memory diagnostics and synchronously +returns the exact original native thunk result, retaining its array and error +identities. It does not inspect persisted values or add storage calls. Manual +stop, pagehide, scope changes and the deadline retire observation. The artifact +was neither installed nor published at that stage; its metadata and offline +tests did not establish Tampermonkey acceptance. + +The generated native-only artifact was then run through `addInitScript` in a +fresh anonymous Chrome `154.0.8037.98` context, beginning at +`2026-10-06T16:30:27.651Z` (October 7 Beijing time). It recorded one attempt, +one exact match and one factory execution, with module capture completed, four +native thunk dispatches and 15 counted entries. Failure was null and page errors +were zero. Manual stop set `active=false`, and the temporary browser closed. +The inspected screenshot showed the chart, moving averages, volume and orderbook. +Evidence is retained in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-native-artifact-probe-JSBjWC/`. +The preflight's `completed` field describes factory capture, and its dispatch +count is not an asynchronous write-commit count. This is actual built-artifact +execution through Chrome initialization-script injection, not Tampermonkey +injection or installation. The artifact was still unpublished and uninstalled +at the end of that anonymous run. + +Remaining production boundaries include verified Tampermonkey timing/MAIN_WORLD, +the user's logged-in profile and broader saved-data shapes/limits, long-running +concurrent revisions and native clear interleaving, and the blank-page root cause +and affected-profile recovery. The anonymous mounted stop case is one bounded +sample. Default scope still rejects production origins; +the explicit exact-page probe scope relies on its harness for profile isolation. +No production userscript or public install artifact imports the experiment. +The anonymous runs did not change installed scripts or extension settings. + +### Tampermonkey native-only acceptance: 2026-10-07 (Beijing) + +With explicit installation and new-tab test authorization, the native-only +`Binance Chart Mirror Native Preflight (Experiment)` version `0.0.1` was installed +under UUID `d0601269-eb03-446c-8c9a-a9836e343cb2`. The Editors create operation +returned HTTP 405 and a subsequent list showed no installation. The native +installer path then produced the unique script in the authoritative list. +Fresh MCP source readback, parsed with the repository transport parser, matched +the complete 86,589-byte artifact with SHA-256 +`7aff1d564a8cf9f21bbdb4a039db034c7a593dd846c23616d50bf40d3d856e1e`. + +A new test tab navigated to the exact USUSDT URL at `2026-10-06T18:39:55.733Z` +(02:39:55 Beijing time). Before navigation, a target-local network block was +set for the orderbook script's `adjustLeverage` endpoint because its initial +flat-account observation can otherwise change leverage. Existing user tabs were +not refreshed, and no financial control or chart interval control was clicked. + +| Observation | Result | +| --- | --- | +| First sample, page age 1,234.6 ms | Main-world diagnostic present, active and waiting | +| Page age 18,705.7 ms | One attempt, exact source match and factory execution; capture complete; chart data ready | +| Page age 45,083.2 ms | Stopped by deadline; three native dispatches, 885 total entries; no diagnostic failure | +| Page age 94,216.8 ms | Same stopped counters; chart data ready at resolution `15` | +| Queue restoration | `push` is a data property, with no accessor getter | + +The actual loaded Tampermonkey wrapper contained exactly one byte-identical +copy of the artifact. Its execution context was the top-level Binance default +world (`isDefault: true`), establishing MAIN_WORLD execution for this navigation. +The initial event buffer was truncated; a fresh Debugger enumeration on the +same loaded page recovered the script source without truncation or navigation. +The source proof therefore does not rely on a complete initial event timeline. +No extension injection setting was inspected or changed, and this single +successful early injection is not a guarantee for every future navigation. + +The inspected chart crop showed candles, indicators, annotations and volume. +Manual stop returned the already retired state. Both test-owned tabs were +closed, disposing the temporary endpoint block and CDP attachment. The separate +statistics script remains installed, restricted to the exact USUSDT page; a +fresh matching page can start another observation. Its 30-second timer is a +target duration, not a hard wall-clock cutoff under browser scheduling delays. +No batch writer was enabled in the user's profile and nothing was published. + +This passes the bounded installed-source, loaded-source, MAIN_WORLD, exact +factory capture, native dispatch and diagnostic-retirement checks. `completed` +still means module capture, while `dispatches` counts thunk entry, not resolved +native writes or committed transactions. Asynchronous completion, long-running +concurrent writes, affected-profile blank-page recovery and reliable injection +across navigations remain unverified. The evidence is retained in +`/tmp/binance-tampermonkey-native-preflight-hlIxL9/report.json`, alongside the +loaded experimental source and the inspected `chart-candles.png` crop. + +### Production successor acceptance: 2026-10-07 (Beijing) + +The user then authorized continuing through publication. The initial production +candidate used `src/binance-chart-storage/`, generated as +`scripts/binance-chart-storage.user.js` version `0.1.0`. This separate installer +was superseded by the orderbook integration below before publication. +It imports no experiment implementation. Review identified and +fixed signed-zero/key-order comparison, shared-reference cloning, unsupported +old values and transaction-creation failure handling. Unsupported work returns +to the original map only before transaction creation; genuine transaction failure +never replays. See the [production contract](binance-chart-storage-development.md) +for its atomicity and lifecycle limits. + +Validation passed all 2,145 Node/DOM tests, 126 chart-storage browser scenarios +(including 18 production writer and three production concurrency scenarios), +full test lint and affected build/syntax checks. The prior coverage inventory +test failed because its explicit installer list lacked the new artifact; the +list was extended and the entire Node/DOM suite then passed. Five production +value/connection regressions first failed against the historical writer. + +The actual generated artifact was tested in a fresh anonymous Chrome profile: +five optimized transactions committed six writes and skipped thirteen unchanged +writes, with no failure, abort or remaining work. After stop, a native interval +change produced four native destination commits while all optimizer counters +remained frozen. The inspected screenshot showed the TradingView chart. The +report is `/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-chart-storage-smoke-kwTGVT/result.json`. +An earlier smoke stopped on an HTTP 202 assertion; the corrected runner records +status and waits for semantic readiness. Its successful navigation sequence was +202 then 200. HTTP status alone remains insufficient UI evidence. + +The local Tampermonkey statistics installation was replaced in place by the +production artifact. Installed source matched all 99,037 bytes and SHA-256 +`8fc21dda5322c0465d4bf6dc5149bfd4b50ec02e885ca7134ad047d617cb0501`. +A new USUSDT test tab started at `2026-10-07T01:08:16.254Z`, with a target-local +leverage-endpoint block set before navigation. Its actual loaded wrapper +contained one identical artifact in the top-level Binance MAIN_WORLD. At page +age 198,760.1 ms, six optimized transactions had committed six writes and skipped +1,764 unchanged writes. Failed, rejected, aborted and pending counters were zero; +peak copied pending input was 3,605,478 bytes. Native chart data readiness was +true at resolution `15`, and the inspected crop showed candles and annotations. + +The test tab closed, disposing its temporary block. Existing user pages were not +refreshed, and no financial control was clicked. The final explicit stop diagnostic +contained a JavaScript syntax error before invocation, so this local run does not +claim manual-stop completion; that path passed independently in the anonymous +artifact test. Its last observed pending count was zero. The local report, loaded +source and chart crops are in `/tmp/binance-chart-storage-release-live-wLHfqz/`. +Neither live run read back saved keys or values. Persistent contents, competing +revision integrity and reload are covered by the isolated real-IDB tests. +These are bounded acceptance results, not a universal white-screen recovery or +long-running stability claim. + +### Orderbook integration acceptance: 2026-10-07 (Beijing) + +The final implementation lives in `src/binance-orderbook-trade/chart-storage/` +and ships only through the existing orderbook installer, version `2.7.218`. +The standalone production entry and build target were removed. This optimizes +the current matching Binance page's pinned TradingView mirror destination +writes; it is neither a Chrome-wide userscript optimization nor limited to +orders submitted by the orderbook. The destination database is shared by +same-origin Binance pages and may contain historical symbol records. + +The storage installer runs before orderbook business initialization. During +anonymous early-injection validation, the complete artifact exposed an existing +startup assumption: both `document.head` and `document.documentElement` could +still be absent when disabled-control styles were inserted. A regression first +reproduced the `null.appendChild` failure. The initializer now observes creation +of the HTML root, disconnects and initializes once; storage interception still +starts immediately. The original iframe and SPA orderbook behavior is retained. + +The final artifact has 661,567 bytes and SHA-256 +`f281dd8c7b5f384e6f05ef1f7e837201923a0194d7fcdebd135997ee8e766f64`. +All 2,143 Node/DOM tests, 132 chart-storage browser scenarios, test lint, +build/syntax and final independent read-only review passed. The six integration +scenarios include missing-root startup, source rejection, late injection, +home-to-trading navigation and a real iframe. + +The anonymous Chrome smoke executed this complete artifact with both HTML root +and head absent at injection. The orderbook initialized with exactly one panel +and one style element and zero page errors. The anonymous host lacked its +open/close mode anchor, so the panel remained hidden under its existing +placement contract. The inspected screenshot showed candles and the depth +overlay. Two interval changes committed five optimized transactions, six writes +and thirteen skipped writes, with no failed, rejected, aborted or pending work. +After stop, another interval change committed four native transactions and four +writes while optimizer statistics stayed frozen. Chart data readiness was true +throughout all three stages. The browser was closed. Evidence is retained in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-chart-storage-smoke-30Xntg/`. diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md index ad1c795..4fb524c 100644 --- a/docs/binance-orderbook-trade-development.md +++ b/docs/binance-orderbook-trade-development.md @@ -20,6 +20,16 @@ scripts/binance-orderbook-trade.user.js ## Runtime +### Chart mirror storage + +`chart-storage/install.js` runs before the orderbook business initializer. This +module batches the pinned TradingView mirror destination writes and skips +unchanged records. It is included in the existing orderbook installer, with no +separate script. Its top-frame guard applies only to storage interception; +source drift or late injection must leave the orderbook panel functional. +See [the storage contract](binance-chart-storage-development.md) for native +continuation, transaction boundaries and verification requirements. + ### Chart save completion and serialization `core/chart-save-coalescer.js` batches the observed default diff --git a/e2e/binance-orderbook/helpers/chart-storage-concurrency.js b/e2e/binance-orderbook/helpers/chart-storage-concurrency.js new file mode 100644 index 0000000..30778e4 --- /dev/null +++ b/e2e/binance-orderbook/helpers/chart-storage-concurrency.js @@ -0,0 +1,95 @@ +import { openProductionMirrorWriterLab } from './chart-storage-production-writer.js'; + +/** Observe real transaction creation so queued work never relies on a timed wait. */ +export async function openChartStorageConcurrency(page) { + await openProductionMirrorWriterLab(page); + await page.evaluate(async () => { + await window.__STORAGE_LAB__.native.ready(); + const nativeTransaction = IDBDatabase.prototype.transaction; + const state = { + pending: Object.create(null), + writes: [], + waiters: [], + waitForWrites(count) { + if (state.writes.length >= count) return Promise.resolve(); + return new Promise(resolve => state.waiters.push({ count, resolve })); + }, + restore() { IDBDatabase.prototype.transaction = nativeTransaction; }, + }; + IDBDatabase.prototype.transaction = function (...args) { + const transaction = Reflect.apply(nativeTransaction, this, args); + if (this.name === 'chart_futures' && transaction.mode === 'readwrite') { + const record = { outcome: 'pending' }; + state.writes.push(record); + transaction.addEventListener('complete', () => { record.outcome = 'complete'; }, { once: true }); + transaction.addEventListener('abort', () => { record.outcome = 'abort'; }, { once: true }); + for (const waiter of state.waiters.splice(0)) { + if (state.writes.length >= waiter.count) waiter.resolve(); + else state.waiters.push(waiter); + } + } + return transaction; + }; + window.__CHART_CONCURRENCY__ = state; + }); +} + +/** Return after admission creates a real transaction, while its lock stays blocked. */ +export async function queueChartMirrorBatch(page, id, entries) { + await page.evaluate(async ({ id, entries }) => { + const state = window.__CHART_CONCURRENCY__; + const mirror = window.__MIRROR_WRITER__; + const next = state.writes.length + 1; + state.pending[id] = Promise.all(mirror.writer.dispatch(window.__STORAGE_LAB__.native, entries, mirror.nativeThunk)); + await state.waitForWrites(next); + }, { id, entries }); +} + +export async function queueNativeChartClear(page, id) { + await page.evaluate(async id => { + const state = window.__CHART_CONCURRENCY__; + const next = state.writes.length + 1; + state.pending[id] = window.__STORAGE_LAB__.native.clear(); + await state.waitForWrites(next); + }, id); +} + +/** One native readonly transaction prevents the observation itself from mixing revisions. */ +export async function queueChartSnapshot(page, id) { + await page.evaluate(id => { + const transaction = window.__STORAGE_LAB__.native._dbInfo.db.transaction('keyvaluepairs', 'readonly'); + const store = transaction.objectStore('keyvaluepairs'); + const keys = store.getAllKeys(); + const values = store.getAll(); + window.__CHART_CONCURRENCY__.pending[id] = new Promise((resolve, reject) => { + transaction.addEventListener('complete', () => { + resolve(keys.result.map((key, index) => [key, values.result[index]])); + }, { once: true }); + transaction.addEventListener('abort', () => reject(transaction.error), { once: true }); + }); + }, id); +} + +export async function finishChartOperations(page, ids) { + return page.evaluate(async ids => { + const pending = window.__CHART_CONCURRENCY__.pending; + const results = await Promise.all(ids.map(id => pending[id])); + for (const id of ids) delete pending[id]; + return results; + }, ids); +} + +export async function chartConcurrencyState(page) { + return page.evaluate(() => ({ + writes: window.__CHART_CONCURRENCY__.writes.map(record => record.outcome), + stats: window.__MIRROR_WRITER__.writer.getStats(), + nativeCalls: window.__MIRROR_WRITER__.nativeCalls, + })); +} + +export async function stopChartConcurrency(page) { + await page.evaluate(async () => { + await window.__MIRROR_WRITER__.writer.stop(); + window.__CHART_CONCURRENCY__.restore(); + }); +} diff --git a/e2e/binance-orderbook/helpers/chart-storage-host-workflows.js b/e2e/binance-orderbook/helpers/chart-storage-host-workflows.js new file mode 100644 index 0000000..bbb4335 --- /dev/null +++ b/e2e/binance-orderbook/helpers/chart-storage-host-workflows.js @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { openStorageLab, STORAGE_LAB_ORIGIN } from './chart-storage-lab.js'; + +const fixtureRoot = new URL('../../../test/fixtures/binance-chart-storage/', import.meta.url); +const sha = value => createHash('sha256').update(value).digest('hex'); + +/** Keep the executable fixture tied to every exact source slice recorded by extraction. */ +async function verifiedFixture() { + const [source, manifestText] = await Promise.all([ + readFile(new URL('host-persistence.js', fixtureRoot), 'utf8'), + readFile(new URL('host-persistence-manifest.json', fixtureRoot), 'utf8'), + ]); + const manifest = JSON.parse(manifestText); + assert.equal(sha(source), manifest.fixture.sha256, 'Host persistence fixture hash changed'); + for (const fragment of manifest.fragments) { + assert.equal(sha(source.slice(...fragment.fixture_utf16_range)), fragment.sha256, fragment.label); + } + return source; +} + +/** The existing lab still blocks all external requests; this adds one exact local asset. */ +export async function openChartHostWorkflows(page, options) { + await openStorageLab(page, options); + const source = await verifiedFixture(); + await page.context().route(`${STORAGE_LAB_ORIGIN}/host-persistence.js`, route => route.fulfill({ + contentType: 'text/javascript', body: source, + })); + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/host-persistence.js` }); + await page.evaluate(() => { + let runtimeRequire; + self.webpackChunkfutures_trade_ui.push([ + ['codex-chart-host-workflows-entry'], {}, require => { runtimeRequire = require; }, + ]); + window.__CHART_HOST__ = { + tradingView: runtimeRequire('codex-chart-host-tv'), + basic: runtimeRequire('codex-chart-host-basic'), + mirror: runtimeRequire('codex-chart-host-mirror'), + }; + }); +} diff --git a/e2e/binance-orderbook/helpers/chart-storage-lab.js b/e2e/binance-orderbook/helpers/chart-storage-lab.js new file mode 100644 index 0000000..0a5e663 --- /dev/null +++ b/e2e/binance-orderbook/helpers/chart-storage-lab.js @@ -0,0 +1,235 @@ +import { readFile } from 'node:fs/promises'; + +export const STORAGE_LAB_ORIGIN = 'https://chart-storage.test'; +const configuredContexts = new WeakSet(); +const fixtureRoot = new URL('../../../test/fixtures/binance-chart-storage/', import.meta.url); +const adapterUrl = new URL('../../../experiments/binance-chart-storage/adapter.js', import.meta.url); +const captureUrl = new URL('../../../experiments/binance-chart-storage/capture.js', import.meta.url); +const bootstrapUrl = new URL('../../../experiments/binance-chart-storage/bootstrap.js', import.meta.url); +const preflightUrl = new URL('../../../experiments/binance-chart-storage/preflight.js', import.meta.url); +const pageHtml = ` +Storage Lab

Storage Lab

+

Isolated Chromium · real IndexedDB · public localForage fixture · all external requests blocked

+
Ready for an isolated chart-storage experiment.
+ +`; + +/** Count native database operations without replacing the storage workflow. */ +function installNativeObservation() { + const nativeTransaction = IDBDatabase.prototype.transaction; + const nativePut = IDBObjectStore.prototype.put; + const nativeGet = IDBObjectStore.prototype.get; + const nativeOpen = IDBFactory.prototype.open; + const isTarget = name => name === 'chart_futures' || name === 'chart_delivery'; + const connections = new Map(); + const state = { + enabled: false, + abortNextPut: false, + transactions: 0, + puts: 0, + gets: 0, + opens: 0, + completed: 0, + aborted: 0, + pending: 0, + versionChanges: 0, + }; + IDBDatabase.prototype.transaction = function (...args) { + const transaction = nativeTransaction.apply(this, args); + if (isTarget(this.name) && !connections.has(this)) { + connections.set(this, this.version); + this.addEventListener('versionchange', () => { + if (state.enabled) state.versionChanges += 1; + }); + } + if (state.enabled && isTarget(this.name)) { + state.transactions += 1; + state.pending += 1; + transaction.addEventListener('complete', () => { + state.completed += 1; + state.pending -= 1; + }, { once: true }); + transaction.addEventListener('abort', () => { + state.aborted += 1; + state.pending -= 1; + }, { once: true }); + } + return transaction; + }; + IDBObjectStore.prototype.put = function (...args) { + const request = nativePut.apply(this, args); + if (state.enabled && isTarget(this.transaction.db.name)) state.puts += 1; + if (state.abortNextPut && isTarget(this.transaction.db.name)) { + state.abortNextPut = false; + request.addEventListener('success', () => this.transaction.abort(), { once: true }); + } + return request; + }; + IDBObjectStore.prototype.get = function (...args) { + if (state.enabled && isTarget(this.transaction.db.name)) state.gets += 1; + return nativeGet.apply(this, args); + }; + IDBFactory.prototype.open = function (...args) { + if (state.enabled && isTarget(args[0])) state.opens += 1; + return nativeOpen.apply(this, args); + }; + window.__STORAGE_NATIVE__ = { + snapshot: () => ({ ...state }), + reset() { + Object.assign(state, { + enabled: true, abortNextPut: false, transactions: 0, puts: 0, + gets: 0, opens: 0, completed: 0, aborted: 0, pending: 0, + versionChanges: 0, + }); + }, + abortNextPut() { state.abortNextPut = true; }, + closeTarget(name) { + let count = 0; + for (const database of connections.keys()) { + if (database.name === name) { + database.close(); + count += 1; + } + } + return count; + }, + version(name) { + return Math.max(...[...connections].filter(([database]) => database.name === name).map(([, version]) => version)); + }, + }; +} + +/** The context owns every request, including runtime attempts to load remote chunks. */ +async function configureContext(context) { + if (configuredContexts.has(context)) return; + await context.route('**/*', async route => { + const url = new URL(route.request().url()); + if (url.origin !== STORAGE_LAB_ORIGIN) return route.abort('blockedbyclient'); + if (url.pathname === '/' || url.pathname === '/empty') { + return route.fulfill({ + contentType: 'text/html', + body: url.pathname === '/empty' + ? pageHtml.replace('', '') + : url.searchParams.has('capture') + ? pageHtml.replace('', '') + : pageHtml, + }); + } + const asset = { + '/webpack-runtime.js': new URL('webpack-runtime.js', fixtureRoot), + '/localforage-module.js': new URL('localforage-module.js', fixtureRoot), + '/adapter.js': adapterUrl, + '/capture.js': captureUrl, + '/bootstrap.js': bootstrapUrl, + '/preflight.js': preflightUrl, + }[url.pathname]; + if (!asset) return route.abort('blockedbyclient'); + return route.fulfill({ contentType: 'text/javascript', body: await readFile(asset, 'utf8') }); + }); + await context.addInitScript(installNativeObservation); + configuredContexts.add(context); +} + +/** Registration precedes the host's first require, preserving the real runtime cache. */ +export async function initializeStorageLab(page, { install = true, options = {}, captureInstalled = false } = {}) { + await page.evaluate(async ({ install, options, captureInstalled }) => { + let runtimeRequire; + self.webpackChunkfutures_trade_ui.push([ + ['codex-chart-storage-host'], {}, require => { runtimeRequire = require; }, + ]); + if (typeof runtimeRequire !== 'function') throw new Error('The real runtime did not expose require'); + const originalFactory = runtimeRequire.m[43917]; + let factoryExecutions = 0; + runtimeRequire.m[43917] = function (...args) { + factoryExecutions += 1; + return originalFactory.apply(this, args); + }; + const hostLocalforage = runtimeRequire(43917); + const nativeCreateInstance = captureInstalled + ? window.__STORAGE_CAPTURE_NATIVE_CREATE__ + : hostLocalforage.createInstance; + const native = nativeCreateInstance.call(hostLocalforage, { + name: 'chart_futures', storeName: 'keyvaluepairs', driver: hostLocalforage.INDEXEDDB, + }); + window.__STORAGE_LAB__ = { + localforage: hostLocalforage, + nativeCreateInstance, + native, + cacheIdentity() { + return { + sameExport: runtimeRequire(43917) === hostLocalforage, + factoryExecutions, + }; + }, + create(name = 'chart_futures', extra = {}) { + return hostLocalforage.createInstance({ + name, storeName: 'keyvaluepairs', driver: hostLocalforage.INDEXEDDB, ...extra, + }); + }, + controller: captureInstalled + ? window.__STORAGE_CAPTURE_CONTROLLER__ + : install ? (await import('/adapter.js')).installChartStoragePrototype(hostLocalforage, options) : null, + }; + }, { install, options, captureInstalled }); +} + +export async function openStorageLab(page, { capture = false, captureInstall = false, initialize = true, ...options } = {}) { + await configureContext(page.context()); + await page.goto(`${STORAGE_LAB_ORIGIN}/${capture ? '?capture=1' : ''}`); + if (capture) { + await page.evaluate(async ({ captureInstall, options }) => { + const { observeChartStorageModule } = await import('/capture.js'); + window.__STORAGE_CAPTURE_ORIGINAL_PUSH__ = self.webpackChunkfutures_trade_ui.push; + if (captureInstall) { + const { installChartStoragePrototype } = await import('/adapter.js'); + window.__STORAGE_CAPTURE__ = observeChartStorageModule(undefined, { + onCapture(localforage) { + window.__STORAGE_CAPTURE_NATIVE_CREATE__ = localforage.createInstance; + window.__STORAGE_CAPTURE_CONTROLLER__ = installChartStoragePrototype(localforage, options); + }, + }); + } else { + window.__STORAGE_CAPTURE__ = observeChartStorageModule(); + } + }, { captureInstall, options: options.options || {} }); + if (!initialize) { + await page.evaluate(() => { + const queue = self.webpackChunkfutures_trade_ui; + const observedPush = queue.push; + window.__STORAGE_CAPTURE_OBSERVED_PUSH__ = observedPush; + queue.push = function (chunk) { + if (Object.hasOwn(chunk[1], '43917')) window.__STORAGE_ORIGINAL_MODULE_FACTORY__ = chunk[1][43917]; + return observedPush.call(this, chunk); + }; + }); + } + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/localforage-module.js` }); + if (!initialize) { + await page.evaluate(() => { + self.webpackChunkfutures_trade_ui.push = window.__STORAGE_CAPTURE_OBSERVED_PUSH__; + }); + } + } + if (initialize) await initializeStorageLab(page, { ...options, captureInstalled: captureInstall }); +} + +/** Failure scenarios register their own bounded module boundary into the real runtime. */ +export async function openStorageRuntime(page) { + await configureContext(page.context()); + await page.goto(`${STORAGE_LAB_ORIGIN}/?capture=1`); +} + +/** Startup integration scenarios install capture before either real host script. */ +export async function openEmptyStorageLab(page) { + await configureContext(page.context()); + await page.goto(`${STORAGE_LAB_ORIGIN}/empty`); +} + +export async function displayStorageResult(page, result) { + await page.locator('#result').evaluate((element, value) => { + element.textContent = JSON.stringify(value, null, 2); + }, result); +} diff --git a/e2e/binance-orderbook/helpers/chart-storage-mirror-scoped.js b/e2e/binance-orderbook/helpers/chart-storage-mirror-scoped.js new file mode 100644 index 0000000..4956b3f --- /dev/null +++ b/e2e/binance-orderbook/helpers/chart-storage-mirror-scoped.js @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { openEmptyStorageLab, STORAGE_LAB_ORIGIN } from './chart-storage-lab.js'; +import { openChartHostWorkflows } from './chart-storage-host-workflows.js'; + +const fixtureRoot = new URL('../../../test/fixtures/binance-chart-storage/', import.meta.url); +const experimentRoot = new URL('../../../experiments/binance-chart-storage/', import.meta.url); + +/** The existing lab route owns and blocks every request outside these exact assets. */ +async function addScopedAssets(context, includeWriter) { + const manifest = JSON.parse(await readFile(new URL('mirror-scoped-manifest.json', fixtureRoot), 'utf8')); + for (const [asset, root] of [ + ['mirror-module.js', experimentRoot], ['mirror-scoped-callback.js', fixtureRoot], + ...(includeWriter ? [['mirror-writer.js', experimentRoot]] : []), + ]) { + const source = await readFile(new URL(asset, root), 'utf8'); + if (asset !== 'mirror-writer.js') { + assert.equal(createHash('sha256').update(source).digest('hex'), manifest.outputs[asset].sha256, `${asset} hash changed`); + } + await context.route(`${STORAGE_LAB_ORIGIN}/${asset}`, route => route.fulfill({ contentType: 'text/javascript', body: source })); + } +} + +export async function openMirrorBootstrapLab(page) { + await openEmptyStorageLab(page); + await addScopedAssets(page.context(), false); +} + +/** Execute original host storage business functions against unmodified localForage instances. */ +export async function openChartMirrorScoped(page, { mode = 'scoped' } = {}) { + await openChartHostWorkflows(page, { install: false }); + await addScopedAssets(page.context(), mode === 'scoped'); + await page.evaluate(async mode => { + const { createMirrorCallbacks } = await import('/mirror-scoped-callback.js'); + let runtimeRequire; + self.webpackChunkfutures_trade_ui.push([['mirror-scoped-callback-entry'], {}, require => { runtimeRequire = require; }]); + const localforage = window.__STORAGE_LAB__.localforage; + const state = { localforage, nativeCreateInstance: localforage.createInstance, writer: null, dispatches: 0 }; + if (mode === 'scoped') state.writer = (await import('/mirror-writer.js')).createChartMirrorWriter(); + const callbacks = createMirrorCallbacks(runtimeRequire, (target, entries, nativeThunk) => { + state.dispatches += 1; + return state.writer.dispatch(target, entries, nativeThunk); + }); + state.create = mode === 'native' ? callbacks.native : callbacks.patched; + window.__MIRROR_SCOPED__ = state; + }, mode); +} diff --git a/e2e/binance-orderbook/helpers/chart-storage-mirror-writer.js b/e2e/binance-orderbook/helpers/chart-storage-mirror-writer.js new file mode 100644 index 0000000..5232329 --- /dev/null +++ b/e2e/binance-orderbook/helpers/chart-storage-mirror-writer.js @@ -0,0 +1,57 @@ +import { readFile } from 'node:fs/promises'; +import { openStorageLab, STORAGE_LAB_ORIGIN } from './chart-storage-lab.js'; + +/** Route only the new writer; the existing lab owns every other request. */ +export async function openMirrorWriterLab(page, { limits = {} } = {}) { + await openStorageLab(page, { install: false }); + await page.route(`${STORAGE_LAB_ORIGIN}/mirror-writer.js`, async route => { + await route.fulfill({ + contentType: 'text/javascript', + body: await readFile(new URL('../../../experiments/binance-chart-storage/mirror-writer.js', import.meta.url), 'utf8'), + }); + }); + await page.evaluate(async limits => { + const { createChartMirrorWriter } = await import('/mirror-writer.js'); + window.__MIRROR_WRITER__ = { + writer: createChartMirrorWriter({ limits }), + nativeCalls: 0, + nativeThunk() { + window.__MIRROR_WRITER__.nativeCalls += 1; + throw new Error('An active mirror batch must not replay its native expression'); + }, + }; + }, limits); +} + +/** Native request events retain a real write lock without a synthetic clock. */ +export async function holdMirrorWriteLock(page) { + await page.evaluate(() => { + const transaction = window.__STORAGE_LAB__.native._dbInfo.db.transaction('keyvaluepairs', 'readwrite'); + const store = transaction.objectStore('keyvaluepairs'); + const pulses = []; + const lock = { + released: false, + progress: () => new Promise(resolve => pulses.push(resolve)), + completion: new Promise((resolve, reject) => { + transaction.addEventListener('complete', () => resolve('complete'), { once: true }); + transaction.addEventListener('abort', () => reject(transaction.error), { once: true }); + }), + }; + function hold() { + store.get('#TV_SYMBOL-LOCK').onsuccess = () => { + for (const resolve of pulses.splice(0)) resolve(); + if (!lock.released) hold(); + }; + } + window.__MIRROR_WRITE_LOCK__ = lock; + hold(); + }); + await page.evaluate(() => window.__MIRROR_WRITE_LOCK__.progress()); +} + +export async function releaseMirrorWriteLock(page) { + await page.evaluate(async () => { + window.__MIRROR_WRITE_LOCK__.released = true; + await window.__MIRROR_WRITE_LOCK__.completion; + }); +} diff --git a/e2e/binance-orderbook/helpers/chart-storage-production-writer.js b/e2e/binance-orderbook/helpers/chart-storage-production-writer.js new file mode 100644 index 0000000..6f76080 --- /dev/null +++ b/e2e/binance-orderbook/helpers/chart-storage-production-writer.js @@ -0,0 +1,33 @@ +import { readFile } from 'node:fs/promises'; +import { openStorageLab, STORAGE_LAB_ORIGIN } from './chart-storage-lab.js'; + +/** Exercise the production writer against the captured native localForage driver. */ +export async function openProductionMirrorWriterLab(page, { limits = {} } = {}) { + await openStorageLab(page, { install: false }); + await page.route(`${STORAGE_LAB_ORIGIN}/{production-writer,json}.js`, async route => { + const filename = new URL(route.request().url()).pathname === '/production-writer.js' + ? 'mirror-writer.js' : 'json.js'; + await route.fulfill({ + contentType: 'text/javascript', + body: await readFile(new URL(`../../../src/binance-orderbook-trade/chart-storage/${filename}`, import.meta.url), 'utf8'), + }); + }); + await page.evaluate(async limits => { + const { createChartMirrorWriter } = await import('/production-writer.js'); + const state = { + writer: createChartMirrorWriter({ limits }), + nativeCalls: 0, + nativeThunk() { + state.nativeCalls += 1; + throw new Error('An optimized mirror must not replay its native expression'); + }, + dispatch(entries, target = window.__STORAGE_LAB__.native) { + return state.writer.dispatch(target, entries, () => { + state.nativeCalls += 1; + return entries.map(([key, value]) => target.setItem(key, value)); + }); + }, + }; + window.__MIRROR_WRITER__ = state; + }, limits); +} diff --git a/e2e/binance-orderbook/specs/chart-storage-concurrency.pw.js b/e2e/binance-orderbook/specs/chart-storage-concurrency.pw.js new file mode 100644 index 0000000..8b46be2 --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-concurrency.pw.js @@ -0,0 +1,202 @@ +import { test, expect, reloadPageWithCoverage } from '../test.js'; +import { holdMirrorWriteLock, releaseMirrorWriteLock } from '../helpers/chart-storage-mirror-writer.js'; +import { initializeStorageLab, openStorageLab } from '../helpers/chart-storage-lab.js'; +import { + openChartStorageConcurrency, + queueChartMirrorBatch, + queueNativeChartClear, + queueChartSnapshot, + finishChartOperations, + chartConcurrencyState, + stopChartConcurrency, +} from '../helpers/chart-storage-concurrency.js'; + +const chartKeys = ['#TV_SYMBOL-BTCUSDT', '#TV_SYMBOL-ETHUSDT', 'myTradingView', 'myTradingView.layout']; +const entriesFor = (writer, revision) => chartKeys.map((key, index) => [key, { writer, revision, index, points: [revision, index] }]); +const sortedEntries = entries => [...entries].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0); + +test('user reads complete committed revisions through twelve rounds of three-tab contention and reload', async ({ page, context }) => { + // Given three independent writers share native IndexedDB and an unrelated target record. + const tabs = [page, await context.newPage(), await context.newPage()]; + const preserved = [['candlestick-setting', { theme: 'target' }], ['#TV_SYMBOL-TARGET-ONLY', ['keep']]]; + for (const tab of tabs) await openChartStorageConcurrency(tab); + await page.evaluate(async entries => { + for (const [key, value] of entries) await window.__STORAGE_LAB__.native.setItem(key, value); + }, preserved); + const rounds = []; + try { + // When all three revision batches and intervening native reads queue behind a real write lock each round. + for (let round = 0; round < 12; round += 1) { + await holdMirrorWriteLock(page); + const order = [0, 1, 2].map(offset => (round + offset) % tabs.length); + const batches = order.map(writer => entriesFor(writer, round + 1)); + let blocked; + try { + for (let index = 0; index < order.length; index += 1) { + const entries = index % 2 === 0 ? batches[index] : [...batches[index]].reverse(); + await queueChartMirrorBatch(tabs[order[index]], 'batch', entries); + await queueChartSnapshot(page, `snapshot-${index}`); + } + blocked = await Promise.all(tabs.map(chartConcurrencyState)); + } finally { + await releaseMirrorWriteLock(page); + } + const outcomes = await Promise.all(tabs.map(tab => finishChartOperations(tab, ['batch']))); + const snapshots = await finishChartOperations(page, ['snapshot-0', 'snapshot-1', 'snapshot-2']); + await queueChartSnapshot(tabs[order[0]], 'final'); + const [final] = await finishChartOperations(tabs[order[0]], ['final']); + rounds.push({ blocked, outcomes, snapshots, final, batches }); + } + const states = await Promise.all(tabs.map(chartConcurrencyState)); + + // Then every observed commit is its entire batch, all tabs finish each round, and no writer replays natively. + expect(rounds).toHaveLength(12); + for (const round of rounds) { + expect(round.blocked.map(state => state.stats.pendingBatches)).toEqual([1, 1, 1]); + expect(round.outcomes).toEqual([[[undefined]], [[undefined]], [[undefined]]]); + expect(round.snapshots).toEqual(round.batches.map(batch => sortedEntries([...preserved, ...batch]))); + expect(round.final).toEqual(sortedEntries([...preserved, ...round.batches[2]])); + } + for (const state of states) { + expect(state.stats).toMatchObject({ acceptedBatches: 12, committedTransactions: 12, committedWrites: 48, pendingBatches: 0, pendingBytes: 0, failedBatches: 0 }); + expect(state.writes.every(outcome => outcome === 'complete')).toBe(true); + expect(state.nativeCalls).toBe(0); + } + + // When an independent native reader reloads after the last competing batch has committed. + const reader = await context.newPage(); + try { + await openStorageLab(reader, { install: false }); + await reloadPageWithCoverage(reader); + await initializeStorageLab(reader, { install: false }); + const reloaded = await reader.evaluate(async () => { + const entries = []; + await window.__STORAGE_LAB__.native.iterate((value, key) => { entries.push([key, value]); }); + return entries; + }); + + // Then unchanged native iteration retrieves the exact last committed revision and preserved records. + expect(reloaded).toEqual(sortedEntries([...preserved, ...rounds.at(-1).batches[2]])); + } finally { + await reader.close(); + } + } finally { + for (const tab of tabs) await stopChartConcurrency(tab); + for (const tab of tabs.slice(1)) await tab.close(); + } +}); + +test('user observes batch clear batch commits in their native cross-tab transaction order', async ({ page, context }) => { + // Given ready same-origin tabs share a target containing existing chart and Basic records. + const tabs = [page, await context.newPage(), await context.newPage()]; + for (const tab of tabs) await openChartStorageConcurrency(tab); + await page.evaluate(() => window.__STORAGE_LAB__.native.setItem('candlestick-setting', { theme: 'old' })); + const before = entriesFor('before-clear', 1); + const after = entriesFor('after-clear', 2); + await holdMirrorWriteLock(page); + let blocked; + try { + // When a mirror, native clear and another tab's mirror queue with native reads between them. + try { + await queueChartMirrorBatch(tabs[1], 'before', before); + await queueChartSnapshot(page, 'before'); + await queueNativeChartClear(page, 'clear'); + await queueChartSnapshot(page, 'cleared'); + await queueChartMirrorBatch(tabs[2], 'after', after); + await queueChartSnapshot(page, 'after'); + blocked = await Promise.all(tabs.map(chartConcurrencyState)); + } finally { + await releaseMirrorWriteLock(page); + } + const [snapshots, first, last] = await Promise.all([ + finishChartOperations(page, ['before', 'clear', 'cleared', 'after']), + finishChartOperations(tabs[1], ['before']), + finishChartOperations(tabs[2], ['after']), + ]); + const states = await Promise.all(tabs.map(chartConcurrencyState)); + + // Then clear removes every earlier record before the later complete revision repopulates the target. + expect(blocked.map(state => state.stats.pendingBatches)).toEqual([0, 1, 1]); + expect(blocked.map(state => state.writes.filter(outcome => outcome === 'pending').length)).toEqual([2, 1, 1]); + expect(snapshots).toEqual([sortedEntries([['candlestick-setting', { theme: 'old' }], ...before]), undefined, [], sortedEntries(after)]); + expect(first).toEqual([[undefined]]); + expect(last).toEqual([[undefined]]); + expect(states.map(state => state.stats.committedTransactions)).toEqual([0, 1, 1]); + expect(states.map(state => state.nativeCalls)).toEqual([0, 0, 0]); + } finally { + for (const tab of tabs) await stopChartConcurrency(tab); + for (const tab of tabs.slice(1)) await tab.close(); + } +}); + +test('user keeps real native values and callbacks after stop drains its last optimized batch', async ({ page }) => { + // Given an accepted complete revision waits for the target's real write lock. + await openChartStorageConcurrency(page); + await holdMirrorWriteLock(page); + await queueChartMirrorBatch(page, 'accepted', entriesFor('optimized', 1)); + let blocked; + try { + // When stop closes admission and the continuation invokes unchanged native setItem promises and callbacks. + try { + blocked = await page.evaluate(entries => { + const state = window.__CHART_CONCURRENCY__; + const writer = window.__MIRROR_WRITER__.writer; + state.events = []; + state.callbacks = []; + state.nativeCalls = 0; + state.stopped = false; + state.stopping = writer.stop(); + state.stopping.then(() => { state.stopped = true; state.events.push('stopped'); }); + state.pending.tail = Promise.all(writer.dispatch(window.__STORAGE_LAB__.native, entries, () => { + state.nativeCalls += 1; + state.events.push('native-thunk'); + return entries.map(([key, value]) => window.__STORAGE_LAB__.native.setItem(key, value, (error, result) => { + state.callbacks.push({ key, error, value: result }); + })); + })); + return { stopped: state.stopped, events: state.events, callbacks: state.callbacks, nativeCalls: state.nativeCalls, stats: writer.getStats() }; + }, entriesFor('native-draining', 2)); + } finally { + await releaseMirrorWriteLock(page); + } + const [accepted, tail] = await finishChartOperations(page, ['accepted', 'tail']); + await queueChartSnapshot(page, 'drained'); + const [drained] = await finishChartOperations(page, ['drained']); + const continuation = await page.evaluate(async entries => { + const state = window.__CHART_CONCURRENCY__; + const writer = window.__MIRROR_WRITER__.writer; + await state.stopping; + const atStop = writer.getStats(); + let nativeResult; + const dispatched = writer.dispatch(null, null, () => { + state.nativeCalls += 1; + nativeResult = entries.map(([key, value]) => window.__STORAGE_LAB__.native.setItem(key, value, (error, result) => { + state.callbacks.push({ key, error, value: result }); + })); + return nativeResult; + }); + const exactArray = dispatched === nativeResult; + const exactPromises = dispatched.every((promise, index) => promise === nativeResult[index]); + return { values: await Promise.all(dispatched), callbacks: state.callbacks, nativeCalls: state.nativeCalls, + events: state.events, stopped: state.stopped, exactArray, exactPromises, atStop, after: writer.getStats() }; + }, entriesFor('native-stopped', 3)); + await queueChartSnapshot(page, 'final'); + const [final] = await finishChartOperations(page, ['final']); + + // Then native continuations persist both revisions and preserve callbacks without accepting another optimized batch. + expect(blocked).toMatchObject({ stopped: false, events: [], callbacks: [], nativeCalls: 0, stats: { acceptedBatches: 1, pendingBatches: 1, committedTransactions: 0 } }); + expect(accepted).toEqual([undefined]); + expect(tail).toEqual([entriesFor('native-draining', 2).map(([, value]) => value)]); + expect(drained).toEqual(sortedEntries(entriesFor('native-draining', 2))); + expect(continuation.values).toEqual(entriesFor('native-stopped', 3).map(([, value]) => value)); + expect(continuation.callbacks).toEqual([ + ...entriesFor('native-draining', 2), ...entriesFor('native-stopped', 3), + ].map(([key, value]) => ({ key, error: null, value }))); + expect(continuation).toMatchObject({ events: ['stopped', 'native-thunk'], nativeCalls: 2, stopped: true, exactArray: true, exactPromises: true }); + expect(continuation.atStop).toMatchObject({ acceptedBatches: 1, committedTransactions: 1, committedWrites: 4, pendingBatches: 0, pendingBytes: 0 }); + expect(continuation.after).toEqual(continuation.atStop); + expect(final).toEqual(sortedEntries(entriesFor('native-stopped', 3))); + } finally { + await stopChartConcurrency(page); + } +}); diff --git a/e2e/binance-orderbook/specs/chart-storage-host-workflows.pw.js b/e2e/binance-orderbook/specs/chart-storage-host-workflows.pw.js new file mode 100644 index 0000000..c317373 --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-host-workflows.pw.js @@ -0,0 +1,378 @@ +import { test, expect } from '../test.js'; +import { openChartHostWorkflows } from '../helpers/chart-storage-host-workflows.js'; + +function chart(chartId, symbol, drawing = true) { + const mainId = `main-${symbol}`; + return { + chartId, + panes: [{ + mainSourceId: mainId, + sources: [ + { id: mainId, type: 'MainSeries', state: { symbol } }, + ...(drawing ? [{ + id: `drawing-${symbol}`, type: 'LineToolTrendLine', ownerSource: mainId, + state: { symbol }, points: [{ time: 1000, price: 20 }, { time: 2000, price: 25 }], + }] : []), + ], + }], + }; +} + +for (const install of [false, true]) { + const mode = install ? 'adapter' : 'native'; + + test(`user restores staged chart saves and their native cross-database mirror with ${mode}`, async ({ page }, testInfo) => { + // Given both databases have independent settings and the source retains an older layout chart. + await openChartHostWorkflows(page, { install }); + const oldChart = chart('1', 'SOLUSDT', false); + const first = { layout: '2h', revision: 1, charts: [chart('2', 'ETHUSDT'), chart('0', 'BTCUSDT')] }; + await page.evaluate(async ({ oldChart, first }) => { + const { localforage } = window.__STORAGE_LAB__; + const storage = localforage.createInstance({ name: 'chart_futures' }); + const delivery = localforage.createInstance({ name: 'chart_delivery' }); + await storage.setItem('myTradingView.layout', [oldChart]); + await storage.setItem('candlestick-setting', { theme: 'source-basic' }); + await delivery.setItem('candlestick-setting', { theme: 'delivery-basic' }); + await delivery.setItem('#TV_SYMBOL-DESTINATION-ONLY', []); + const widget = { activeChartIndex: () => 1 }; + await window.__CHART_HOST__.tradingView.saveCustomSettings({ storage, key: 'myTradingView', widget }); + const mirror = window.__CHART_HOST__.mirror.create('chart_futures'); + window.__HOST_SCENARIO__ = { + storage, delivery, first, + save: window.__CHART_HOST__.tradingView.createSaveCallback(storage, 'myTradingView', widget, mirror), + }; + window.__STORAGE_NATIVE__.reset(); + }, { oldChart, first }); + + // When the original host callback saves four iframe snapshots and awaits its original mirror callback. + const result = await page.evaluate(async () => { + const state = window.__HOST_SCENARIO__; + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + try { + for (const revision of [1, 1, 2, 2]) { + const snapshot = iframe.contentWindow.JSON.parse(JSON.stringify({ ...state.first, revision })); + await state.save(snapshot); + } + } finally { + iframe.remove(); + } + const metrics = window.__STORAGE_NATIVE__.snapshot(); + const { localforage, controller } = window.__STORAGE_LAB__; + const source = localforage.createInstance({ name: 'chart_futures' }); + const target = localforage.createInstance({ name: 'chart_delivery' }); + const load = storage => window.__CHART_HOST__.tradingView.load({ storage, key: 'myTradingView' }); + return { + source: await load(source), target: await load(target), + sourceBasic: await source.getItem('candlestick-setting'), targetBasic: await target.getItem('candlestick-setting'), + targetOnly: await target.getItem('#TV_SYMBOL-DESTINATION-ONLY'), + sourceKeys: (await source.keys()).sort(), targetKeys: (await target.keys()).sort(), + snapshot: await source.getItem('myTradingView'), metrics, + sourceDriver: source.driver(), targetDriver: target.driver(), + pending: controller ? controller.getStats().pendingOperations : null, + }; + }); + await testInfo.attach(`host-save-mirror-${mode}.json`, { + body: JSON.stringify(result.metrics, null, 2), contentType: 'application/json', + }); + + // Then rebuilt instances restore charts and drawings while unrelated settings remain local to each database. + const expected = { + chartSave: { layout: '2h', revision: 2, charts: [chart('0', 'BTCUSDT'), oldChart, chart('2', 'ETHUSDT')] }, + customSettings: { activeChartIndex: 1 }, + }; + expect(result.source).toEqual(expected); + expect(result.target).toEqual(expected); + expect(result.snapshot).toEqual({ layout: '2h', revision: 2, charts: [chart('2', 'ETHUSDT', false), chart('0', 'BTCUSDT', false)] }); + expect(result.sourceBasic).toEqual({ theme: 'source-basic' }); + expect(result.targetBasic).toEqual({ theme: 'delivery-basic' }); + expect(result.targetOnly).toEqual([]); + const sourceKeys = ['#TV_SYMBOL-BTCUSDT', '#TV_SYMBOL-ETHUSDT', 'candlestick-setting', 'myTradingView', 'myTradingView.customSettings', 'myTradingView.layout']; + expect(result.sourceKeys).toEqual(sourceKeys); + expect(result.targetKeys).toEqual([...sourceKeys, '#TV_SYMBOL-DESTINATION-ONLY'].sort()); + expect(result.sourceDriver).toBe('asyncStorage'); + expect(result.targetDriver).toBe('asyncStorage'); + expect(result.metrics.transactions).toBe(install ? 28 : 64); + expect(result.metrics.puts).toBe(install ? 11 : 36); + expect(result.metrics.aborted).toBe(0); + expect(result.metrics.pending).toBe(0); + expect(result.pending).toBe(install ? 0 : null); + }); + + test(`user removes a symbol's last drawing without reviving it on load with ${mode}`, async ({ page }) => { + // Given the original save workflow has persisted a drawing for each of two symbols. + await openChartHostWorkflows(page, { install }); + await page.evaluate(async save => { + const storage = window.__STORAGE_LAB__.localforage.createInstance({ name: 'chart_futures' }); + window.__HOST_SCENARIO__ = { storage }; + await window.__CHART_HOST__.tradingView.save({ storage, save, key: 'myTradingView', widget: {} }); + }, { layout: '2h', charts: [chart('0', 'BTCUSDT'), chart('1', 'ETHUSDT')] }); + + // When a new snapshot contains no BTC drawing and a newly created instance loads the saved layout. + const snapshot = { layout: '2h', charts: [chart('0', 'BTCUSDT', false), chart('1', 'ETHUSDT')] }; + const result = await page.evaluate(async save => { + const storage = window.__HOST_SCENARIO__.storage; + await window.__CHART_HOST__.tradingView.save({ storage, save, key: 'myTradingView', widget: {} }); + const reopened = window.__STORAGE_LAB__.localforage.createInstance({ name: 'chart_futures' }); + return { + btc: await reopened.getItem('#TV_SYMBOL-BTCUSDT'), eth: await reopened.getItem('#TV_SYMBOL-ETHUSDT'), + loaded: await window.__CHART_HOST__.tradingView.load({ storage: reopened, key: 'myTradingView' }), + }; + }, snapshot); + + // Then an explicit empty drawing list prevents resurrection while the other symbol's drawing survives. + expect(result.btc).toEqual([]); + expect(result.eth).toEqual([chart('1', 'ETHUSDT').panes[0].sources[1]]); + expect(result.loaded).toEqual({ chartSave: snapshot, customSettings: null }); + }); + + test(`user restores all six Basic storage categories through original host functions with ${mode}`, async ({ page }) => { + // Given a fresh Basic storage instance receives the JSON values produced at its host storage boundary. + await openChartHostWorkflows(page, { install }); + const values = { + annotations: [{ id: 'CANDLE', annotations: [{ + id: 'basic-line', type: 'trend', data: [{ time: 1000, value: 20 }, { time: 2000, value: 25 }], + }] }], + indicators: [{ id: 'CANDLE', indicators: [{ id: 'MA', categories: ['main'] }], weight: 1 }], + indicatorSettings: { MA: { lengths: [7, 25, 99] } }, + settings: { '@colorTable': { 'candle.upBarColor': '#00ff00' }, '@watermark': 'test-watermark', background: '#101010', grid: { show: true } }, + annotationConfig: { trend: { lineWidth: 2, color: '#ffffff' } }, + compareSymbolSettings: { ETHUSDT: { color: '#ff9900', visible: true } }, + }; + + // When all original Basic save methods persist their categories and a new instance loads them. + const result = await page.evaluate(async values => { + const create = () => window.__CHART_HOST__.basic.create({ namespace: 'chart_futures', symbol: 'BTCUSDT' }); + const storage = create(); + await Promise.all([ + storage.saveAnnotations(values.annotations, { interval: '15m' }), + storage.saveIndicators(values.indicators), storage.saveIndicatorSettings(values.indicatorSettings), + storage.saveSettings(values.settings), storage.saveAnnotationConfig(values.annotationConfig), + storage.saveCompareSymbolSettings(values.compareSymbolSettings), + ]); + const raw = window.__STORAGE_LAB__.localforage.createInstance({ name: 'chart_futures' }); + return { + loaded: await create().load({ interval: '15m' }), keys: (await raw.keys()).sort(), + annotations: await raw.getItem('BASIC_KLINE_BTCUSDT'), settings: await raw.getItem('candlestick-setting'), + }; + }, values); + + // Then actual annotation conversion and settings exclusions survive a complete six-category reload. + const annotations = [{ id: 'CANDLE', annotations: [{ + id: 'basic-line', type: 'trend', data: { p1: { time: 1000, value: 20 }, p2: { time: 2000, value: 25 } }, + }] }]; + expect(result.annotations).toEqual(annotations); + expect(result.settings).toEqual({ background: '#101010', grid: { show: true } }); + expect(result.keys).toEqual([ + 'BASIC_KLINE_BTCUSDT', 'BASIC_KLINE_COMPARE_SYMBOL_SETTINGS', 'BASIC_KLINE_INDICATOR', + 'candlestick-indicator-setting', 'candlestick-setting', 'original-candlestick', + ]); + expect(result.loaded).toEqual({ + storeData: [{ ...values.indicators[0], annotations: annotations[0].annotations }], + indicatorSetting: values.indicatorSettings, setting: { background: '#101010', grid: { show: true } }, + annotationConfig: values.annotationConfig, compareSymbolSettings: values.compareSymbolSettings, + }); + }); + + test(`user loads original Basic JSON symbol keys through the native migration with ${mode}`, async ({ page }) => { + // Given the real store contains only the host's legacy JSON symbol key and explicit indicator data. + await openChartHostWorkflows(page, { install }); + const oldKey = 'BASIC_KLINE_{"pair":"SOLUSDT","contractType":"PERPETUAL"}'; + const annotations = [{ type: 'CANDLE', annotations: [{ type: 'trend', data: { time: 1000, value: 20 } }] }]; + await page.evaluate(async ({ oldKey, annotations }) => { + const raw = window.__STORAGE_LAB__.native; + await raw.setItem(oldKey, annotations); + await raw.setItem('BASIC_KLINE_INDICATOR', [{ id: 'CANDLE', indicators: [] }]); + }, { oldKey, annotations }); + + // When the original Basic load discovers a missing current key and executes Lc before reading settings. + const result = await page.evaluate(async oldKey => { + const storage = window.__CHART_HOST__.basic.create({ namespace: 'chart_futures', symbol: 'SOLUSDT' }); + const loaded = await storage.load({ interval: '15m' }); + const raw = window.__STORAGE_LAB__.native; + return { loaded, old: await raw.getItem(oldKey), current: await raw.getItem('BASIC_KLINE_SOLUSDT'), keys: (await raw.keys()).sort() }; + }, oldKey); + + // Then migration copies the stored value without deleting its original identity and normalizes the loaded panel. + expect(result.old).toEqual(annotations); + expect(result.current).toEqual(annotations); + expect(result.keys).toEqual(['BASIC_KLINE_INDICATOR', 'BASIC_KLINE_SOLUSDT', oldKey].sort()); + expect(result.loaded).toEqual({ + storeData: [{ id: 'CANDLE', type: 'CANDLE', indicators: [], annotations: annotations[0].annotations }], + indicatorSetting: null, setting: null, annotationConfig: null, compareSymbolSettings: null, + }); + }); + + test(`user clears the shared chart store through Basic while preserving the other database with ${mode}`, async ({ page }) => { + // Given Basic and TradingView share one initialized store while delivery retains an independent record. + await openChartHostWorkflows(page, { install }); + await page.evaluate(async () => { + const { localforage } = window.__STORAGE_LAB__; + const tv = localforage.createInstance({ name: 'chart_futures' }); + const delivery = localforage.createInstance({ name: 'chart_delivery' }); + const basic = window.__CHART_HOST__.basic.create({ namespace: 'chart_futures', symbol: 'BTCUSDT' }); + await basic.saveSettings({ background: 'initial' }); + await tv.setItem('myTradingView', { revision: 0 }); + await delivery.setItem('myTradingView', { revision: 'delivery' }); + window.__HOST_SCENARIO__ = { basic, tv, delivery }; + }); + + // When completed chart and Basic saves are cleared before another chart save starts. + const result = await page.evaluate(async () => { + const { basic, tv, delivery } = window.__HOST_SCENARIO__; + await Promise.all([ + tv.setItem('#TV_SYMBOL-BEFORE', []), basic.saveSettings({ background: 'before-clear' }), + ]); + await basic.clear(); + await tv.setItem('#TV_SYMBOL-AFTER', []); + return { + keys: await tv.keys(), after: await tv.getItem('#TV_SYMBOL-AFTER'), + snapshot: await tv.getItem('myTradingView'), settings: await tv.getItem('candlestick-setting'), + delivery: await delivery.getItem('myTradingView'), + }; + }); + + // Then shared-store clear removes both chart types in call order and leaves the other database untouched. + expect(result).toEqual({ keys: ['#TV_SYMBOL-AFTER'], after: [], snapshot: null, settings: null, delivery: { revision: 'delivery' } }); + }); + + test(`user observes the documented concurrent Basic clear ordering boundary with ${mode}`, async ({ page }) => { + // Given retained host instances have finished initialization with one committed chart and Basic setting. + await openChartHostWorkflows(page, { install }); + await page.evaluate(async () => { + const tv = window.__STORAGE_LAB__.localforage.createInstance({ name: 'chart_futures' }); + const basic = window.__CHART_HOST__.basic.create({ namespace: 'chart_futures', symbol: 'BTCUSDT' }); + await tv.setItem('myTradingView', { revision: 0 }); + await basic.saveSettings({ background: 'initial' }); + window.__HOST_SCENARIO__ = { tv, basic }; + }); + + // When saves and the original Basic clear are issued together without awaiting earlier saves. + const result = await page.evaluate(async () => { + const { tv, basic } = window.__HOST_SCENARIO__; + await Promise.all([ + tv.setItem('#TV_SYMBOL-BEFORE', []), basic.saveSettings({ background: 'before-clear' }), + basic.clear(), tv.setItem('#TV_SYMBOL-AFTER', []), + ]); + return { + keys: (await tv.keys()).sort(), snapshot: await tv.getItem('myTradingView'), + settings: await tv.getItem('candlestick-setting'), + }; + }); + + // Then native set preparation runs after clear while the adapter enforces its stronger issue-order contract. + expect(result).toEqual(install + ? { keys: ['#TV_SYMBOL-AFTER'], snapshot: null, settings: null } + : { keys: ['#TV_SYMBOL-AFTER', '#TV_SYMBOL-BEFORE', 'candlestick-setting'], snapshot: null, settings: { background: 'before-clear' } }); + }); + + test(`user preserves 290 historical drawings through 32 bounded concurrent host save chains with ${mode}`, async ({ page }, testInfo) => { + // Given both databases contain identical historical drawings and one fully saved and mirrored initial snapshot. + test.setTimeout(30_000); + await openChartHostWorkflows(page, { install }); + const history = Array.from({ length: 290 }, (_, index) => { + const symbol = `LAB${String(index).padStart(3, '0')}USDT`; + return { key: `#TV_SYMBOL-${symbol}`, value: [chart(String(index), symbol).panes[0].sources[1]] }; + }); + const initial = { layout: 's', revision: 0, charts: [chart('0', 'BTCUSDT')] }; + await page.evaluate(async ({ history, initial }) => { + const { localforage } = window.__STORAGE_LAB__; + const source = localforage.createInstance({ name: 'chart_futures' }); + const target = localforage.createInstance({ name: 'chart_delivery' }); + await Promise.all(history.flatMap(({ key, value }) => [source.setItem(key, value), target.setItem(key, value)])); + const save = window.__CHART_HOST__.tradingView.createSaveCallback( + source, 'myTradingView', {}, window.__CHART_HOST__.mirror.create('chart_futures'), + ); + await save(initial); + const controller = window.__STORAGE_LAB__.controller; + window.__HOST_SCENARIO__ = { + source, target, save, initial, history, statsBefore: controller ? controller.getStats() : null, + }; + window.__STORAGE_NATIVE__.reset(); + }, { history, initial }); + + // When eight successive revisions each run four original save and mirror chains concurrently to completion. + const result = await page.evaluate(async () => { + const state = window.__HOST_SCENARIO__; + const startedAt = performance.now(); + let completed = 0; + for (let revision = 1; revision <= 8; revision += 1) { + const snapshot = structuredClone(state.initial); + snapshot.revision = revision; + snapshot.charts[0].panes[0].sources[1].points[1].price = 25 + revision; + const results = await Promise.all(Array.from({ length: 4 }, () => state.save(snapshot))); + completed += results.length; + } + const elapsedMs = performance.now() - startedAt; + const metrics = window.__STORAGE_NATIVE__.snapshot(); + const controller = window.__STORAGE_LAB__.controller; + let stats = null; + let peaks = null; + if (controller) { + const current = controller.getStats(); + const counters = [ + 'acceptedOperations', 'rejectedOperations', 'failedOperations', 'transactions', + 'committedTransactions', 'abortedTransactions', 'committedWrites', 'skippedWrites', 'barriers', + ]; + stats = { + ...Object.fromEntries(counters.map(name => [name, current[name] - state.statsBefore[name]])), + pendingOperations: current.pendingOperations, pendingBytes: current.pendingBytes, + }; + peaks = { + scope: 'Since installation, including setup.', + pendingOperations: current.peakPendingOperations, pendingBytes: current.peakPendingBytes, + setupPendingOperations: state.statsBefore.peakPendingOperations, setupPendingBytes: state.statsBefore.peakPendingBytes, + }; + } + const read = async storage => ({ + keys: (await storage.keys()).sort(), + snapshot: await storage.getItem('myTradingView'), layout: await storage.getItem('myTradingView.layout'), + drawing: await storage.getItem('#TV_SYMBOL-BTCUSDT'), + historical: await Promise.all(state.history.map(async ({ key }) => ({ key, value: await storage.getItem(key) }))), + loaded: await window.__CHART_HOST__.tradingView.load({ storage, key: 'myTradingView' }), + }); + return { completed, elapsedMs, metrics, stats, peaks, source: await read(state.source), target: await read(state.target) }; + }); + const measurement = { + mode, historicalKeysPerDatabase: 290, rounds: 8, concurrentChainsPerRound: 4, + completed: result.completed, elapsedMs: result.elapsedMs, metrics: result.metrics, stats: result.stats, peaks: result.peaks, + scope: 'Isolated fixture workload only; setup and final verification IO excluded from IDB metrics and controller counters. Peaks include setup.', + }; + await testInfo.attach(`host-bounded-load-${mode}.json`, { + body: JSON.stringify(measurement, null, 2), contentType: 'application/json', + }); + console.info(JSON.stringify(measurement)); + + // Then every chain completes and both complete databases retain all historical values plus the final changed snapshot and drawing. + const finalChart = chart('0', 'BTCUSDT'); + finalChart.panes[0].sources[1].points[1].price = 33; + const expectedKeys = [...history.map(({ key }) => key), '#TV_SYMBOL-BTCUSDT', 'myTradingView', 'myTradingView.layout'].sort(); + const expected = { + keys: expectedKeys, + snapshot: { layout: 's', revision: 8, charts: [chart('0', 'BTCUSDT', false)] }, + layout: [chart('0', 'BTCUSDT', false)], drawing: [finalChart.panes[0].sources[1]], historical: history, + loaded: { chartSave: { layout: 's', revision: 8, charts: [finalChart] }, customSettings: null }, + }; + expect(result.completed).toBe(32); + expect(result.source).toEqual(expected); + expect(result.target).toEqual(expected); + expect(result.elapsedMs).toBeGreaterThan(0); + expect(result.metrics.pending).toBe(0); + expect(result.metrics.aborted).toBe(0); + expect(result.metrics.completed).toBe(result.metrics.transactions); + expect(result.metrics.puts).toBe(install ? 32 : 296 * 32); + if (install) { + expect(result.metrics.transactions).toBeLessThan(591 * 32); + expect(result.stats.pendingOperations).toBe(0); + expect(result.stats.pendingBytes).toBe(0); + expect(result.stats.failedOperations).toBe(0); + expect(result.stats.rejectedOperations).toBe(0); + expect(result.stats.acceptedOperations).toBe(591 * 32); + expect(result.stats.committedWrites).toBe(32); + expect(result.stats.skippedWrites).toBe(296 * 32 - 32); + } else { + expect(result.metrics.transactions).toBe(591 * 32); + expect(result.stats).toBe(null); + } + }); +} diff --git a/e2e/binance-orderbook/specs/chart-storage-integration.pw.js b/e2e/binance-orderbook/specs/chart-storage-integration.pw.js new file mode 100644 index 0000000..f211fb6 --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-integration.pw.js @@ -0,0 +1,534 @@ +import { test, expect } from '../test.js'; +import { initializeStorageLab, openEmptyStorageLab, openStorageLab, STORAGE_LAB_ORIGIN } from '../helpers/chart-storage-lab.js'; +import { installScenarioClock, pauseScenarioClock } from '../helpers/scenario-clock.js'; + +async function installEarlyCapture(page, { existingQueue = true, callbackFails = false } = {}) { + await page.evaluate(async ({ existingQueue, callbackFails }) => { + const { observeChartStorageBootstrap } = await import('/bootstrap.js'); + const { installChartStoragePrototype } = await import('/adapter.js'); + if (existingQueue) self.webpackChunkfutures_trade_ui = []; + const state = { + captures: 0, factoryExecutions: 0, completion: 'pending', + hostResult: null, hostError: null, + }; + window.__STORAGE_STARTUP__ = state; + state.capture = observeChartStorageBootstrap({ + onCapture(localforage) { + state.captures += 1; + if (callbackFails) throw new Error('Startup capture callback failed'); + state.capturedExport = localforage; + state.nativeCreateInstance = localforage.createInstance; + state.controller = installChartStoragePrototype(localforage); + }, + }); + state.queue = self.webpackChunkfutures_trade_ui; + state.capture.captured.then( + value => { state.completion = 'fulfilled'; state.completedExport = value; }, + error => { state.completion = 'rejected'; state.captureError = error.message; }, + ); + state.runHost = require => { + try { + const localforage = require(43917); + state.hostExport = localforage; + state.store = localforage.createInstance({ + name: 'chart_futures', storeName: 'keyvaluepairs', driver: localforage.INDEXEDDB, + }); + state.saved = state.store.setItem('#TV_SYMBOL-STARTUP', { interval: '15' }); + state.hostResult = { + captures: state.captures, + acceptedBeforeReturn: state.controller ? state.controller.getStats().acceptedOperations : 0, + exportMatches: localforage === state.capturedExport, + }; + } catch (error) { + state.hostError = error.message; + } + }; + }, { existingQueue, callbackFails }); +} + +/** + * Execute the unchanged library fixture with only its chunk-registration boundary + * supplied by the host. The real factory and runtime cache own module execution. + */ +async function registerStorageChunk(page, { requireStorage = true } = {}) { + await page.evaluate(async ({ requireStorage }) => { + const response = await fetch('/localforage-module.js'); + if (!response.ok) throw new Error(`Storage fixture request failed: ${response.status}`); + const source = await response.text(); + const state = window.__STORAGE_STARTUP__; + const registration = { + webpackChunkfutures_trade_ui: { + push(chunk) { + const actualFactory = chunk[1][43917]; + state.originalFactory = function (...args) { + state.factoryExecutions += 1; + const result = actualFactory.apply(this, args); + state.nativeFactoryBeforeCapture = args[0].exports.createInstance; + return result; + }; + chunk[1][43917] = state.originalFactory; + chunk[2] = require => { + state.require = require; + if (requireStorage) state.runHost(require); + }; + return state.queue.push(chunk); + }, + }, + }; + new Function('self', source)(registration); + }, { requireStorage }); +} + +async function installStoragePreflight(page) { + await page.evaluate(async () => { + const { startChartStoragePreflight } = await import('/preflight.js'); + const preflight = startChartStoragePreflight(); + const state = { + preflight, queue: self.webpackChunkfutures_trade_ui, factoryExecutions: 0, + runHost(require) { + state.hostExport = require(43917); + state.store = state.hostExport.createInstance({ name: 'chart_futures' }); + state.snapshotBeforeHostReturns = preflight.snapshot(); + }, + }; + window.__STORAGE_STARTUP__ = state; + window.__STORAGE_NATIVE__.reset(); + }); +} + +async function readCaptureCleanup(page) { + return page.evaluate(() => { + const state = window.__STORAGE_STARTUP__; + const push = state.queue.push; + state.capture.stop(); + let laterHostExport; + state.queue.push([['storage-lab-after-capture'], {}, require => { laterHostExport = require(43917); }]); + const descriptor = Object.getOwnPropertyDescriptor(state.queue, 'push'); + return { + sameExport: laterHostExport === state.hostExport, + samePush: state.queue.push === push, + ordinaryPush: Object.hasOwn(descriptor, 'value') && descriptor.writable && descriptor.configurable && descriptor.enumerable, + factoryRestored: state.require.m[43917] === state.originalFactory, + queuedFactoryRestored: state.queue.find(chunk => Object.hasOwn(chunk[1], '43917'))[1][43917] === state.originalFactory, + factoryExecutions: state.factoryExecutions, + captures: state.captures, + }; + }); +} + +for (const order of ['runtime then module', 'module then runtime']) { + test(`user captures chart storage before the first host save with ${order}`, async ({ page }) => { + // Given capture observes an empty global queue before either real host fixture loads. + await openEmptyStorageLab(page); + await installEarlyCapture(page); + + // When the real runtime consumes the library chunk and its host requires storage in the same stack. + if (order === 'runtime then module') { + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + await registerStorageChunk(page); + } else { + await registerStorageChunk(page); + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + } + const result = await page.evaluate(async () => { + const state = window.__STORAGE_STARTUP__; + const persisted = state.saved ? await state.saved : null; + return { + hostResult: state.hostResult, hostError: state.hostError, + captures: state.captures, factoryExecutions: state.factoryExecutions, + completion: state.completion, persisted, + }; + }); + + // Then the first host save already uses the adapter and the actual library factory executes once. + expect(result).toEqual({ + hostResult: { captures: 1, acceptedBeforeReturn: 1, exportMatches: true }, + hostError: null, captures: 1, factoryExecutions: 1, + completion: 'fulfilled', persisted: { interval: '15' }, + }); + + // When later host work uses the restored queue after successful capture is stopped again. + const cleanup = await readCaptureCleanup(page); + + // Then no observer accessor or wrapped factory survives and require reuses the existing library export. + expect(cleanup).toEqual({ + sameExport: true, samePush: true, ordinaryPush: true, + factoryRestored: true, queuedFactoryRestored: true, factoryExecutions: 1, captures: 1, + }); + }); +} + +test('user captures the first chart save when the global chunk queue does not exist yet', async ({ page }) => { + // Given the empty document has not created a global runtime queue. + await openEmptyStorageLab(page); + expect(await page.evaluate(() => Object.hasOwn(self, 'webpackChunkfutures_trade_ui'))).toBe(false); + await installEarlyCapture(page, { existingQueue: false }); + + // When the runtime initializes its queue and the first module chunk immediately requires chart storage. + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + await registerStorageChunk(page); + const result = await page.evaluate(async () => { + const state = window.__STORAGE_STARTUP__; + return { hostResult: state.hostResult, saved: await state.saved, completion: state.completion }; + }); + + // Then bootstrap-created registration still captures the exact export before the first save returns. + expect(result).toEqual({ + hostResult: { captures: 1, acceptedBeforeReturn: 1, exportMatches: true }, + saved: { interval: '15' }, completion: 'fulfilled', + }); +}); + +for (const stage of ['before scripts', 'before runtime consumes module', 'after runtime registers module']) { + test(`user stops startup observation ${stage} and keeps native host storage available`, async ({ page }) => { + // Given capture has not executed the actual library factory at the selected host startup boundary. + await openEmptyStorageLab(page); + await installEarlyCapture(page); + if (stage !== 'before scripts') await registerStorageChunk(page, { requireStorage: false }); + if (stage === 'after runtime registers module') { + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + } + expect(await page.evaluate(() => window.__STORAGE_STARTUP__.factoryExecutions)).toBe(0); + + // When observation stops and the host completes startup before its first storage use. + await page.evaluate(() => window.__STORAGE_STARTUP__.capture.stop()); + if (stage !== 'after runtime registers module') { + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + } + if (stage === 'before scripts') await registerStorageChunk(page, { requireStorage: false }); + const result = await page.evaluate(async () => { + const state = window.__STORAGE_STARTUP__; + state.runHost(state.require); + return { + completion: state.completion, captureError: state.captureError, + captures: state.captures, hostError: state.hostError, + hostResult: state.hostResult, saved: await state.saved, + }; + }); + const cleanup = await readCaptureCleanup(page); + + // Then only capture rejects while the restored factory executes exactly once and persists the native host save. + expect(result.completion).toBe('rejected'); + expect(result.captureError).toMatch(/stop.*before.*execution/i); + expect(result.captures).toBe(0); + expect(result.hostError).toBe(null); + expect(result.hostResult).toEqual({ captures: 0, acceptedBeforeReturn: 0, exportMatches: false }); + expect(result.saved).toEqual({ interval: '15' }); + expect(cleanup).toEqual({ + sameExport: true, samePush: true, ordinaryPush: true, + factoryRestored: true, queuedFactoryRestored: true, factoryExecutions: 1, captures: 0, + }); + }); +} + +for (const stage of ['runtime starts', 'module registers']) { + test(`user receives an explicit late-bootstrap refusal after the ${stage}`, async ({ page }) => { + // Given one real fixture has started before bootstrap can observe a native empty queue. + await openEmptyStorageLab(page); + const first = stage === 'runtime starts' ? 'webpack-runtime.js' : 'localforage-module.js'; + const second = stage === 'runtime starts' ? 'localforage-module.js' : 'webpack-runtime.js'; + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/${first}` }); + + // When bootstrap attempts late installation and the host then completes its unmodified startup. + const rejection = await page.evaluate(async () => { + const { observeChartStorageBootstrap } = await import('/bootstrap.js'); + try { + const capture = observeChartStorageBootstrap(); + capture.captured.catch(error => { window.__LATE_CAPTURE_ERROR__ = error.message; }); + capture.stop(); + return null; + } catch (error) { + return error.message; + } + }); + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/${second}` }); + await initializeStorageLab(page, { install: false }); + const identity = await page.evaluate(() => window.__STORAGE_LAB__.cacheIdentity()); + + // Then refusal is immediate and the original runtime still executes and caches localForage once. + expect(rejection).toMatch(/late|before|already|native|empty|runtime/i); + expect(identity).toEqual({ sameExport: true, factoryExecutions: 1 }); + }); +} + +test('user retains a working host module when the startup capture callback throws', async ({ page }) => { + // Given the observer callback will fail after the real library factory creates its export. + await openEmptyStorageLab(page); + await installEarlyCapture(page, { callbackFails: true }); + + // When the real runtime registers and requires storage in the same host stack. + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + await registerStorageChunk(page); + const result = await page.evaluate(async () => { + const state = window.__STORAGE_STARTUP__; + return { + saved: await state.saved, hostError: state.hostError, + completion: state.completion, captureError: state.captureError, + }; + }); + const cleanup = await readCaptureCleanup(page); + + // Then only the observer rejects and the host retains its successful save and original cache identity. + expect(result).toEqual({ + saved: { interval: '15' }, hostError: null, + completion: 'rejected', captureError: 'Startup capture callback failed', + }); + expect(cleanup).toEqual({ + sameExport: true, samePush: true, ordinaryPush: true, + factoryRestored: true, queuedFactoryRestored: true, factoryExecutions: 1, captures: 1, + }); +}); + +test('user retains a queued host module when its null runtime callback ends observation', async ({ page }) => { + // Given bootstrap creates a previously absent queue before a host chunk with a valid falsy runtime callback. + await openEmptyStorageLab(page); + await page.evaluate(async () => { + const { observeChartStorageBootstrap } = await import('/bootstrap.js'); + const observer = observeChartStorageBootstrap(); + const state = { queue: self.webpackChunkfutures_trade_ui, factoryExecutions: 0, outcome: 'pending' }; + state.originalFactory = module => { + state.factoryExecutions += 1; + module.exports = { hostValue: 'retained' }; + }; + state.chunk = [['chart-storage-null-runtime'], { 43917: state.originalFactory }, null]; + observer.captured.then( + () => { state.outcome = 'fulfilled'; }, + error => { state.outcome = 'rejected'; state.error = error.message; }, + ); + window.__STORAGE_NULL_RUNTIME__ = state; + }); + + // When registration ends observation before the real runtime consumes the exact original queue. + await page.evaluate(() => { + const state = window.__STORAGE_NULL_RUNTIME__; + state.queue.push(state.chunk); + state.sameQueueAfterAppend = self.webpackChunkfutures_trade_ui === state.queue; + state.sameChunkAfterAppend = state.queue[0] === state.chunk; + }); + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + const result = await page.evaluate(() => { + const state = window.__STORAGE_NULL_RUNTIME__; + let runtimeRequire; + self.webpackChunkfutures_trade_ui.push([['chart-storage-null-host'], {}, require => { runtimeRequire = require; }]); + const factoryRetained = runtimeRequire.m[43917] === state.originalFactory; + const value = factoryRetained ? runtimeRequire(43917) : null; + return { + outcome: state.outcome, error: state.error, + sameQueueAfterAppend: state.sameQueueAfterAppend, + sameChunkAfterAppend: state.sameChunkAfterAppend, + sameQueueAfterRuntime: self.webpackChunkfutures_trade_ui === state.queue, + factoryRetained, factoryExecutions: state.factoryExecutions, value, + }; + }); + + // Then the observer rejects its unsupported shape while the unmodified host factory remains registered and callable. + expect(result.outcome).toBe('rejected'); + expect(result.error).toMatch(/runtime callback/i); + expect(result.sameQueueAfterAppend).toBe(true); + expect(result.sameChunkAfterAppend).toBe(true); + expect(result.sameQueueAfterRuntime).toBe(true); + expect(result.factoryRetained).toBe(true); + expect(result.factoryExecutions).toBe(1); + expect(result.value).toEqual({ hostValue: 'retained' }); +}); + +test('user saves nested chart JSON created by a same-origin iframe without changing its value', async ({ page }) => { + // Given the parent owns a real storage facade and an iframe creates the complete chart value in its own realm. + await openStorageLab(page); + await page.evaluate(() => { + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + const json = '{"symbol":"BTCUSDT","layout":{"panes":[{"id":1,"studies":[{"name":"volume","inputs":[20,true,null]}]}]},"__proto__":{"chart":"own-data"}}'; + window.__STORAGE_FRAME_CHART__ = { + frame, value: frame.contentWindow.JSON.parse(json), expected: JSON.parse(json), + }; + }); + + // When the iframe value is submitted through create and then read with the parent's native localForage instance. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const { frame, value, expected } = window.__STORAGE_FRAME_CHART__; + const foreignPrototype = Object.getPrototypeOf(value); + const ownPrototype = Object.getPrototypeOf(value.layout); + const outcomes = await Promise.allSettled([lab.create().setItem('#TV_SYMBOL-IFRAME', value)]); + const outcome = outcomes[0]; + return { + differentRealm: foreignPrototype === frame.contentWindow.Object.prototype && foreignPrototype !== Object.prototype, + status: outcome.status, + rejection: outcome.status === 'rejected' ? outcome.reason.message : null, + returned: outcome.status === 'fulfilled' ? outcome.value : null, + persisted: await lab.native.getItem('#TV_SYMBOL-IFRAME'), expected, + original: value, + prototypesPreserved: Object.getPrototypeOf(value) === foreignPrototype && Object.getPrototypeOf(value.layout) === ownPrototype, + dangerousKeyPreserved: Object.hasOwn(value, '__proto__'), + }; + }); + + // Then the real IndexedDB record preserves all nested JSON and the input keeps its original realm and own properties. + expect(result.differentRealm).toBe(true); + expect(result.status).toBe('fulfilled'); + expect(result.rejection).toBe(null); + expect(result.returned).toEqual(result.expected); + expect(result.persisted).toEqual(result.expected); + expect(result.original).toEqual(result.expected); + expect(result.prototypesPreserved).toBe(true); + expect(result.dangerousKeyPreserved).toBe(true); +}); + +test('user keeps the original chart when iframe Date and class values are refused', async ({ page }) => { + // Given the real database contains a valid chart and iframe values include non-JSON object types. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + const realm = frame.contentWindow; + const ChartModel = realm.eval('(class ChartModel { constructor() { this.interval = "30"; } })'); + const ObjectNamedClass = realm.eval('(class Object { constructor() { this.interval = "60"; } })'); + const forgedPrototype = realm.Object.create(null); + realm.Object.defineProperty(forgedPrototype, 'constructor', { value: realm.Object }); + const forgedPlainObject = realm.Object.create(forgedPrototype); + forgedPlainObject.interval = '240'; + const values = [new realm.Date(0), new ChartModel(), new ObjectNamedClass(), new realm.Map([['interval', '15']]), forgedPlainObject]; + lab.invalidCharts = values.map(value => ({ layout: value })); + lab.invalidChartSnapshots = lab.invalidCharts.map(value => JSON.stringify(value)); + lab.invalidChartPrototypes = values.map(value => Object.getPrototypeOf(value)); + await lab.native.setItem('#TV_SYMBOL-PRESERVED', { interval: '15', studies: ['volume'] }); + }); + + // When each invalid nested value attempts to replace the same existing chart through the public facade. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const store = lab.create(); + const outcomes = await Promise.allSettled(lab.invalidCharts.map(value => store.setItem('#TV_SYMBOL-PRESERVED', value))); + return { + outcomes: outcomes.map(outcome => ({ + status: outcome.status, message: outcome.status === 'rejected' ? outcome.reason.message : null, + })), + persisted: await lab.native.getItem('#TV_SYMBOL-PRESERVED'), + originalValuesPreserved: lab.invalidCharts.every((value, index) => ( + JSON.stringify(value) === lab.invalidChartSnapshots[index] + && Object.getPrototypeOf(value.layout) === lab.invalidChartPrototypes[index] + )), + }; + }); + + // Then all unsupported values reject explicitly and neither the stored chart nor the caller's objects change. + expect(result.outcomes.map(outcome => outcome.status)).toEqual(['rejected', 'rejected', 'rejected', 'rejected', 'rejected']); + for (const outcome of result.outcomes) expect(outcome.message).toMatch(/plain JSON/i); + expect(result.persisted).toEqual({ interval: '15', studies: ['volume'] }); + expect(result.originalValuesPreserved).toBe(true); +}); + +test('user observes native chart instance creation through preflight without adding storage work', async ({ page }) => { + // Given preflight starts before the real runtime without installing a storage adapter. + await openEmptyStorageLab(page); + await installStoragePreflight(page); + + // When the first real module chunk creates a chart instance in the same host stack. + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + await registerStorageChunk(page); + const observed = await page.evaluate(() => { + const state = window.__STORAGE_STARTUP__; + return { snapshot: state.snapshotBeforeHostReturns, metrics: window.__STORAGE_NATIVE__.snapshot() }; + }); + + // Then preflight records only the native creation and does not open a database or submit storage operations. + expect(observed.snapshot.outcome).toBe('captured'); + expect(observed.snapshot.active).toBe(true); + expect(observed.snapshot.instances).toEqual({ chart_futures: 1, chart_delivery: 0 }); + expect(observed.snapshot.captureAtMs).toBeGreaterThanOrEqual(0); + expect(observed.snapshot.firstInstances).toHaveLength(1); + expect(observed.snapshot.firstInstances[0]).toEqual({ + name: 'chart_futures', storeName: 'keyvaluepairs', atMs: expect.any(Number), + }); + expect(observed.snapshot.firstInstances[0].atMs).toBeGreaterThanOrEqual(observed.snapshot.captureAtMs); + expect(observed.metrics).toMatchObject({ opens: 0, transactions: 0, puts: 0, gets: 0, pending: 0 }); + + // When the host initializes its native instances, stops observation, and uses the retained original instance. + const result = await page.evaluate(async () => { + const state = window.__STORAGE_STARTUP__; + const control = state.nativeFactoryBeforeCapture.call(state.hostExport, { name: 'chart_futures' }); + await Promise.all([state.store.ready(), control.ready()]); + const methodsAreNative = state.store.getItem === control.getItem && state.store.setItem === control.setItem; + const getItem = state.store.getItem; + const setItem = state.store.setItem; + window.__STORAGE_NATIVE__.reset(); + state.preflight.stop(); + const metricsAfterStop = window.__STORAGE_NATIVE__.snapshot(); + const written = await state.store.setItem('#TV_SYMBOL-PREFLIGHT', { interval: '15', studies: ['volume'] }); + const read = await state.store.getItem('#TV_SYMBOL-PREFLIGHT'); + const metrics = window.__STORAGE_NATIVE__.snapshot(); + return { + methodsAreNative, + methodsRetained: state.store.getItem === getItem && state.store.setItem === setItem, + factoryRestored: state.hostExport.createInstance === state.nativeFactoryBeforeCapture, + factoryExecutions: state.factoryExecutions, + sameExport: state.require(43917) === state.hostExport, + metricsAfterStop, metrics, written, read, snapshot: state.preflight.snapshot(), + }; + }); + + // Then stopping restores the exact factory while the unwrapped retained instance performs one native write and one read. + expect(result.methodsAreNative).toBe(true); + expect(result.methodsRetained).toBe(true); + expect(result.factoryRestored).toBe(true); + expect(result.factoryExecutions).toBe(1); + expect(result.sameExport).toBe(true); + expect(result.metricsAfterStop).toMatchObject({ opens: 0, transactions: 0, puts: 0, gets: 0, pending: 0 }); + expect(result.metrics).toMatchObject({ opens: 0, transactions: 2, puts: 1, gets: 1 }); + expect(result.written).toEqual({ interval: '15', studies: ['volume'] }); + expect(result.read).toEqual(result.written); + expect(result.snapshot.active).toBe(false); + expect(result.snapshot.instances).toEqual({ chart_futures: 1, chart_delivery: 0 }); +}); + +test('user has preflight restore the native factory automatically after thirty seconds', async ({ page }) => { + // Given a paused virtual clock owns the preflight lifetime before any real host fixture loads. + await installScenarioClock(page); + await openEmptyStorageLab(page); + await pauseScenarioClock(page); + await installStoragePreflight(page); + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + await registerStorageChunk(page); + + // When the clock reaches the instant immediately before the bounded observation ends. + await page.clock.runFor(29_999); + const beforeDeadline = await page.evaluate(() => { + const state = window.__STORAGE_STARTUP__; + return { + active: state.preflight.snapshot().active, + factoryRestored: state.hostExport.createInstance === state.nativeFactoryBeforeCapture, + }; + }); + + // Then the observer remains active throughout the authorized observation window. + expect(beforeDeadline).toEqual({ active: true, factoryRestored: false }); + + // When the deadline arrives and the host creates another native instance after automatic cleanup. + await page.clock.runFor(1); + const afterDeadline = await page.evaluate(() => { + const state = window.__STORAGE_STARTUP__; + state.hostExport.createInstance({ name: 'chart_delivery' }); + const descriptor = Object.getOwnPropertyDescriptor(state.queue, 'push'); + return { + snapshot: state.preflight.snapshot(), + factoryRestored: state.hostExport.createInstance === state.nativeFactoryBeforeCapture, + ordinaryPush: Object.hasOwn(descriptor, 'value') && descriptor.writable && descriptor.configurable, + moduleFactoryRestored: state.require.m[43917] === state.originalFactory, + factoryExecutions: state.factoryExecutions, + metrics: window.__STORAGE_NATIVE__.snapshot(), + }; + }); + + // Then all observation hooks are restored at thirty seconds and later native creation is neither counted nor accessed. + expect(afterDeadline.snapshot).toMatchObject({ + outcome: 'captured', active: false, instances: { chart_futures: 1, chart_delivery: 0 }, + }); + expect(afterDeadline.snapshot.firstInstances).toHaveLength(1); + expect(afterDeadline.factoryRestored).toBe(true); + expect(afterDeadline.ordinaryPush).toBe(true); + expect(afterDeadline.moduleFactoryRestored).toBe(true); + expect(afterDeadline.factoryExecutions).toBe(1); + expect(afterDeadline.metrics).toMatchObject({ opens: 0, transactions: 0, puts: 0, gets: 0, pending: 0 }); +}); diff --git a/e2e/binance-orderbook/specs/chart-storage-lifecycle.pw.js b/e2e/binance-orderbook/specs/chart-storage-lifecycle.pw.js new file mode 100644 index 0000000..42b7faa --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-lifecycle.pw.js @@ -0,0 +1,586 @@ +import { test, expect } from '../test.js'; +import { openStorageLab } from '../helpers/chart-storage-lab.js'; + +/** Real request events retain the write lock until the scenario releases it. */ +async function holdChartWriteLock(page) { + await page.evaluate(() => { + const transaction = window.__STORAGE_LAB__.native._dbInfo.db.transaction('keyvaluepairs', 'readwrite'); + const store = transaction.objectStore('keyvaluepairs'); + const pulses = []; + const lock = { + released: false, + progress: () => new Promise(resolve => pulses.push(resolve)), + completion: new Promise((resolve, reject) => { + transaction.addEventListener('complete', () => resolve('complete'), { once: true }); + transaction.addEventListener('abort', () => reject(transaction.error), { once: true }); + }), + }; + function hold() { + const request = store.get('#TV_SYMBOL-STOP'); + request.onsuccess = () => { + for (const resolve of pulses.splice(0)) resolve(); + if (!lock.released) hold(); + }; + } + window.__STORAGE_STOP_LOCK__ = lock; + hold(); + }); + await page.evaluate(() => window.__STORAGE_STOP_LOCK__.progress()); +} + +async function releaseChartWriteLock(page) { + await page.evaluate(async () => { + const lock = window.__STORAGE_STOP_LOCK__; + lock.released = true; + await lock.completion; + }); +} + +test('user preserves accepted writes before retained and newly created stores resume native saves', async ({ page, browser }) => { + // Given an independent native baseline and an actual lock blocking the accepted chart batch. + const nativeContext = await browser.newContext(); + let nativeBaseline; + try { + const nativePage = await nativeContext.newPage(); + await openStorageLab(nativePage, { install: false }); + nativeBaseline = await nativePage.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await lab.native.setItem('#TV_SYMBOL-STOP', 1); + const retained = lab.create(); + await retained.ready(); + const fresh = lab.create(); + const callbacks = []; + const callback = id => (error, value) => callbacks.push({ id, error: error === null, value }); + const tail = await Promise.allSettled([ + retained.getItem('#TV_SYMBOL-STOP', callback('retained-read')), + retained.setItem('#TV_SYMBOL-RETAINED', 2, callback('retained-write')), + fresh.setItem('#TV_SYMBOL-STOP', 3, callback('fresh-write')), + fresh.getItem('#TV_SYMBOL-STOP', callback('fresh-read')), + ]); + return { tail, callbacks, finalValue: await lab.native.getItem('#TV_SYMBOL-STOP') }; + }); + } finally { + await nativeContext.close(); + } + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await lab.native.setItem('#TV_SYMBOL-STOP', 0); + lab.store = lab.create(); + await lab.store.ready(); + }); + await holdChartWriteLock(page); + await page.evaluate(() => { + const lab = window.__STORAGE_LAB__; + window.__STORAGE_NATIVE__.reset(); + lab.accepted = Promise.all([ + lab.store.setItem('#TV_SYMBOL-STOP', 1), lab.store.getItem('#TV_SYMBOL-STOP'), + ]); + }); + await expect.poll(() => page.evaluate(() => window.__STORAGE_NATIVE__.snapshot().pending)).toBe(1); + + // When stop and calls through both retained and new stores occur in the same stack. + let beforeRelease; + try { + beforeRelease = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.stopResolved = false; + lab.stopping = lab.controller.stop(); + lab.stopping.then(() => { lab.stopResolved = true; }); + lab.callbacks = []; + const callback = id => (error, value) => lab.callbacks.push({ id, error: error === null, value }); + const fresh = lab.create(); + lab.nativeTail = Promise.allSettled([ + lab.store.getItem('#TV_SYMBOL-STOP', callback('retained-read')), + lab.store.setItem('#TV_SYMBOL-RETAINED', 2, callback('retained-write')), + fresh.setItem('#TV_SYMBOL-STOP', 3, callback('fresh-write')), + fresh.getItem('#TV_SYMBOL-STOP', callback('fresh-read')), + ]); + lab.readyCallbacks = []; + lab.nativeReady = lab.store.ready((...args) => { lab.readyCallbacks.push(args); }); + let configError; + try { + lab.store.config({ description: 'Cannot mutate during drain' }); + } catch (error) { + configError = error.message; + } + await window.__STORAGE_STOP_LOCK__.progress(); + return { + stopResolved: lab.stopResolved, + restoredFactory: lab.localforage.createInstance === lab.nativeCreateInstance, + callbacks: [...lab.callbacks], + acceptedOperations: lab.controller.getStats().acceptedOperations, + pendingOperations: lab.controller.getStats().pendingOperations, + transactions: window.__STORAGE_NATIVE__.snapshot().transactions, + readyCallbacks: [...lab.readyCallbacks], + configName: lab.store.config('name'), driver: lab.store.driver(), configError, + }; + }); + } finally { + await releaseChartWriteLock(page); + } + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const accepted = await lab.accepted; + await lab.stopping; + const tail = await lab.nativeTail; + await lab.nativeReady; + return { + accepted, tail, callbacks: lab.callbacks, stats: lab.controller.getStats(), + readyCallbacks: lab.readyCallbacks, + restoredFactory: lab.localforage.createInstance === lab.nativeCreateInstance, + finalValue: await lab.native.getItem('#TV_SYMBOL-STOP'), + retainedValue: await lab.native.getItem('#TV_SYMBOL-RETAINED'), + }; + }); + + // Then only accepted work crosses the lock and every later call completes after its drain fence. + expect(beforeRelease).toEqual({ + stopResolved: false, restoredFactory: false, callbacks: [], + acceptedOperations: 2, pendingOperations: 2, transactions: 1, + readyCallbacks: [], configName: 'chart_futures', driver: 'asyncStorage', + configError: 'Managed chart configuration is immutable', + }); + expect(result.accepted).toEqual([1, 1]); + // Native setItem prepares its value in an extra Promise stage, so a same-stack read can precede it. + expect(nativeBaseline.tail).toEqual([1, 2, 3, 1].map(value => ({ status: 'fulfilled', value }))); + expect(nativeBaseline.finalValue).toBe(3); + expect(result.tail).toEqual(nativeBaseline.tail); + expect(result.callbacks).toEqual(nativeBaseline.callbacks); + expect(result.readyCallbacks).toEqual([[undefined]]); + expect(result.finalValue).toBe(3); + expect(result.retainedValue).toBe(2); + expect(result.restoredFactory).toBe(true); + expect(result.stats).toMatchObject({ acceptedOperations: 2, rejectedOperations: 0, pendingOperations: 0, pendingBytes: 0 }); +}); + +test('user preserves a replacement factory when ownership changes while accepted writes drain', async ({ page }) => { + // Given an accepted write waits behind a real native transaction lock. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await lab.native.setItem('#TV_SYMBOL-OWNERSHIP', 0); + lab.store = lab.create(); + await lab.store.ready(); + }); + await holdChartWriteLock(page); + await page.evaluate(() => { + const lab = window.__STORAGE_LAB__; + window.__STORAGE_NATIVE__.reset(); + lab.accepted = lab.store.setItem('#TV_SYMBOL-OWNERSHIP', 1); + }); + await expect.poll(() => page.evaluate(() => window.__STORAGE_NATIVE__.snapshot().pending)).toBe(1); + + // When another owner replaces the factory after stop starts and before the write commits. + let whileLocked; + try { + whileLocked = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.stopSettled = false; + lab.stopped = Promise.allSettled([lab.controller.stop()]).then(outcomes => { + lab.stopSettled = true; + return outcomes; + }); + lab.replacementFactory = function (...args) { + return Reflect.apply(lab.nativeCreateInstance, lab.localforage, args); + }; + Object.defineProperty(lab.localforage, 'createInstance', { + value: lab.replacementFactory, enumerable: false, writable: true, configurable: true, + }); + await window.__STORAGE_STOP_LOCK__.progress(); + return { stopSettled: lab.stopSettled, pendingOperations: lab.controller.getStats().pendingOperations }; + }); + } finally { + await releaseChartWriteLock(page); + } + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const accepted = await lab.accepted; + const [stopped] = await lab.stopped; + const descriptor = Object.getOwnPropertyDescriptor(lab.localforage, 'createInstance'); + return { + accepted, stopStatus: stopped.status, stopError: stopped.reason.message, + replacementPreserved: descriptor.value === lab.replacementFactory, + descriptor: { enumerable: descriptor.enumerable, writable: descriptor.writable, configurable: descriptor.configurable }, + persisted: await lab.native.getItem('#TV_SYMBOL-OWNERSHIP'), stats: lab.controller.getStats(), + }; + }); + + // Then the accepted write commits and stop refuses to overwrite the other owner's exact descriptor. + expect(whileLocked).toEqual({ stopSettled: false, pendingOperations: 1 }); + expect(result.accepted).toBe(1); + expect(result.persisted).toBe(1); + expect(result.stopStatus).toBe('rejected'); + expect(result.stopError).toMatch(/ownership/i); + expect(result.replacementPreserved).toBe(true); + expect(result.descriptor).toEqual({ enumerable: false, writable: true, configurable: true }); + expect(result.stats).toMatchObject({ acceptedOperations: 1, failedOperations: 0, pendingOperations: 0, pendingBytes: 0 }); +}); + +test('user drains saves awaiting a blocked native database upgrade before restoring the factory', async ({ page }) => { + // Given an external version-one connection will block the real library's version-two initialization. + await openStorageLab(page); + await page.evaluate(async () => { + const database = await new Promise((resolve, reject) => { + const request = indexedDB.open('chart_futures', 1); + request.onupgradeneeded = () => request.result.createObjectStore('keyvaluepairs'); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const nativeOpen = IDBFactory.prototype.open; + let blocked; + let opened; + const gate = { + database, nativeOpen, + blocked: new Promise(resolve => { blocked = resolve; }), + opened: new Promise(resolve => { opened = resolve; }), + }; + /** Observe the native upgrade request without replacing its initialization or event behavior. */ + IDBFactory.prototype.open = function (...args) { + const request = Reflect.apply(nativeOpen, this, args); + if (args[0] === 'chart_futures' && args[1] === 2) { + request.addEventListener('blocked', event => { + blocked({ oldVersion: event.oldVersion, newVersion: event.newVersion }); + }, { once: true }); + request.addEventListener('success', () => opened(request.result.version), { once: true }); + } + return request; + }; + window.__STORAGE_INITIALIZATION_GATE__ = gate; + window.__STORAGE_NATIVE__.reset(); + const lab = window.__STORAGE_LAB__; + lab.store = lab.create('chart_futures', { version: 2 }); + lab.accepted = Promise.all([ + lab.store.setItem('#TV_SYMBOL-INITIALIZING', 1), lab.store.getItem('#TV_SYMBOL-INITIALIZING'), + ]); + }); + + // When stop and new managed calls occur while IndexedDB explicitly reports the initialization blocked. + let blockedState; + try { + blockedState = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const versionEvent = await window.__STORAGE_INITIALIZATION_GATE__.blocked; + lab.stopResolved = false; + lab.stopping = lab.controller.stop(); + lab.stopping.then(() => { lab.stopResolved = true; }); + lab.callbacks = []; + const callback = id => (error, value) => lab.callbacks.push({ id, error: error === null, value }); + const fresh = lab.create('chart_futures', { version: 2 }); + lab.tail = Promise.all([ + lab.store.getItem('#TV_SYMBOL-INITIALIZING', callback('retained-read')), + fresh.setItem('#TV_SYMBOL-INITIALIZING', 3, callback('fresh-write')), + ]); + await Promise.resolve(); + return { + versionEvent, stopResolved: lab.stopResolved, callbacks: [...lab.callbacks], + restoredFactory: lab.localforage.createInstance === lab.nativeCreateInstance, + acceptedOperations: lab.controller.getStats().acceptedOperations, + pendingOperations: lab.controller.getStats().pendingOperations, + transactions: window.__STORAGE_NATIVE__.snapshot().transactions, + }; + }); + } finally { + await page.evaluate(async () => { + const gate = window.__STORAGE_INITIALIZATION_GATE__; + IDBFactory.prototype.open = gate.nativeOpen; + gate.database.close(); + await gate.opened; + }); + } + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const accepted = await lab.accepted; + await lab.stopping; + const tail = await lab.tail; + return { + accepted, tail, callbacks: lab.callbacks, + restoredFactory: lab.localforage.createInstance === lab.nativeCreateInstance, + persisted: await lab.store.getItem('#TV_SYMBOL-INITIALIZING'), stats: lab.controller.getStats(), + }; + }); + + // Then factory restoration waits for initialization and accepted commits before native calls resume. + expect(blockedState).toEqual({ + versionEvent: { oldVersion: 1, newVersion: 2 }, + stopResolved: false, callbacks: [], restoredFactory: false, + acceptedOperations: 2, pendingOperations: 2, transactions: 0, + }); + expect(result.accepted).toEqual([1, 1]); + expect(result.tail).toEqual([1, 3]); + expect(result.callbacks).toEqual([ + { id: 'retained-read', error: true, value: 1 }, { id: 'fresh-write', error: true, value: 3 }, + ]); + expect(result.restoredFactory).toBe(true); + expect(result.persisted).toBe(3); + expect(result.stats).toMatchObject({ acceptedOperations: 2, pendingOperations: 0, pendingBytes: 0 }); +}); + +test('user rejects a save when synchronous value inspection stops admission before enqueue', async ({ page }) => { + // Given a ready store has no accepted work when a transparent Proxy invokes stop during inspection. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When value validation synchronously stops the controller before the save can enter its queue. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + let stopping; + let inspections = 0; + const value = new Proxy({ revision: 1 }, { + getPrototypeOf(target) { + inspections += 1; + stopping = lab.controller.stop(); + return Reflect.getPrototypeOf(target); + }, + }); + const callbacks = []; + const [outcome] = await Promise.allSettled([ + lab.store.setItem('#TV_SYMBOL-ADMISSION', value, (error, saved) => { + callbacks.push({ message: error?.message, value: saved }); + }), + ]); + await stopping; + return { + status: outcome.status, message: outcome.reason?.message, callbacks, inspections, + restoredFactory: lab.localforage.createInstance === lab.nativeCreateInstance, + stats: lab.controller.getStats(), metrics: window.__STORAGE_NATIVE__.snapshot(), + persisted: await lab.native.getItem('#TV_SYMBOL-ADMISSION'), + }; + }); + + // Then the stopped admission rejects once without queuing or replaying the inspected value. + expect(result.inspections).toBeGreaterThan(0); + expect(result.status).toBe('rejected'); + expect(result.message).toMatch(/stopped|active|admission/i); + expect(result.callbacks).toEqual([{ message: result.message, value: undefined }]); + expect(result.restoredFactory).toBe(true); + expect(result.stats).toMatchObject({ acceptedOperations: 0, rejectedOperations: 1, pendingOperations: 0, pendingBytes: 0 }); + expect(result.metrics).toMatchObject({ puts: 0, transactions: 0 }); + expect(result.persisted).toBe(null); +}); + +test('user can stop from a save callback and continue saving without blocking the remaining accepted batches', async ({ page }) => { + // Given each accepted write uses its own real transaction and a ready retained store. + await openStorageLab(page, { options: { maxBatchOperations: 1 } }); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When the first callback stops optimization and immediately saves through the retained instance. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const callbacks = []; + let stopping; + let resumed; + const accepted = await Promise.all([1, 2, 3].map(value => lab.store.setItem('#TV_SYMBOL-REENTRANT', value, (error, actual) => { + callbacks.push({ phase: 'accepted', error: error === null, value: actual }); + if (value === 1) { + stopping = lab.controller.stop(); + resumed = Promise.allSettled([ + lab.store.setItem('#TV_SYMBOL-REENTRANT', 4, (failure, saved) => { + callbacks.push({ phase: 'native', error: failure === null, value: saved }); + }), + ]); + } + }))); + await stopping; + const tail = await resumed; + return { + accepted, tail, callbacks, stats: lab.controller.getStats(), + persisted: await lab.native.getItem('#TV_SYMBOL-REENTRANT'), + metrics: window.__STORAGE_NATIVE__.snapshot(), + }; + }); + + // Then all accepted callbacks settle and the native continuation saves the final revision once. + expect(result.accepted).toEqual([1, 2, 3]); + expect(result.tail).toEqual([{ status: 'fulfilled', value: 4 }]); + expect(result.callbacks).toEqual([ + { phase: 'accepted', error: true, value: 1 }, + { phase: 'accepted', error: true, value: 2 }, + { phase: 'accepted', error: true, value: 3 }, + { phase: 'native', error: true, value: 4 }, + ]); + expect(result.persisted).toBe(4); + expect(result.metrics.puts).toBe(4); + expect(result.stats).toMatchObject({ acceptedOperations: 3, failedOperations: 0, pendingOperations: 0 }); +}); + +test('user receives every aborted batch callback and resumes native storage after stop without retrying failed puts', async ({ page }) => { + // Given committed chart values and the next real put aborts its native transaction. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await Promise.all(['A', 'B'].map(key => lab.native.setItem(`#TV_SYMBOL-${key}`, 1))); + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + window.__STORAGE_NATIVE__.abortNextPut(); + }); + + // When stop drains an aborting batch and the retained store immediately queues a native continuation. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const callbacks = []; + const callback = id => (error, value) => callbacks.push({ id, name: error?.name, value }); + const accepted = Promise.allSettled([ + lab.store.setItem('#TV_SYMBOL-A', 2, callback('write-A')), + lab.store.setItem('#TV_SYMBOL-B', 2, callback('write-B')), + lab.store.getItem('#TV_SYMBOL-A', callback('read-A')), + ]); + const stopping = lab.controller.stop(); + const tail = Promise.allSettled([lab.store.setItem('#TV_SYMBOL-NATIVE', 3, callback('native'))]); + const outcomes = await accepted; + await stopping; + const resumed = await tail; + return { + statuses: outcomes.map(value => value.status), names: outcomes.map(value => value.reason.name), + resumed, callbacks, stats: lab.controller.getStats(), metrics: window.__STORAGE_NATIVE__.snapshot(), + persisted: await Promise.all(['A', 'B', 'NATIVE'].map(key => lab.native.getItem(`#TV_SYMBOL-${key}`))), + }; + }); + + // Then each failed call is notified once and only the successful native continuation changes storage. + expect(result.statuses).toEqual(['rejected', 'rejected', 'rejected']); + expect(result.names).toEqual(['AbortError', 'AbortError', 'AbortError']); + expect(result.resumed).toEqual([{ status: 'fulfilled', value: 3 }]); + expect(result.callbacks).toEqual([ + { id: 'write-A', name: 'AbortError', value: undefined }, + { id: 'write-B', name: 'AbortError', value: undefined }, + { id: 'read-A', name: 'AbortError', value: undefined }, + { id: 'native', name: undefined, value: 3 }, + ]); + expect(result.persisted).toEqual([1, 1, 3]); + expect(result.metrics).toMatchObject({ puts: 2, aborted: 1, pending: 0 }); + expect(result.stats).toMatchObject({ acceptedOperations: 3, failedOperations: 3, pendingOperations: 0, pendingBytes: 0 }); +}); + +test('user keeps native callbacks configuration and Blob storage through an existing facade after stop', async ({ page }) => { + // Given an unused managed facade survives stopping before it initializes a database. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.controller.stop(); + }); + + // When the retained instance configures and uses the original native storage API. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const store = lab.store; + const configured = store.config({ description: 'Native storage after stop' }); + const configuration = store.config(); + let readyArgs; + await store.ready((...args) => { readyArgs = args; }); + const blob = new Blob(['native chart payload'], { type: 'text/plain' }); + let savedCallback; + const saved = await store.setItem('#TV_SYMBOL-BLOB', blob, (error, value) => { + savedCallback = { error: error === null, sameBlob: value === blob }; + }); + const restored = await store.getItem('#TV_SYMBOL-BLOB'); + await store.setItem('native-number', 7); + const keys = await store.keys(); + const length = await store.length(); + const firstKey = await store.key(0); + const driver = await store.getDriver(store.driver()); + const entries = []; + await store.iterate((value, key) => { entries.push({ key, blob: value instanceof Blob }); }); + await store.removeItem('native-number'); + const removed = await store.getItem('native-number'); + await store.clear(); + return { + configured, description: configuration.description, configIdentity: store.config() === configuration, + readyArgs, driver: store.driver(), driverMatches: driver._driver === lab.localforage.INDEXEDDB, + savedIsBlob: saved === blob, savedCallback, + restoredBlob: { type: restored.type, content: await restored.text(), isBlob: restored instanceof Blob }, + keys, length, firstKey, entries, removed, remaining: await store.keys(), stats: lab.controller.getStats(), + }; + }); + + // Then callbacks retain native signatures and unrestricted native values and methods remain usable. + expect(result.configured).toBe(true); + expect(result.description).toBe('Native storage after stop'); + expect(result.configIdentity).toBe(true); + expect(result.readyArgs).toEqual([undefined]); + expect(result.driver).toBe('asyncStorage'); + expect(result.driverMatches).toBe(true); + expect(result.savedIsBlob).toBe(true); + expect(result.savedCallback).toEqual({ error: true, sameBlob: true }); + expect(result.restoredBlob).toEqual({ type: 'text/plain', content: 'native chart payload', isBlob: true }); + expect(result.keys).toEqual(['#TV_SYMBOL-BLOB', 'native-number']); + expect(result.length).toBe(2); + expect(result.firstKey).toBe('#TV_SYMBOL-BLOB'); + expect(result.entries).toEqual([{ key: '#TV_SYMBOL-BLOB', blob: true }, { key: 'native-number', blob: false }]); + expect(result.removed).toBe(null); + expect(result.remaining).toEqual([]); + expect(result.stats).toMatchObject({ acceptedOperations: 0, rejectedOperations: 0, transactions: 0 }); +}); + +for (const placement of ['inherited', 'own']) { + test(`user restores the exact ${placement} factory descriptor and cannot reinstall until reload`, async ({ page }) => { + // Given the actual library has either its inherited factory or an explicit own descriptor. + await openStorageLab(page, { install: false }); + await page.evaluate(placement => { + const lab = window.__STORAGE_LAB__; + if (placement === 'own') { + Object.defineProperty(lab.localforage, 'createInstance', { + value: lab.nativeCreateInstance, writable: true, enumerable: false, configurable: true, + }); + } + lab.originalDescriptor = Object.getOwnPropertyDescriptor(lab.localforage, 'createInstance'); + }, placement); + + // When the prototype stops repeatedly and a caller attempts to install it again on the same export. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const { installChartStoragePrototype } = await import('/adapter.js'); + const controller = installChartStoragePrototype(lab.localforage); + const retainedFactory = lab.localforage.createInstance; + const stopping = controller.stop(); + const samePendingStop = controller.stop() === stopping; + await stopping; + const sameCompletedStop = controller.stop() === stopping; + const restoredDescriptor = Object.getOwnPropertyDescriptor(lab.localforage, 'createInstance'); + const descriptorMatches = lab.originalDescriptor === undefined + ? restoredDescriptor === undefined + : Object.keys(lab.originalDescriptor).every(key => restoredDescriptor[key] === lab.originalDescriptor[key]); + const restoredFactory = lab.localforage.createInstance === lab.nativeCreateInstance; + let reinstallError; + try { + const reinstalled = installChartStoragePrototype(lab.localforage); + await reinstalled.stop(); + } catch (error) { + reinstallError = error.message; + } + let retainedFactoryOutcome; + try { + const fresh = retainedFactory({ name: 'chart_futures', storeName: 'keyvaluepairs', driver: lab.localforage.INDEXEDDB }); + retainedFactoryOutcome = { status: 'fulfilled', value: await fresh.setItem('#TV_SYMBOL-FACTORY', 5) }; + } catch (error) { + retainedFactoryOutcome = { status: 'rejected', message: error.message }; + } + return { + samePendingStop, sameCompletedStop, descriptorMatches, restoredFactory, reinstallError, retainedFactoryOutcome, + originallyOwn: lab.originalDescriptor !== undefined, restoredOwn: restoredDescriptor !== undefined, + }; + }); + + // Then stop is idempotent, restores property ownership exactly, and retained factories stay native. + expect(result.samePendingStop).toBe(true); + expect(result.sameCompletedStop).toBe(true); + expect(result.descriptorMatches).toBe(true); + expect(result.restoredFactory).toBe(true); + expect(result.originallyOwn).toBe(placement === 'own'); + expect(result.restoredOwn).toBe(placement === 'own'); + expect(result.reinstallError).toMatch(/already|reload|install/i); + expect(result.retainedFactoryOutcome).toEqual({ status: 'fulfilled', value: 5 }); + }); +} diff --git a/e2e/binance-orderbook/specs/chart-storage-mirror-scoped.pw.js b/e2e/binance-orderbook/specs/chart-storage-mirror-scoped.pw.js new file mode 100644 index 0000000..176960f --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-mirror-scoped.pw.js @@ -0,0 +1,421 @@ +import { readFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { test, expect } from '../test.js'; +import { openChartMirrorScoped, openMirrorBootstrapLab } from '../helpers/chart-storage-mirror-scoped.js'; +import { STORAGE_LAB_ORIGIN } from '../helpers/chart-storage-lab.js'; + +test('user receives a statically generated mirror factory with exactly one pinned expression replacement', async ({ page }) => { + // Given the generated candidate and its source provenance accompany the callback fixture. + const root = new URL('../../../test/fixtures/binance-chart-storage/', import.meta.url); + const manifest = JSON.parse(await readFile(new URL('mirror-scoped-manifest.json', root), 'utf8')); + await openMirrorBootstrapLab(page); + + // When strict registration validation accepts the exact original and rejects a source mismatch. + const { original, changed, failure } = await page.evaluate(async () => { + const module = await import('/mirror-module.js'); + const fixture = await import('/mirror-scoped-callback.js'); + const native = fixture.createNativeMirrorFactory(); + const replacement = module.replaceChartMirrorFactory(native, (_target, _entries, thunk) => thunk()); + let failure; + try { module.replaceChartMirrorFactory(function mismatched() { return 1; }, () => []); } + catch (error) { failure = error.message; } + return { original: Function.prototype.toString.call(native), changed: Function.prototype.toString.call(replacement), failure }; + }); + const [start, end] = manifest.replacement.factory_utf16_range; + const inserted = changed.slice(start, changed.length - (original.length - end)); + + // Then reversing the single inserted dispatch reproduces every byte of the public factory. + expect(createHash('sha256').update(original).digest('hex')).toBe(manifest.original_factory.sha256); + expect(changed.slice(0, start) + original.slice(start, end) + changed.slice(start + inserted.length)).toBe(original); + expect(inserted).toBe(`__dispatchChartMirror(Sr,fr,()=>${original.slice(start, end)})`); + expect(failure).toBe('Mirror factory source does not match the pinned public module'); +}); + +for (const mode of ['native', 'scoped']) { + test(`user retains native same-stack set get remove and clear results with ${mode}`, async ({ page }) => { + // Given a ready unmodified instance starts each operation matrix with one original value. + await openChartMirrorScoped(page, { mode }); + await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + state.matrixStore = state.localforage.createInstance({ name: 'chart_futures' }); + await state.matrixStore.ready(); + state.matrixMethods = ['setItem', 'getItem', 'removeItem', 'clear'].map(name => state.matrixStore[name]); + }); + + // When each public operation sequence starts in one stack without waiting between its calls. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + const store = state.matrixStore; + const outcomes = []; + for (const operations of [['set', 'get'], ['set', 'remove', 'get'], ['set', 'clear', 'get'], ['get', 'remove', 'set', 'get']]) { + await store.clear(); + await store.setItem('myTradingView', { revision: 'original' }); + const callbacks = []; + const pending = operations.map((operation, index) => { + const callback = (error, value) => callbacks.push({ index, operation, error, value }); + if (operation === 'set') return store.setItem('myTradingView', { revision: 'new' }, callback); + if (operation === 'get') return store.getItem('myTradingView', callback); + if (operation === 'remove') return store.removeItem('myTradingView', callback); + return store.clear(callback); + }); + outcomes.push({ values: await Promise.all(pending), callbacks, finalValue: await store.getItem('myTradingView') }); + } + return { outcomes, dispatches: state.dispatches, + sameMethods: ['setItem', 'getItem', 'removeItem', 'clear'].every((name, index) => store[name] === state.matrixMethods[index]), + sameFactory: state.localforage.createInstance === state.nativeCreateInstance }; + }); + + // Then native delayed set preparation preserves concrete values and callback ordering in every matrix. + const before = { revision: 'original' }; + const after = { revision: 'new' }; + const callback = (index, operation, value) => ({ index, operation, error: null, value }); + expect(result).toEqual({ outcomes: [ + { values: [after, before], callbacks: [callback(1, 'get', before), callback(0, 'set', after)], finalValue: after }, + { values: [after, undefined, null], callbacks: [callback(1, 'remove', undefined), callback(2, 'get', null), callback(0, 'set', after)], finalValue: after }, + { values: [after, undefined, null], callbacks: [callback(1, 'clear', undefined), callback(2, 'get', null), callback(0, 'set', after)], finalValue: after }, + { values: [before, undefined, after, null], callbacks: [callback(0, 'get', before), callback(1, 'remove', undefined), callback(3, 'get', null), callback(2, 'set', after)], finalValue: after }, + ], dispatches: 0, sameMethods: true, sameFactory: true }); + }); + + test(`user preserves ordinary storage API identity and native concurrent clear ordering with ${mode}`, async ({ page }) => { + // Given real localForage and Basic storage are ready before the mirror writer is used. + await openChartMirrorScoped(page, { mode }); + await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + state.tv = window.__STORAGE_LAB__.create(); + state.basic = window.__CHART_HOST__.basic.create({ namespace: 'chart_futures', symbol: 'BTCUSDT' }); + await state.tv.setItem('myTradingView', { revision: 0 }); + await state.basic.saveSettings({ background: 'initial' }); + state.setItem = state.tv.setItem; + state.clear = state.tv.clear; + }); + + // When native writes and the original Basic clear are issued in the same stack. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + await Promise.all([ + state.tv.setItem('#TV_SYMBOL-BEFORE', []), state.basic.saveSettings({ background: 'before-clear' }), + state.basic.clear(), state.tv.setItem('#TV_SYMBOL-AFTER', []), + ]); + return { + keys: (await state.tv.keys()).sort(), settings: await state.tv.getItem('candlestick-setting'), + snapshot: await state.tv.getItem('myTradingView'), dispatches: state.dispatches, + identity: state.localforage.createInstance === state.nativeCreateInstance && state.tv.setItem === state.setItem && state.tv.clear === state.clear, + }; + }); + + // Then the same native ordering survives and no ordinary storage call enters the scoped writer. + expect(result).toEqual({ + keys: ['#TV_SYMBOL-AFTER', '#TV_SYMBOL-BEFORE', 'candlestick-setting'], settings: { background: 'before-clear' }, + snapshot: null, dispatches: 0, identity: true, + }); + }); + + test(`user mirrors the original serial save chain while retaining null values and target-only keys with ${mode}`, async ({ page }) => { + // Given source and target hold independent Basic settings and an explicit null drawing. + await openChartMirrorScoped(page, { mode }); + await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + state.source = state.localforage.createInstance({ name: 'chart_futures' }); + state.target = state.localforage.createInstance({ name: 'chart_delivery' }); + await state.source.setItem('#TV_SYMBOL-NULL', null); + await state.source.setItem('candlestick-setting', { theme: 'source' }); + await state.target.setItem('candlestick-setting', { theme: 'target' }); + await state.target.setItem('#TV_SYMBOL-TARGET-ONLY', []); + state.save = window.__CHART_HOST__.tradingView.createSaveCallback(state.source, 'myTradingView', {}, state.create('chart_futures')); + window.__STORAGE_NATIVE__.reset(); + }); + + // When the unchanged original save callback awaits three successive mirror callbacks. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + for (const revision of [1, 1, 2]) await state.save({ layout: 's', revision, charts: [] }); + const metrics = window.__STORAGE_NATIVE__.snapshot(); + return { + source: await state.source.getItem('myTradingView'), target: await state.target.getItem('myTradingView'), + targetKeys: (await state.target.keys()).sort(), targetNull: await state.target.getItem('#TV_SYMBOL-NULL'), + sourceBasic: await state.source.getItem('candlestick-setting'), targetBasic: await state.target.getItem('candlestick-setting'), + targetOnly: await state.target.getItem('#TV_SYMBOL-TARGET-ONLY'), metrics, dispatches: state.dispatches, + }; + }); + + // Then snapshots match, null remains a stored key, and only target mirror writes use fewer transactions. + expect(result.source).toEqual({ layout: 's', revision: 2, charts: [] }); + expect(result.target).toEqual(result.source); + expect(result.targetKeys).toEqual(['#TV_SYMBOL-NULL', '#TV_SYMBOL-TARGET-ONLY', 'candlestick-setting', 'myTradingView', 'myTradingView.layout']); + expect(result.targetNull).toBe(null); + expect(result.sourceBasic).toEqual({ theme: 'source' }); + expect(result.targetBasic).toEqual({ theme: 'target' }); + expect(result.targetOnly).toEqual([]); + expect(result.metrics.puts).toBe(mode === 'native' ? 15 : 10); + expect(result.metrics.transactions).toBe(mode === 'native' ? 30 : 24); + expect(result.metrics.pending).toBe(0); + expect(result.metrics.aborted).toBe(0); + expect(result.dispatches).toBe(mode === 'native' ? 0 : 3); + }); + + test(`user keeps native mirror guards and empty-source behavior with ${mode}`, async ({ page }) => { + // Given only an unrelated Basic key exists and both mirror control refs are available. + await openChartMirrorScoped(page, { mode }); + await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + await window.__STORAGE_LAB__.native.setItem('candlestick-setting', { theme: 'source' }); + state.blocked = state.create('chart_futures', { current: true }, { current: true }); + state.disabled = state.create('chart_futures', { current: false }, { current: false }); + window.__STORAGE_NATIVE__.reset(); + }); + + // When guarded callbacks return before storage and an enabled callback sees zero mirror entries. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + const guarded = await Promise.all([state.blocked(), state.disabled()]); + const guards = window.__STORAGE_NATIVE__.snapshot(); + await state.create('chart_futures')(); + const empty = window.__STORAGE_NATIVE__.snapshot(); + const target = state.localforage.createInstance({ name: 'chart_delivery' }); + return { guarded, guards, empty, targetKeys: await target.keys(), dispatches: state.dispatches }; + }); + + // Then neither guard performs IO and the empty mirror issues only its native source-key read. + expect(result.guarded).toEqual([undefined, undefined]); + expect(result.guards.transactions).toBe(0); + expect(result.empty.transactions).toBe(1); + expect(result.empty.puts).toBe(0); + expect(result.targetKeys).toEqual([]); + expect(result.dispatches).toBe(mode === 'native' ? 0 : 1); + }); +} + +for (const order of ['runtime-first', 'module-first']) { + test(`user replaces only the selected mirror module before first require with ${order}`, async ({ page }) => { + // Given the empty queue observer has an explicit synthetic 70940 replacement and an unrelated module. + await openMirrorBootstrapLab(page); + await page.evaluate(async () => { + const { observeChartStorageBootstrap } = await import('/bootstrap.js'); + const state = { replacements: 0, originalExecutions: 0, patchedExecutions: 0 }; + state.original = function (module) { state.originalExecutions += 1; module.exports = { value: 'original' }; }; + state.patched = function (module) { state.patchedExecutions += 1; module.exports = { value: 'patched' }; }; + state.observer = observeChartStorageBootstrap({ replaceMirrorFactory(original) { + if (original !== state.original) throw new Error('Unexpected synthetic factory'); + state.replacements += 1; + return state.patched; + } }); + state.chunk = [['mirror-scoped-synthetic'], { 70940: state.original, 43917(module) { module.exports = { value: 'untouched' }; } }, require => { + state.require = require; + state.first = require(70940); + state.other = require(43917); + }]; + window.__MIRROR_ENGINE__ = state; + }); + + // When the actual Rspack runtime consumes the synthetic module in either registration order. + if (order === 'runtime-first') await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + await page.evaluate(() => self.webpackChunkfutures_trade_ui.push(window.__MIRROR_ENGINE__.chunk)); + if (order === 'module-first') await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + const result = await page.evaluate(async () => { + const state = window.__MIRROR_ENGINE__; + const captured = await state.observer.captured; + state.observer.stop(); + return { + first: state.first, other: state.other, sameExport: state.require(70940) === captured, + replacements: state.replacements, originalExecutions: state.originalExecutions, patchedExecutions: state.patchedExecutions, + restored: state.require.m[70940] === state.original, + queueRestored: Object.hasOwn(Object.getOwnPropertyDescriptor(self.webpackChunkfutures_trade_ui, 'push'), 'value'), + }; + }); + + // Then the cached transformed export survives observer cleanup while future factory and queue slots are restored. + expect(result).toEqual({ first: { value: 'patched' }, other: { value: 'untouched' }, sameExport: true, + replacements: 1, originalExecutions: 0, patchedExecutions: 1, restored: true, queueRestored: true }); + }); +} + +test('user keeps the native module executable when strict mirror registration validation rejects', async ({ page }) => { + // Given an early observer uses the real strict replacement against a mismatched synthetic factory. + await openMirrorBootstrapLab(page); + await page.evaluate(async () => { + const { observeChartStorageBootstrap } = await import('/bootstrap.js'); + const { replaceChartMirrorFactory } = await import('/mirror-module.js'); + const state = { executions: 0 }; + state.original = function (module) { state.executions += 1; module.exports = { value: 'native' }; }; + state.observer = observeChartStorageBootstrap({ replaceMirrorFactory: original => replaceChartMirrorFactory(original, (_target, _entries, thunk) => thunk()) }); + state.failure = state.observer.captured.then(() => 'unexpected-success', error => error.message); + window.__MIRROR_ENGINE__ = state; + }); + + // When actual runtime registration rejects transformation before the native factory executes. + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + const result = await page.evaluate(async () => { + const state = window.__MIRROR_ENGINE__; + self.webpackChunkfutures_trade_ui.push([['mirror-scoped-rejected'], { 70940: state.original }, require => { state.require = require; state.first = require(70940); }]); + return { failure: await state.failure, first: state.first, sameExport: state.require(70940) === state.first, + executions: state.executions, restored: state.require.m[70940] === state.original }; + }); + + // Then observer rejection cannot enter the runtime module-error cache or alter native exports. + expect(result).toEqual({ failure: 'Mirror factory source does not match the pinned public module', first: { value: 'native' }, + sameExport: true, executions: 1, restored: true }); +}); + +for (const runtimeStarted of [false, true]) { + test(`user retains the native factory when replacement synchronously stops observation with runtime started ${runtimeStarted}`, async ({ page }) => { + // Given the synchronous replacement callback stops its observer before returning a candidate factory. + await openMirrorBootstrapLab(page); + await page.evaluate(async () => { + const { observeChartStorageBootstrap } = await import('/bootstrap.js'); + const state = { originalExecutions: 0, patchedExecutions: 0 }; + state.original = function (module) { state.originalExecutions += 1; module.exports = { value: 'native' }; }; + state.patched = function (module) { state.patchedExecutions += 1; module.exports = { value: 'patched' }; }; + state.observer = observeChartStorageBootstrap({ replaceMirrorFactory() { + state.observer.stop(); + return state.patched; + } }); + state.failure = state.observer.captured.then(() => 'unexpected-success', error => error.message); + window.__MIRROR_ENGINE__ = state; + }); + + // When the real runtime consumes that registration before the host's first require. + if (runtimeStarted) await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + await page.evaluate(() => { + const state = window.__MIRROR_ENGINE__; + self.webpackChunkfutures_trade_ui.push([['mirror-scoped-stop-during-replacement'], { 70940: state.original }, require => { + state.require = require; + state.first = require(70940); + }]); + }); + if (!runtimeStarted) await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + const result = await page.evaluate(async () => { + const state = window.__MIRROR_ENGINE__; + const queue = self.webpackChunkfutures_trade_ui; + return { failure: await state.failure, first: state.first, sameExport: state.require(70940) === state.first, + originalExecutions: state.originalExecutions, patchedExecutions: state.patchedExecutions, + originalFactory: state.require.m[70940] === state.original, + queuedOriginalFactory: queue.find(chunk => Object.hasOwn(chunk[1], '70940'))[1][70940] === state.original, + queueRestored: Object.hasOwn(Object.getOwnPropertyDescriptor(queue, 'push'), 'value') }; + }); + + // Then stop rejects capture while the original factory executes once and owns the unchanged runtime cache. + expect(result).toEqual({ failure: 'Storage bootstrap stopped before module execution', first: { value: 'native' }, + sameExport: true, originalExecutions: 1, patchedExecutions: 0, originalFactory: true, queuedOriginalFactory: true, queueRestored: true }); + }); + + test(`user restores the original unexecuted mirror factory when stopped with runtime started ${runtimeStarted}`, async ({ page }) => { + // Given a replaced synthetic mirror factory has registered without being required. + await openMirrorBootstrapLab(page); + await page.evaluate(async () => { + const { observeChartStorageBootstrap } = await import('/bootstrap.js'); + const state = { executions: 0 }; + state.original = function (module) { state.executions += 1; module.exports = { value: 'native-after-stop' }; }; + state.observer = observeChartStorageBootstrap({ replaceMirrorFactory: () => function () { throw new Error('Stopped replacement executed'); } }); + state.failure = state.observer.captured.then(() => 'unexpected-success', error => error.message); + window.__MIRROR_ENGINE__ = state; + }); + if (runtimeStarted) await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + await page.evaluate(() => self.webpackChunkfutures_trade_ui.push([['mirror-scoped-stopped'], { 70940: window.__MIRROR_ENGINE__.original }])); + + // When stop restores the registered factory before the actual runtime requires it. + await page.evaluate(() => window.__MIRROR_ENGINE__.observer.stop()); + if (!runtimeStarted) await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/webpack-runtime.js` }); + const result = await page.evaluate(async () => { + const state = window.__MIRROR_ENGINE__; + self.webpackChunkfutures_trade_ui.push([['mirror-scoped-stopped-entry'], {}, require => { state.require = require; state.value = require(70940); }]); + return { value: state.value, executions: state.executions, restored: state.require.m[70940] === state.original, + failure: await state.failure, queueRestored: Object.hasOwn(Object.getOwnPropertyDescriptor(self.webpackChunkfutures_trade_ui, 'push'), 'value') }; + }); + + // Then only the original factory executes and the observer reports its explicit pre-execution stop. + expect(result).toEqual({ value: { value: 'native-after-stop' }, executions: 1, restored: true, + failure: 'Storage bootstrap stopped before module execution', queueRestored: true }); + }); +} + +test('user resumes the exact native per-key mirror map through retained callbacks after the scoped writer stops', async ({ page }) => { + // Given one retained transformed mirror callback points to a source with three mirror entries. + await openChartMirrorScoped(page); + await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + const source = state.localforage.createInstance({ name: 'chart_futures' }); + await Promise.all([source.setItem('myTradingView', { revision: 1 }), source.setItem('myTradingView.layout', []), source.setItem('#TV_SYMBOL-NULL', null)]); + state.retained = state.create('chart_futures'); + await state.writer.stop(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When the unchanged surrounding host callback calls dispatch after the writer has stopped. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + await state.retained(); + const metrics = window.__STORAGE_NATIVE__.snapshot(); + const target = state.localforage.createInstance({ name: 'chart_delivery' }); + return { metrics, keys: (await target.keys()).sort(), snapshot: await target.getItem('myTradingView'), dispatches: state.dispatches, + createInstanceUnchanged: state.localforage.createInstance === state.nativeCreateInstance }; + }); + + // Then three native target transactions preserve the original map and native instance factory identity. + expect(result.metrics.transactions).toBe(7); + expect(result.metrics.puts).toBe(3); + expect(result.metrics.pending).toBe(0); + expect(result.keys).toEqual(['#TV_SYMBOL-NULL', 'myTradingView', 'myTradingView.layout']); + expect(result.snapshot).toEqual({ revision: 1 }); + expect(result.dispatches).toBe(1); + expect(result.createInstanceUnchanged).toBe(true); +}); + +for (const mode of ['native', 'scoped']) { + test(`user preserves all historical mirror data through thirty-two real host save chains with ${mode}`, async ({ page }, testInfo) => { + // Given 290 historical drawing keys already exist in each independently initialized database. + test.setTimeout(30_000); + await openChartMirrorScoped(page, { mode }); + await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + state.source = state.localforage.createInstance({ name: 'chart_futures' }); + state.target = state.localforage.createInstance({ name: 'chart_delivery' }); + state.history = Array.from({ length: 290 }, (_, index) => ({ key: `#TV_SYMBOL-LAB${index}`, value: [{ id: `history-${index}`, points: [{ time: 1000, price: index }] }] })); + await Promise.all(state.history.flatMap(({ key, value }) => [state.source.setItem(key, value), state.target.setItem(key, value)])); + state.initial = { layout: 's', revision: 0, charts: [{ chartId: '0', panes: [{ mainSourceId: 'main-BTCUSDT', sources: [ + { id: 'main-BTCUSDT', type: 'MainSeries', state: { symbol: 'BTCUSDT' } }, + { id: 'drawing-BTCUSDT', type: 'LineToolTrendLine', ownerSource: 'main-BTCUSDT', state: { symbol: 'BTCUSDT' }, points: [{ time: 1000, price: 20 }, { time: 2000, price: 25 }] }, + ] }] }] }; + state.save = window.__CHART_HOST__.tradingView.createSaveCallback(state.source, 'myTradingView', {}, state.create('chart_futures')); + await state.save(structuredClone(state.initial)); + window.__STORAGE_NATIVE__.reset(); + state.dispatches = 0; + }); + + // When eight revisions each complete four concurrent unchanged host save and mirror chains. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_SCOPED__; + let completed = 0; + const started = performance.now(); + for (let revision = 1; revision <= 8; revision += 1) { + const snapshot = structuredClone(state.initial); + snapshot.revision = revision; + snapshot.charts[0].panes[0].sources[1].points[1].price = 25 + revision; + completed += (await Promise.all(Array.from({ length: 4 }, () => state.save(snapshot)))).length; + } + const elapsedMs = performance.now() - started; + const metrics = window.__STORAGE_NATIVE__.snapshot(); + const read = async storage => ({ keys: (await storage.keys()).sort(), history: await Promise.all(state.history.map(async ({ key }) => ({ key, value: await storage.getItem(key) }))), + loaded: await window.__CHART_HOST__.tradingView.load({ storage, key: 'myTradingView' }) }); + return { completed, elapsedMs, metrics, dispatches: state.dispatches, source: await read(state.source), target: await read(state.target), + expectedHistory: state.history, createInstanceUnchanged: state.localforage.createInstance === state.nativeCreateInstance }; + }); + await testInfo.attach(`mirror-scoped-load-${mode}.json`, { body: JSON.stringify({ mode, completed: result.completed, elapsedMs: result.elapsedMs, metrics: result.metrics }, null, 2), contentType: 'application/json' }); + + // Then both full datasets retain every historical value and the final drawing revision without facade changes. + expect(result.completed).toBe(32); + expect(result.source).toEqual(result.target); + expect(result.source.history).toEqual(result.expectedHistory); + expect(result.source.keys).toEqual([...result.expectedHistory.map(({ key }) => key), '#TV_SYMBOL-BTCUSDT', 'myTradingView', 'myTradingView.layout'].sort()); + expect(result.source.loaded.chartSave.revision).toBe(8); + expect(result.source.loaded.chartSave.charts[0].panes[0].sources[1].points[1].price).toBe(33); + expect(result.metrics.transactions).toBe((mode === 'native' ? 591 : 299) * 32); + expect(result.metrics.puts).toBe(mode === 'native' ? 296 * 32 : 112); + expect(result.metrics.pending).toBe(0); + expect(result.metrics.aborted).toBe(0); + expect(result.metrics.completed).toBe(result.metrics.transactions); + expect(result.dispatches).toBe(mode === 'native' ? 0 : 32); + expect(result.elapsedMs).toBeGreaterThan(0); + expect(result.createInstanceUnchanged).toBe(true); + }); +} diff --git a/e2e/binance-orderbook/specs/chart-storage-mirror-writer.pw.js b/e2e/binance-orderbook/specs/chart-storage-mirror-writer.pw.js new file mode 100644 index 0000000..455663e --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-mirror-writer.pw.js @@ -0,0 +1,556 @@ +import { test, expect } from '../test.js'; +import { openStorageLab, STORAGE_LAB_ORIGIN } from '../helpers/chart-storage-lab.js'; +import { openMirrorWriterLab, holdMirrorWriteLock, releaseMirrorWriteLock } from '../helpers/chart-storage-mirror-writer.js'; + +test('user mirrors changed and missing values atomically while preserving equal values native APIs and copied input', async ({ page }) => { + // Given a ready native target has equal, changed, explicit-null and target-only records. + await openMirrorWriterLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const state = window.__MIRROR_WRITER__; + await Promise.all([ + lab.native.setItem('myTradingView.equal', { b: [2, null], a: 1 }), + lab.native.setItem('myTradingView.changed', { revision: 0 }), + lab.native.setItem('#TV_SYMBOL-NULL', null), + lab.native.setItem('#TV_SYMBOL-TARGET-ONLY', ['keep']), + lab.native.setItem('candlestick-setting', { theme: 'target' }), + ]); + state.methods = Object.fromEntries(['setItem', 'getItem', 'clear', 'ready', 'createInstance'].map(name => [name, lab.native[name]])); + state.factory = lab.localforage.createInstance; + state.beforeOwnKeys = Reflect.ownKeys(lab.native); + window.__STORAGE_NATIVE__.reset(); + }); + + // When the caller mutates its input immediately after dispatch and repeats the null batch. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const state = window.__MIRROR_WRITER__; + const changed = { revision: 1, charts: [{ symbol: 'BTCUSDT' }] }; + const entries = [ + ['myTradingView.equal', { a: 1, b: [2, null] }], + ['myTradingView.changed', changed], ['#TV_SYMBOL-NULL', null], ['#TV_SYMBOL-MISSING', null], + ]; + const before = JSON.stringify(entries); + const dispatched = state.writer.dispatch(lab.native, entries, state.nativeThunk); + const unchangedByDispatch = JSON.stringify(entries) === before; + changed.revision = 99; + changed.charts[0].symbol = 'ETHUSDT'; + entries[0][0] = 'myTradingView.caller-mutated'; + const outcomes = await Promise.allSettled(dispatched); + const first = window.__STORAGE_NATIVE__.snapshot(); + const repeated = await Promise.allSettled(state.writer.dispatch(lab.native, [ + ['#TV_SYMBOL-NULL', null], ['#TV_SYMBOL-MISSING', null], + ], state.nativeThunk)); + const metrics = window.__STORAGE_NATIVE__.snapshot(); + const persisted = await Promise.all(['myTradingView.equal', 'myTradingView.changed', '#TV_SYMBOL-NULL', '#TV_SYMBOL-MISSING', '#TV_SYMBOL-TARGET-ONLY', 'candlestick-setting'].map(key => lab.native.getItem(key))); + return { + outcomes, repeated, first, metrics, persisted, keys: (await lab.native.keys()).sort(), + unchangedByDispatch, callerValue: changed, nativeCalls: state.nativeCalls, stats: state.writer.getStats(), + equalKeyOrder: Object.keys(persisted[0]), + identity: state.factory === lab.localforage.createInstance && Object.entries(state.methods).every(([name, method]) => lab.native[name] === method), + ownKeysUnchanged: JSON.stringify(Reflect.ownKeys(lab.native)) === JSON.stringify(state.beforeOwnKeys), + }; + }); + + // Then one transaction per batch changes only two keys and keeps native identity and caller ownership. + expect(result.outcomes).toEqual([{ status: 'fulfilled', value: undefined }]); + expect(result.repeated).toEqual([{ status: 'fulfilled', value: undefined }]); + expect(result.first).toMatchObject({ transactions: 1, completed: 1, puts: 2, pending: 0 }); + expect(result.metrics).toMatchObject({ transactions: 2, completed: 2, puts: 2, pending: 0 }); + expect(result.persisted).toEqual([{ b: [2, null], a: 1 }, { revision: 1, charts: [{ symbol: 'BTCUSDT' }] }, null, null, ['keep'], { theme: 'target' }]); + expect(result.keys).toEqual(['#TV_SYMBOL-MISSING', '#TV_SYMBOL-NULL', '#TV_SYMBOL-TARGET-ONLY', 'candlestick-setting', 'myTradingView.changed', 'myTradingView.equal']); + expect(result.equalKeyOrder).toEqual(['b', 'a']); + expect(result.unchangedByDispatch).toBe(true); + expect(result.callerValue).toEqual({ revision: 99, charts: [{ symbol: 'ETHUSDT' }] }); + expect(result.identity).toBe(true); + expect(result.ownKeysUnchanged).toBe(true); + expect(result.nativeCalls).toBe(0); + expect(result.stats).toMatchObject({ acceptedBatches: 2, committedWrites: 2, skippedWrites: 4, pendingBatches: 0, pendingBytes: 0 }); +}); + +test('user receives one rejected bulk result and keeps every original record after a real second-put abort', async ({ page }) => { + // Given two committed records and a native observer that aborts after the second successful put. + await openMirrorWriterLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await lab.native.setItem('myTradingView.A', { revision: 0 }); + await lab.native.setItem('myTradingView.B', { revision: 0 }); + const nativePut = IDBObjectStore.prototype.put; + let puts = 0; + window.__MIRROR_WRITER__.restorePut = () => { IDBObjectStore.prototype.put = nativePut; }; + IDBObjectStore.prototype.put = function (...args) { + const request = Reflect.apply(nativePut, this, args); + if (this.transaction.db.name === 'chart_futures' && ++puts === 2) { + request.addEventListener('success', () => this.transaction.abort(), { once: true }); + } + return request; + }; + window.__STORAGE_NATIVE__.reset(); + }); + + // When both replacements have executed within one transaction before its real abort event. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const state = window.__MIRROR_WRITER__; + const outcomes = await Promise.allSettled(state.writer.dispatch(lab.native, [ + ['myTradingView.A', { revision: 1 }], ['myTradingView.B', { revision: 1 }], + ], state.nativeThunk)); + state.restorePut(); + await state.writer.stop(); + return { + outcomes: outcomes.map(outcome => ({ status: outcome.status, name: outcome.reason?.name })), + metrics: window.__STORAGE_NATIVE__.snapshot(), stats: state.writer.getStats(), nativeCalls: state.nativeCalls, + persisted: await Promise.all(['A', 'B'].map(key => lab.native.getItem(`myTradingView.${key}`))), + }; + }); + + // Then the abort rolls back both successful puts and the original native expression is never retried. + expect(result.outcomes).toEqual([{ status: 'rejected', name: 'AbortError' }]); + expect(result.persisted).toEqual([{ revision: 0 }, { revision: 0 }]); + expect(result.metrics).toMatchObject({ transactions: 1, puts: 2, completed: 0, aborted: 1, pending: 0 }); + expect(result.stats).toMatchObject({ acceptedBatches: 1, failedBatches: 1, committedWrites: 0, pendingBatches: 0, pendingBytes: 0 }); + expect(result.nativeCalls).toBe(0); +}); + +test('user keeps the first replacement rolled back when the second record exceeds the real origin quota', async ({ page, context }) => { + // Given the isolated origin has a real quota before its first IndexedDB initialization. + await openMirrorWriterLab(page); + const session = await context.newCDPSession(page); + try { + await session.send('Storage.overrideQuotaForOrigin', { origin: STORAGE_LAB_ORIGIN, quotaSize: 64 * 1024 }); + const quota = await session.send('Storage.getUsageAndQuota', { origin: STORAGE_LAB_ORIGIN }); + expect(quota.overrideActive).toBe(true); + expect(quota.quota).toBe(64 * 1024); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await lab.native.setItem('myTradingView.small', { revision: 0 }); + await lab.native.setItem('myTradingView.large', { revision: 0 }); + window.__STORAGE_NATIVE__.reset(); + }); + + // When an earlier small put shares a transaction with an incompressible one-MiB value. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const state = window.__MIRROR_WRITER__; + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + const large = Array.from({ length: 16 }, () => Array.from(crypto.getRandomValues(new Uint8Array(65536)), byte => alphabet[byte & 63]).join('')).join(''); + const outcomes = await Promise.allSettled(state.writer.dispatch(lab.native, [ + ['myTradingView.small', { revision: 1 }], ['myTradingView.large', large], + ], state.nativeThunk)); + return { + outcomes: outcomes.map(outcome => ({ status: outcome.status, name: outcome.reason?.name })), + metrics: window.__STORAGE_NATIVE__.snapshot(), stats: state.writer.getStats(), nativeCalls: state.nativeCalls, + persisted: await Promise.all(['small', 'large'].map(key => lab.native.getItem(`myTradingView.${key}`))), + }; + }); + + // Then Chromium reports its real quota failure and neither record commits or replays. + expect(result.outcomes).toEqual([{ status: 'rejected', name: 'QuotaExceededError' }]); + expect(result.persisted).toEqual([{ revision: 0 }, { revision: 0 }]); + expect(result.metrics).toMatchObject({ transactions: 1, puts: 2, completed: 0, aborted: 1, pending: 0 }); + expect(result.stats).toMatchObject({ failedBatches: 1, committedWrites: 0, pendingBatches: 0, pendingBytes: 0 }); + expect(result.nativeCalls).toBe(0); + } finally { + await session.send('Storage.overrideQuotaForOrigin', { origin: STORAGE_LAB_ORIGIN }); + await session.detach(); + } +}); + +test('user retains native clear transaction ordering while a mirror batch waits for the real write lock', async ({ page }) => { + // Given a native transaction holds the target lock and all later transaction creation is observed. + await openMirrorWriterLab(page); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.setItem('myTradingView.initial', 0); }); + await holdMirrorWriteLock(page); + await page.evaluate(() => { + const state = window.__MIRROR_WRITER__; + const nativeTransaction = IDBDatabase.prototype.transaction; + const nativeClear = IDBObjectStore.prototype.clear; + state.created = []; + state.completed = []; + const ids = new WeakMap(); + IDBDatabase.prototype.transaction = function (...args) { + const transaction = Reflect.apply(nativeTransaction, this, args); + if (this.name === 'chart_futures') { + const id = state.created.length + 1; + ids.set(transaction, id); + state.created.push({ id, mode: transaction.mode, clear: false }); + transaction.addEventListener('complete', () => state.completed.push(id), { once: true }); + } + return transaction; + }; + IDBObjectStore.prototype.clear = function (...args) { + state.created.find(entry => entry.id === ids.get(this.transaction)).clear = true; + return Reflect.apply(nativeClear, this, args); + }; + state.restoreObservation = () => { + IDBDatabase.prototype.transaction = nativeTransaction; + IDBObjectStore.prototype.clear = nativeClear; + }; + window.__STORAGE_NATIVE__.reset(); + state.first = Promise.all(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.before', 1]], state.nativeThunk)); + }); + await expect.poll(() => page.evaluate(() => window.__MIRROR_WRITER__.created.length)).toBe(1); + + // When native clear creates its own transaction before the lock releases and a later bulk is admitted. + let blocked; + try { + await page.evaluate(() => { window.__MIRROR_WRITER__.cleared = window.__STORAGE_LAB__.native.clear(); }); + await expect.poll(() => page.evaluate(() => window.__MIRROR_WRITER__.created.length)).toBe(2); + await page.evaluate(() => { + const state = window.__MIRROR_WRITER__; + state.last = Promise.all(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.after', 2]], state.nativeThunk)); + }); + await expect.poll(() => page.evaluate(() => window.__MIRROR_WRITER__.created.length)).toBe(3); + blocked = await page.evaluate(() => ({ created: window.__MIRROR_WRITER__.created, completed: window.__MIRROR_WRITER__.completed })); + } finally { + await releaseMirrorWriteLock(page); + } + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const outcomes = await Promise.all([state.first, state.cleared, state.last]); + state.restoreObservation(); + return { outcomes, completed: state.completed, nativeCalls: state.nativeCalls, keys: await window.__STORAGE_LAB__.native.keys() }; + }); + + // Then all three native transactions exist while blocked and commit in their creation order. + expect(blocked).toEqual({ created: [{ id: 1, mode: 'readwrite', clear: false }, { id: 2, mode: 'readwrite', clear: true }, { id: 3, mode: 'readwrite', clear: false }], completed: [] }); + expect(result.completed).toEqual([1, 2, 3]); + expect(result.outcomes).toEqual([[undefined], undefined, [undefined]]); + expect(result.keys).toEqual(['myTradingView.after']); + expect(result.nativeCalls).toBe(0); +}); + +test('user compares current target data after another tab replaces an earlier mirrored value', async ({ page, context }) => { + // Given one writer has committed a value and another real page shares its IndexedDB origin. + await openMirrorWriterLab(page); + const other = await context.newPage(); + try { + await openStorageLab(other, { install: false }); + await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + await Promise.all(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.shared', { revision: 1 }]], state.nativeThunk)); + }); + + // When the other page changes storage and the first page requests its original value again. + await other.evaluate(async () => { await window.__STORAGE_LAB__.native.setItem('myTradingView.shared', { revision: 2 }); }); + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + window.__STORAGE_NATIVE__.reset(); + await Promise.all(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.shared', { revision: 1 }]], state.nativeThunk)); + return { metrics: window.__STORAGE_NATIVE__.snapshot(), nativeCalls: state.nativeCalls }; + }); + const persisted = await other.evaluate(() => window.__STORAGE_LAB__.native.getItem('myTradingView.shared')); + + // Then the writer reads the shared target again and restores it with one actual put. + expect(result.metrics).toMatchObject({ transactions: 1, gets: 1, puts: 1, completed: 1 }); + expect(persisted).toEqual({ revision: 1 }); + expect(result.nativeCalls).toBe(0); + } finally { + await other.close(); + } +}); + +test('user stops at a fixed accepted-work fence while native clear and later mirror thunks remain independent', async ({ page }) => { + // Given one accepted mirror waits behind an actual write lock. + await openMirrorWriterLab(page); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.ready(); }); + await holdMirrorWriteLock(page); + await page.evaluate(() => { + const state = window.__MIRROR_WRITER__; + window.__STORAGE_NATIVE__.reset(); + state.accepted = Promise.all(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.accepted', 1]], state.nativeThunk)); + }); + await expect.poll(() => page.evaluate(() => window.__STORAGE_NATIVE__.snapshot().transactions)).toBe(1); + + // When stop defers new thunks but a native clear still creates a transaction immediately. + let blocked; + try { + await page.evaluate(() => { + const state = window.__MIRROR_WRITER__; + state.events = []; + state.stopping = state.writer.stop(); + state.sameStop = state.writer.stop() === state.stopping; + state.stopping.then(() => state.events.push('stopped')); + state.tail = Promise.all(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.tail', 2]], () => { + state.events.push('native-thunk'); + return [new Promise(resolve => { state.releaseTail = resolve; })]; + })).then(value => { state.events.push('tail-complete'); return value; }); + state.cleared = window.__STORAGE_LAB__.native.clear(); + }); + await expect.poll(() => page.evaluate(() => window.__STORAGE_NATIVE__.snapshot().transactions)).toBe(2); + blocked = await page.evaluate(() => ({ events: window.__MIRROR_WRITER__.events, stats: window.__MIRROR_WRITER__.writer.getStats(), metrics: window.__STORAGE_NATIVE__.snapshot() })); + } finally { + await releaseMirrorWriteLock(page); + } + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const accepted = await state.accepted; + await state.stopping; + await state.cleared; + const atFence = [...state.events]; + const nativeResult = [Promise.resolve('native-result')]; + let calls = 0; + const exact = state.writer.dispatch(null, null, () => { calls += 1; return nativeResult; }); + state.releaseTail('tail-result'); + return { + accepted, atFence, tail: await state.tail, events: state.events, sameStop: state.sameStop, + exactArray: exact === nativeResult, exactPromise: exact[0] === nativeResult[0], calls, + stoppedResult: await Promise.all(exact), stats: state.writer.getStats(), keys: await window.__STORAGE_LAB__.native.keys(), + }; + }); + + // Then stop resolves without waiting for the tail and stopped dispatch returns the exact native array. + expect(blocked.events).toEqual([]); + expect(blocked.stats).toMatchObject({ acceptedBatches: 1, pendingBatches: 1 }); + expect(blocked.metrics).toMatchObject({ transactions: 2, completed: 0, pending: 2 }); + expect(result.accepted).toEqual([undefined]); + expect(result.atFence).toEqual(['stopped', 'native-thunk']); + expect(result.tail).toEqual([['tail-result']]); + expect(result.events).toEqual(['stopped', 'native-thunk', 'tail-complete']); + expect(result.sameStop).toBe(true); + expect(result.exactArray).toBe(true); + expect(result.exactPromise).toBe(true); + expect(result.calls).toBe(1); + expect(result.stoppedResult).toEqual(['native-result']); + expect(result.keys).toEqual([]); + expect(result.stats).toMatchObject({ acceptedBatches: 1, pendingBatches: 0, pendingBytes: 0 }); +}); + +test('user drains a blocked database initialization while unrelated raw native writes stay available', async ({ page }) => { + // Given an external version-one connection blocks the target native version-two initialization. + await openMirrorWriterLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const state = window.__MIRROR_WRITER__; + await lab.native.ready(); + state.blocker = await new Promise((resolve, reject) => { + const request = indexedDB.open('chart_delivery', 1); + request.onupgradeneeded = () => request.result.createObjectStore('keyvaluepairs'); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const nativeOpen = IDBFactory.prototype.open; + let resolveBlocked; + state.blocked = new Promise(resolve => { resolveBlocked = resolve; }); + IDBFactory.prototype.open = function (...args) { + const request = Reflect.apply(nativeOpen, this, args); + if (args[0] === 'chart_delivery' && args[1] === 2) request.addEventListener('blocked', event => resolveBlocked({ oldVersion: event.oldVersion, newVersion: event.newVersion }), { once: true }); + return request; + }; + state.restoreOpen = () => { IDBFactory.prototype.open = nativeOpen; }; + state.target = lab.create('chart_delivery', { version: 2 }); + state.accepted = Promise.all(state.writer.dispatch(state.target, [['myTradingView.upgrade', 1]], state.nativeThunk)); + }); + + // When stop waits for accepted initialization while a raw source write completes and a mirror tail waits. + let blocked; + try { + blocked = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const version = await state.blocked; + state.events = []; + state.stopping = state.writer.stop(); + state.stopping.then(() => state.events.push('stopped')); + state.tail = Promise.all(state.writer.dispatch(state.target, [['myTradingView.upgrade', 2]], () => { + state.events.push('native-thunk'); + return [state.target.setItem('myTradingView.upgrade', 2)]; + })); + const raw = await window.__STORAGE_LAB__.native.setItem('myTradingView.raw', 3); + return { version, raw, events: [...state.events], stats: state.writer.getStats() }; + }); + } finally { + await page.evaluate(() => { const state = window.__MIRROR_WRITER__; state.restoreOpen(); state.blocker.close(); }); + } + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const accepted = await state.accepted; + await state.stopping; + return { accepted, tail: await state.tail, events: state.events, stats: state.writer.getStats(), persisted: await state.target.getItem('myTradingView.upgrade') }; + }); + + // Then independent native work succeeds before the drain and the accepted write precedes its native tail. + expect(blocked.version).toEqual({ oldVersion: 1, newVersion: 2 }); + expect(blocked.raw).toBe(3); + expect(blocked.events).toEqual([]); + expect(blocked.stats).toMatchObject({ acceptedBatches: 1, pendingBatches: 1, transactions: 0 }); + expect(result.accepted).toEqual([undefined]); + expect(result.tail).toEqual([[2]]); + expect(result.events).toEqual(['stopped', 'native-thunk']); + expect(result.persisted).toBe(2); + expect(result.stats).toMatchObject({ acceptedBatches: 1, pendingBatches: 0, pendingBytes: 0, committedWrites: 1 }); +}); + +test('user dispatches an empty mirror without initializing the native target or opening a transaction', async ({ page }) => { + // Given the native instance has never needed a database connection. + await openMirrorWriterLab(page); + await page.evaluate(() => { window.__STORAGE_NATIVE__.reset(); }); + + // When active dispatch receives zero source entries and stop drains the empty writer. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const target = window.__STORAGE_LAB__.native; + const beforeReady = target._ready; + const dispatched = state.writer.dispatch(target, [], state.nativeThunk); + await state.writer.stop(); + return { length: dispatched.length, unchangedReady: target._ready === beforeReady, hasDatabase: Boolean(target._dbInfo?.db), metrics: window.__STORAGE_NATIVE__.snapshot(), stats: state.writer.getStats(), nativeCalls: state.nativeCalls }; + }); + + // Then the empty array costs no ready initialization, database open, transaction or accepted batch. + expect(result.length).toBe(0); + expect(result.unchangedReady).toBe(true); + expect(result.hasDatabase).toBe(false); + expect(result.metrics).toMatchObject({ opens: 0, transactions: 0, puts: 0, gets: 0 }); + expect(result.stats).toMatchObject({ acceptedBatches: 0, rejectedBatches: 0, pendingBatches: 0, pendingBytes: 0 }); + expect(result.nativeCalls).toBe(0); +}); + +for (const scenario of [ + { name: 'entry count', limits: { maxEntries: 1 }, kind: 'entries', error: /entry limit/ }, + { name: 'encoded batch bytes', limits: { maxBatchBytes: 32 }, kind: 'bytes', error: /batch byte limit/ }, + { name: 'duplicate keys', limits: {}, kind: 'duplicate', error: /unique/ }, + { name: 'non-chart keys', limits: {}, kind: 'key', error: /non-chart key/ }, + { name: 'non-JSON values', limits: {}, kind: 'value', error: /plain JSON/ }, + { name: 'invalid pair shape', limits: {}, kind: 'pair', error: /key\/value pair/ }, +]) { + test(`user rejects ${scenario.name} before any mirror entry is written`, async ({ page }) => { + // Given a committed first key would visibly change if admission wrote a partial batch. + await openMirrorWriterLab(page, { limits: scenario.limits }); + await page.evaluate(async () => { + await window.__STORAGE_LAB__.native.setItem('myTradingView.first', 0); + window.__STORAGE_NATIVE__.reset(); + }); + + // When a valid first entry precedes the specified invalid or oversized entry. + const result = await page.evaluate(async kind => { + const state = window.__MIRROR_WRITER__; + const first = ['myTradingView.first', 1]; + const second = { + entries: ['myTradingView.second', 2], bytes: ['myTradingView.second', 'x'.repeat(64)], + duplicate: ['myTradingView.first', 2], key: ['candlestick-setting', 2], + value: ['myTradingView.second', new Date(0)], pair: ['myTradingView.second'], + }[kind]; + const outcomes = await Promise.allSettled(state.writer.dispatch(window.__STORAGE_LAB__.native, [first, second], state.nativeThunk)); + return { + outcomes: outcomes.map(outcome => ({ status: outcome.status, message: outcome.reason?.message })), + metrics: window.__STORAGE_NATIVE__.snapshot(), stats: state.writer.getStats(), nativeCalls: state.nativeCalls, + first: await window.__STORAGE_LAB__.native.getItem('myTradingView.first'), keys: await window.__STORAGE_LAB__.native.keys(), + }; + }, scenario.kind); + + // Then the one rejected result preserves original data without opening a transaction or retrying. + expect(result.outcomes).toHaveLength(1); + expect(result.outcomes[0].status).toBe('rejected'); + expect(result.outcomes[0].message).toMatch(scenario.error); + expect(result.first).toBe(0); + expect(result.keys).toEqual(['myTradingView.first']); + expect(result.metrics).toMatchObject({ opens: 0, transactions: 0, puts: 0 }); + expect(result.stats).toMatchObject({ acceptedBatches: 0, rejectedBatches: 1, pendingBatches: 0, pendingBytes: 0 }); + expect(result.nativeCalls).toBe(0); + }); +} + +for (const kind of ['batches', 'bytes']) { + test(`user rejects pending ${kind} overflow without disturbing the accepted mirror`, async ({ page }) => { + // Given a real lock holds one batch inside the configured pending capacity. + const limits = kind === 'batches' ? { maxPendingBatches: 1 } : { maxPendingBytes: 48 }; + await openMirrorWriterLab(page, { limits }); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.ready(); }); + await holdMirrorWriteLock(page); + await page.evaluate(() => { + const state = window.__MIRROR_WRITER__; + window.__STORAGE_NATIVE__.reset(); + state.accepted = Promise.all(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.first', 1]], state.nativeThunk)); + }); + await expect.poll(() => page.evaluate(() => window.__STORAGE_NATIVE__.snapshot().transactions)).toBe(1); + + // When another valid batch exceeds the pending bound while the first transaction cannot finish. + let rejected; + try { + rejected = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const outcomes = await Promise.allSettled(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.second', 2]], state.nativeThunk)); + return { outcomes: outcomes.map(outcome => ({ status: outcome.status, message: outcome.reason?.message })), metrics: window.__STORAGE_NATIVE__.snapshot(), stats: state.writer.getStats() }; + }); + } finally { + await releaseMirrorWriteLock(page); + } + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + return { accepted: await state.accepted, keys: await window.__STORAGE_LAB__.native.keys(), stats: state.writer.getStats(), nativeCalls: state.nativeCalls }; + }); + + // Then only the first batch owns a transaction and its successful commit releases all capacity. + expect(rejected.outcomes).toHaveLength(1); + expect(rejected.outcomes[0].status).toBe('rejected'); + expect(rejected.outcomes[0].message).toMatch(kind === 'batches' ? /pending batch limit/ : /pending byte limit/); + expect(rejected.metrics).toMatchObject({ transactions: 1, puts: 0 }); + expect(rejected.stats).toMatchObject({ acceptedBatches: 1, rejectedBatches: 1, pendingBatches: 1 }); + expect(result.accepted).toEqual([undefined]); + expect(result.keys).toEqual(['myTradingView.first']); + expect(result.stats).toMatchObject({ acceptedBatches: 1, rejectedBatches: 1, pendingBatches: 0, pendingBytes: 0 }); + expect(result.nativeCalls).toBe(0); + }); +} + +test('user rejects a Proxy value that stops admission during validation without replaying its native thunk', async ({ page }) => { + // Given no active work exists and the native target is already ready. + await openMirrorWriterLab(page); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.ready(); window.__STORAGE_NATIVE__.reset(); }); + + // When synchronous object inspection invokes stop before the batch can be admitted. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + let stopping; + let inspections = 0; + const value = new Proxy({ revision: 1 }, { + getPrototypeOf(target) { + inspections += 1; + stopping = state.writer.stop(); + return Reflect.getPrototypeOf(target); + }, + }); + const outcomes = await Promise.allSettled(state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.reentrant', value]], state.nativeThunk)); + await stopping; + return { outcomes: outcomes.map(outcome => ({ status: outcome.status, message: outcome.reason?.message })), inspections, metrics: window.__STORAGE_NATIVE__.snapshot(), stats: state.writer.getStats(), nativeCalls: state.nativeCalls, keys: await window.__STORAGE_LAB__.native.keys() }; + }); + + // Then reentrant stop rejects admission before any put and no active failure replays natively. + expect(result.inspections).toBeGreaterThan(0); + expect(result.outcomes).toEqual([{ status: 'rejected', message: 'Chart mirror admission stopped during validation' }]); + expect(result.metrics).toMatchObject({ transactions: 0, puts: 0 }); + expect(result.stats).toMatchObject({ acceptedBatches: 0, rejectedBatches: 1, pendingBatches: 0, pendingBytes: 0 }); + expect(result.nativeCalls).toBe(0); + expect(result.keys).toEqual([]); +}); + +test('user rolls back earlier replacements when a later stored value exceeds the read budget', async ({ page }) => { + // Given native storage contains a small first value and a second value larger than the mirror read bound. + await openMirrorWriterLab(page, { limits: { maxReadBytes: 64 } }); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await lab.native.setItem('myTradingView.first', { revision: 0 }); + await lab.native.setItem('myTradingView.large', 'x'.repeat(128)); + window.__STORAGE_NATIVE__.reset(); + }); + + // When the first replacement succeeds inside the transaction before the oversized value is read. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const state = window.__MIRROR_WRITER__; + const outcomes = await Promise.allSettled(state.writer.dispatch(lab.native, [ + ['myTradingView.first', { revision: 1 }], ['myTradingView.large', 'small'], + ], state.nativeThunk)); + return { + outcomes: outcomes.map(outcome => ({ status: outcome.status, message: outcome.reason?.message })), + metrics: window.__STORAGE_NATIVE__.snapshot(), stats: state.writer.getStats(), nativeCalls: state.nativeCalls, + persisted: await Promise.all(['first', 'large'].map(key => lab.native.getItem(`myTradingView.${key}`))), + }; + }); + + // Then the explicit read-limit failure aborts all changes and preserves both original native values. + expect(result.outcomes).toEqual([{ status: 'rejected', message: 'Mirror stored-value byte limit exceeded' }]); + expect(result.persisted).toEqual([{ revision: 0 }, 'x'.repeat(128)]); + expect(result.metrics).toMatchObject({ transactions: 1, gets: 2, puts: 1, aborted: 1, completed: 0, pending: 0 }); + expect(result.stats).toMatchObject({ acceptedBatches: 1, failedBatches: 1, committedWrites: 0, pendingBatches: 0, pendingBytes: 0 }); + expect(result.nativeCalls).toBe(0); +}); diff --git a/e2e/binance-orderbook/specs/chart-storage-orderbook-entry.pw.js b/e2e/binance-orderbook/specs/chart-storage-orderbook-entry.pw.js new file mode 100644 index 0000000..8d265a7 --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-orderbook-entry.pw.js @@ -0,0 +1,207 @@ +import { readFile } from 'node:fs/promises'; +import { test, expect } from '../test.js'; +import { createCancelScenario } from '../scenarios/cancel-current-symbol.js'; +import { openUserscriptScenario, readFixtureState } from '../helpers/userscript-page.js'; + +const PANEL = '#jh-binance-close-qty-multiplier-panel'; +const INPUT = '#jh-binance-close-qty-multiplier-input'; + +test('user gets one working orderbook when document-start injection precedes the document root', async ({ page }) => { + // Given the real artifact runs once through the browser init-script boundary before any fixture HTML. + await page.route('**/*', route => route.abort('blockedbyclient')); + const errors = []; + page.on('pageerror', error => errors.push({ message: error.message, stack: error.stack })); + const source = await readFile(new URL('../../../scripts/binance-orderbook-trade.user.js', import.meta.url), 'utf8'); + await page.addInitScript({ content: ` + window.__EARLY_ORDERBOOK_ENTRY__ = { + documentRootPresent: Boolean(document.documentElement), + injections: 1, + fixtureScriptLoads: 0, + }; + ${source} + ` }); + let initial; + + // When the normal host HTML arrives with its later copy of the artifact explicitly disabled. + const host = await openUserscriptScenario(page, createCancelScenario(), { + beforeOrderbook: 'window.__EARLY_ORDERBOOK_ENTRY__.fixtureScriptLoads += 1; if (false) {', + afterOrderbook: '}', + afterNavigation: async () => { + initial = await page.evaluate(() => ({ + entry: window.__EARLY_ORDERBOOK_ENTRY__, + storageInstalled: Object.hasOwn(self, '__BINANCE_CHART_STORAGE__'), + queueIntercepted: typeof Object.getOwnPropertyDescriptor(self.webpackChunkfutures_trade_ui, 'push').get === 'function', + })); + expect(errors, 'The initial artifact must complete without a later reinjection repairing it').toEqual([]); + }, + }); + const disabledStyle = await page.locator('#jh-binance-close-qty-multiplier-dec').evaluate(element => { + const style = getComputedStyle(element); + return { disabled: element.disabled, background: style.backgroundColor, cursor: style.cursor }; + }); + await page.locator(INPUT).fill('3'); + await page.locator(INPUT).blur(); + + // Then the first injection owns storage and initializes exactly one usable panel after the root appears. + expect(initial).toEqual({ entry: { documentRootPresent: false, injections: 1, fixtureScriptLoads: 1 }, storageInstalled: true, queueIntercepted: true }); + expect(await page.evaluate(() => typeof window.__TM_CLOSE_LONG_DEBUG__)).toBe('object'); + await expect(page.locator('style#jh-disabled-control-style')).toHaveCount(1); + expect(disabledStyle).toEqual({ disabled: true, background: 'rgb(245, 245, 245)', cursor: 'not-allowed' }); + await expect(page.locator(PANEL)).toHaveCount(1); + await expect(page.locator(PANEL)).toBeVisible(); + await expect(page.locator(INPUT)).toHaveValue('3'); + await expect(page.locator('#jh-binance-close-qty-final')).toHaveText('0.21'); + expect(errors).toEqual([]); + expect(host.errors).toEqual([]); + expect((await readFixtureState(page)).events.filter(({ type }) => ['order-submitted', 'cancel-requested'].includes(type))).toEqual([]); +}); + +test('user installs chart storage before the generated orderbook starts its native network observation', async ({ page }) => { + // Given every request is isolated and a native constructor read records the first business initialization. + await page.route('**/*', route => route.abort('blockedbyclient')); + const observeStartup = ` + { + const descriptor = Object.getOwnPropertyDescriptor(window, 'WebSocket'); + let value = window.WebSocket; + window.__CHART_ENTRY_OBSERVATIONS__ = []; + window.__RESTORE_ENTRY_OBSERVATION__ = () => Object.defineProperty(window, 'WebSocket', { ...descriptor, value }); + Object.defineProperty(window, 'WebSocket', { + configurable: true, + get() { + const queue = self.webpackChunkfutures_trade_ui; + const push = queue && Object.getOwnPropertyDescriptor(queue, 'push'); + window.__CHART_ENTRY_OBSERVATIONS__.push({ + storageInstalled: Object.hasOwn(self, '__BINANCE_CHART_STORAGE__'), + queueIntercepted: Boolean(push && typeof push.get === 'function'), + bookDebug: typeof window.__TM_CLOSE_LONG_DEBUG__, + readyState: document.readyState, + }); + return value; + }, + set(next) { value = next; }, + }); + } + `; + + // When the unchanged generated install artifact starts on the existing full orderbook host fixture. + const host = await openUserscriptScenario(page, createCancelScenario(), { + beforeOrderbook: observeStartup, + afterOrderbook: 'window.__RESTORE_ENTRY_OBSERVATION__();', + }); + const first = await page.evaluate(() => window.__CHART_ENTRY_OBSERVATIONS__[0]); + + // Then queue interception precedes the first business network observer and the normal panel still mounts. + expect(first).toEqual({ storageInstalled: true, queueIntercepted: true, bookDebug: 'undefined', readyState: 'loading' }); + await expect(page.locator(PANEL)).toBeVisible(); + await expect(page.locator(INPUT)).toHaveValue('1'); + expect(host.errors).toEqual([]); +}); + +for (const kind of ['source-mismatch', 'late-bootstrap']) { + test(`user can edit the orderbook multiplier when chart storage retires after ${kind}`, async ({ page }) => { + // Given the fixture supplies either an unsupported host module or an already populated native queue. + await page.route('**/*', route => route.abort('blockedbyclient')); + const beforeOrderbook = ` + self.__CHART_ENTRY_NATIVE_FACTORY__ = function nativeHost(module) { module.exports = 'native-chart-module'; }; + self.__CHART_ENTRY_NATIVE_CHUNK__ = [['existing-chart'], { 70940: self.__CHART_ENTRY_NATIVE_FACTORY__ }]; + self.webpackChunkfutures_trade_ui = ${kind === 'late-bootstrap' ? '[self.__CHART_ENTRY_NATIVE_CHUNK__]' : '[]'}; + `; + + // When the generated orderbook loads and the user edits its real quantity control after storage retires. + const host = await openUserscriptScenario(page, createCancelScenario(), { + beforeOrderbook, + afterOrderbook: kind === 'source-mismatch' + ? 'self.webpackChunkfutures_trade_ui.push(self.__CHART_ENTRY_NATIVE_CHUNK__);' : '', + }); + await page.locator(INPUT).fill('3'); + await page.locator(INPUT).blur(); + const storage = await page.evaluate(async () => { + const snapshot = await self.__BINANCE_CHART_STORAGE__.stop(); + const chunk = self.webpackChunkfutures_trade_ui[0]; + const module = { exports: {} }; + chunk[1][70940](module); + return { + snapshot, + sameChunk: chunk === self.__CHART_ENTRY_NATIVE_CHUNK__, + sameFactory: chunk[1][70940] === self.__CHART_ENTRY_NATIVE_FACTORY__, + nativePush: self.webpackChunkfutures_trade_ui.push === Array.prototype.push, + nativeValue: module.exports, + }; + }); + + // Then native chart execution remains intact and the normal orderbook calculation works without trading. + expect(storage.snapshot).toMatchObject({ status: 'native', phase: 'stopped', + reason: kind === 'source-mismatch' ? 'source_mismatch' : 'bootstrap_unavailable', + attempts: kind === 'source-mismatch' ? 1 : 0, matches: 0, executions: 0, + writer: { acceptedBatches: 0, transactions: 0 }, + }); + expect(storage).toMatchObject({ sameChunk: true, sameFactory: true, nativePush: true, nativeValue: 'native-chart-module' }); + await expect(page.locator(PANEL)).toBeVisible(); + await expect(page.locator(INPUT)).toHaveValue('3'); + await expect(page.locator('#jh-binance-close-qty-final')).toHaveText('0.21'); + expect((await readFixtureState(page)).events.filter(({ type }) => ['order-submitted', 'cancel-requested'].includes(type))).toEqual([]); + expect(host.errors).toEqual([]); + }); +} + +test('user keeps storage and the trading panel absent on futures home before native SPA entry', async ({ page }) => { + // Given the generated artifact starts on the futures homepage despite a stale trading host DOM. + await page.route('**/*', route => route.abort('blockedbyclient')); + let homepage; + + // When the existing fixture navigates from home to its supported contract without reinjecting the script. + const host = await openUserscriptScenario(page, createCancelScenario(), { + beforeOrderbook: "history.replaceState({}, '', '/zh-CN/futures/home');", + afterNavigation: async () => { + homepage = await page.evaluate(() => ({ + pathname: location.pathname, + storageInstalled: Object.hasOwn(self, '__BINANCE_CHART_STORAGE__'), + queueCreated: Object.hasOwn(self, 'webpackChunkfutures_trade_ui'), + bookDebug: typeof window.__TM_CLOSE_LONG_DEBUG__, + panels: document.querySelectorAll('#jh-binance-close-qty-multiplier-panel').length, + })); + await page.evaluate(() => history.pushState({}, '', '/zh-CN/futures/HYPEUSDT')); + }, + }); + await page.locator(INPUT).fill('3'); + + // Then home performed no storage or trading initialization and the unchanged orderbook SPA path still works. + expect(homepage).toEqual({ pathname: '/zh-CN/futures/home', storageInstalled: false, queueCreated: false, bookDebug: 'undefined', panels: 0 }); + expect(await page.evaluate(() => Object.hasOwn(self, '__BINANCE_CHART_STORAGE__'))).toBe(false); + await expect(page.locator(PANEL)).toBeVisible(); + await expect(page.locator('#jh-binance-close-qty-final')).toHaveText('0.21'); + expect((await readFixtureState(page)).events.filter(({ type }) => ['order-submitted', 'cancel-requested'].includes(type))).toEqual([]); + expect(host.errors).toEqual([]); +}); + +test('user retains the existing orderbook behavior in a child frame while storage remains top-frame only', async ({ page }) => { + // Given the generated artifact has mounted on a top-level fixture that owns every child-frame request. + await page.route('**/*', route => route.abort('blockedbyclient')); + const host = await openUserscriptScenario(page, createCancelScenario()); + + // When a real same-origin child trading page loads the same artifact and edits its own multiplier. + await page.evaluate(() => { + const frame = document.createElement('iframe'); + frame.name = 'chart-storage-entry-child'; + frame.src = '/zh-CN/futures/HYPEUSDT?chart-storage-entry-child=1'; + frame.style.cssText = 'width:1440px;height:1000px;border:0'; + document.body.appendChild(frame); + }); + const frame = page.frameLocator('iframe[name="chart-storage-entry-child"]'); + await expect(frame.locator(PANEL)).toBeVisible(); + await frame.locator(INPUT).fill('3'); + const state = await frame.locator('body').evaluate(() => ({ + isTop: self === top, + storageInstalled: Object.hasOwn(self, '__BINANCE_CHART_STORAGE__'), + queueCreated: Object.hasOwn(self, 'webpackChunkfutures_trade_ui'), + bookDebug: typeof window.__TM_CLOSE_LONG_DEBUG__, + actions: window.__BINANCE_FIXTURE__.snapshot().events.filter(({ type }) => ['order-submitted', 'cancel-requested'].includes(type)), + })); + + // Then only storage is frame-restricted while the original orderbook panel and quantity calculation remain usable. + expect(state).toEqual({ isTop: false, storageInstalled: false, queueCreated: false, bookDebug: 'object', actions: [] }); + expect(await page.evaluate(() => Object.hasOwn(self, '__BINANCE_CHART_STORAGE__'))).toBe(true); + await expect(frame.locator(INPUT)).toHaveValue('3'); + await expect(frame.locator('#jh-binance-close-qty-final')).toHaveText('0.21'); + expect(host.errors).toEqual([]); +}); diff --git a/e2e/binance-orderbook/specs/chart-storage-production-writer.pw.js b/e2e/binance-orderbook/specs/chart-storage-production-writer.pw.js new file mode 100644 index 0000000..e88aaa0 --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-production-writer.pw.js @@ -0,0 +1,348 @@ +import { test, expect } from '../test.js'; +import { openProductionMirrorWriterLab } from '../helpers/chart-storage-production-writer.js'; +import { holdMirrorWriteLock, releaseMirrorWriteLock } from '../helpers/chart-storage-mirror-writer.js'; + +test('user persists signed zero and property order changes without rewriting equal snapshots', async ({ page }) => { + // Given native records differ only by signed zero or insertion order. + await openProductionMirrorWriterLab(page); + await page.evaluate(async () => { + const target = window.__STORAGE_LAB__.native; + await Promise.all([ + target.setItem('myTradingView.zero', { value: -0 }), + target.setItem('myTradingView.order', { a: 1, b: 2 }), + target.setItem('myTradingView.equal', { same: [1, null] }), + ]); + window.__STORAGE_NATIVE__.reset(); + }); + + // When the native mirror receives the next ordered snapshot. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + await Promise.all(state.dispatch([ + ['myTradingView.zero', { value: 0 }], + ['myTradingView.order', { b: 2, a: 1 }], + ['myTradingView.equal', { same: [1, null] }], + ])); + const target = window.__STORAGE_LAB__.native; + const metrics = window.__STORAGE_NATIVE__.snapshot(); + return { metrics, zero: Object.is((await target.getItem('myTradingView.zero')).value, 0), + keys: Object.keys(await target.getItem('myTradingView.order')), stats: state.writer.getStats(), nativeCalls: state.nativeCalls }; + }); + + // Then exactly the observable changes are committed in one transaction. + expect(result.zero).toBe(true); + expect(result.keys).toEqual(['b', 'a']); + expect(result.metrics).toMatchObject({ transactions: 1, puts: 2, aborted: 0, completed: 1 }); + expect(result.stats).toMatchObject({ committedWrites: 2, skippedWrites: 1, failedBatches: 0 }); + expect(result.nativeCalls).toBe(0); +}); + +for (const kind of ['shared reference', 'cycle', 'date', 'oversized input']) { + test(`user preserves native storage semantics for ${kind} before a batch transaction exists`, async ({ page }) => { + // Given the supported optimization limits exclude the next structured clone value. + await openProductionMirrorWriterLab(page, { limits: kind === 'oversized input' ? { maxBatchBytes: 40 } : {} }); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.ready(); window.__STORAGE_NATIVE__.reset(); }); + + // When the original native map receives the unsupported snapshot once. + const result = await page.evaluate(async kind => { + const state = window.__MIRROR_WRITER__; + const shared = { revision: 1 }; + const value = kind === 'shared reference' ? { a: shared, b: shared } + : kind === 'cycle' ? { revision: 1 } : kind === 'date' ? new Date(0) : 'x'.repeat(100); + if (kind === 'cycle') value.self = value; + const outcomes = await Promise.allSettled(state.dispatch([['myTradingView.native', value]])); + await state.writer.stop(); + const persisted = await window.__STORAGE_LAB__.native.getItem('myTradingView.native'); + return { status: outcomes.map(outcome => outcome.status), nativeCalls: state.nativeCalls, stats: state.writer.getStats(), + preserved: kind === 'shared reference' ? persisted.a === persisted.b + : kind === 'cycle' ? persisted.self === persisted : kind === 'date' ? persisted instanceof Date && persisted.getTime() === 0 : persisted === value }; + }, kind); + + // Then native structured cloning retains the value without splitting aliases or dropping a save. + expect(result.status).toEqual(['fulfilled']); + expect(result.preserved).toBe(true); + expect(result.nativeCalls).toBe(1); + expect(result.stats).toMatchObject({ acceptedBatches: 0, rejectedBatches: 1, nativePassthroughBatches: 1, + transactions: 0, failedBatches: 0, pendingBatches: 0, pendingBytes: 0 }); + }); +} + +for (const kind of ['date', 'read budget']) { + test(`user replaces a stored ${kind} inside the original transaction`, async ({ page }) => { + // Given the second old value cannot be safely compared within the optimization budget. + await openProductionMirrorWriterLab(page, { limits: { maxReadBytes: 64 } }); + await page.evaluate(async kind => { + const target = window.__STORAGE_LAB__.native; + await target.setItem('myTradingView.first', { revision: 0 }); + await target.setItem('myTradingView.second', kind === 'date' ? new Date(0) : 'x'.repeat(128)); + window.__STORAGE_NATIVE__.reset(); + }, kind); + + // When both records receive fresh JSON snapshots. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const outcomes = await Promise.allSettled(state.dispatch([ + ['myTradingView.first', { revision: 1 }], ['myTradingView.second', { revision: 2 }], + ])); + return { status: outcomes.map(outcome => outcome.status), nativeCalls: state.nativeCalls, metrics: window.__STORAGE_NATIVE__.snapshot(), + stats: state.writer.getStats(), values: await Promise.all(['first', 'second'].map(key => window.__STORAGE_LAB__.native.getItem(`myTradingView.${key}`))) }; + }); + + // Then the transaction commits both replacements without aborting or native replay. + expect(result.status).toEqual(['fulfilled']); + expect(result.values).toEqual([{ revision: 1 }, { revision: 2 }]); + expect(result.nativeCalls).toBe(0); + expect(result.metrics).toMatchObject({ transactions: 1, puts: 2, completed: 1, aborted: 0 }); + expect(result.stats).toMatchObject({ acceptedBatches: 1, failedBatches: 0, committedWrites: 2, pendingBytes: 0 }); + }); +} + +test('user accepts plain snapshots from the chart iframe without changing native methods', async ({ page }) => { + // Given the chart creates ordinary objects in a different JavaScript realm. + await openProductionMirrorWriterLab(page); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.ready(); window.__STORAGE_NATIVE__.reset(); }); + + // When dispatch copies the iframe snapshot before its owner mutates it. + const result = await page.evaluate(async () => { + const iframe = document.createElement('iframe'); + document.body.append(iframe); + const value = new iframe.contentWindow.Object(); + value.revision = 1; + const target = window.__STORAGE_LAB__.native; + const method = target.setItem; + const state = window.__MIRROR_WRITER__; + const work = state.dispatch([['myTradingView.realm', value]]); + value.revision = 2; + await Promise.all(work); + iframe.remove(); + return { value: await target.getItem('myTradingView.realm'), sameMethod: method === target.setItem, stats: state.writer.getStats(), nativeCalls: state.nativeCalls }; + }); + + // Then the accepted snapshot is isolated from later caller changes. + expect(result.value).toEqual({ revision: 1 }); + expect(result.sameMethod).toBe(true); + expect(result.nativeCalls).toBe(0); + expect(result.stats).toMatchObject({ acceptedBatches: 1, committedWrites: 1 }); +}); + +test('user resumes native writes exactly once after a closed IndexedDB connection', async ({ page }) => { + // Given localForage owns a connection that has really been closed. + await openProductionMirrorWriterLab(page); + await page.evaluate(async () => { + await window.__STORAGE_LAB__.native.ready(); + window.__STORAGE_LAB__.native._dbInfo.db.close(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When transaction creation fails before any mirror transaction was accepted. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const outcomes = await Promise.allSettled(state.dispatch([['myTradingView.reopened', 42]])); + await state.writer.stop(); + return { status: outcomes.map(outcome => outcome.status), value: await window.__STORAGE_LAB__.native.getItem('myTradingView.reopened'), + stats: state.writer.getStats(), nativeCalls: state.nativeCalls }; + }); + + // Then the real library reopens its connection and commits the original native map once. + expect(result.status).toEqual(['fulfilled']); + expect(result.value).toBe(42); + expect(result.nativeCalls).toBe(1); + expect(result.stats).toMatchObject({ transactions: 0, failedBatches: 0, nativePassthroughBatches: 1, pendingBatches: 0 }); +}); + +for (const kind of ['driver', 'database configuration', 'ready rejection']) { + test(`user preserves the native result after a ${kind} mismatch before transaction creation`, async ({ page }) => { + // Given a real localForage instance does not satisfy the mirror destination contract. + await openProductionMirrorWriterLab(page); + + // When the writer encounters the actual configured driver or readiness failure. + const result = await page.evaluate(async kind => { + const lab = window.__STORAGE_LAB__; + const state = window.__MIRROR_WRITER__; + const target = kind === 'driver' ? lab.create('chart_futures', { driver: lab.localforage.LOCALSTORAGE }) + : kind === 'database configuration' ? lab.create('other_chart') : lab.create('chart_futures', { driver: 'not-an-installed-driver' }); + const outcomes = await Promise.allSettled(state.dispatch([['myTradingView.native', 42]], target)); + await state.writer.stop(); + return { outcomes: outcomes.map(outcome => ({ status: outcome.status, message: outcome.reason?.message })), + value: kind === 'ready rejection' ? null : await target.getItem('myTradingView.native'), + stats: state.writer.getStats(), nativeCalls: state.nativeCalls }; + }, kind); + + // Then only the original native expression determines the saved value or rejection. + expect(result.nativeCalls).toBe(1); + expect(result.stats).toMatchObject({ transactions: 0, nativePassthroughBatches: 1, pendingBatches: 0, pendingBytes: 0 }); + if (kind === 'ready rejection') { + expect(result.outcomes).toEqual([{ status: 'rejected', message: 'No available storage method found.' }]); + } else { + expect(result.outcomes).toEqual([{ status: 'fulfilled', message: undefined }]); + expect(result.value).toBe(42); + } + }); +} + +test('user keeps transaction aborts atomic and routes later calls through the untouched native API', async ({ page }) => { + // Given the actual IndexedDB transaction is aborted after its first successful put. + await openProductionMirrorWriterLab(page); + await page.evaluate(async () => { + await window.__STORAGE_LAB__.native.setItem('myTradingView.first', 0); + window.__STORAGE_NATIVE__.reset(); + window.__STORAGE_NATIVE__.abortNextPut(); + }); + + // When the admitted batch aborts and a later mirror is submitted. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const outcomes = await Promise.allSettled(state.dispatch([['myTradingView.first', 1], ['myTradingView.second', 2]])); + const first = await window.__STORAGE_LAB__.native.getItem('myTradingView.first'); + const before = state.writer.getStats(); + const nativeResult = [window.__STORAGE_LAB__.native.setItem('myTradingView.later', 3)]; + let nativeCalls = 0; + const returned = state.writer.dispatch(window.__STORAGE_LAB__.native, [['myTradingView.later', 3]], () => { nativeCalls += 1; return nativeResult; }); + await Promise.all(returned); + return { status: outcomes.map(outcome => outcome.status), first, noReplay: state.nativeCalls, nativeCalls, + originalReturn: returned === nativeResult, before, after: state.writer.getStats(), keys: (await window.__STORAGE_LAB__.native.keys()).sort() }; + }); + + // Then rollback is reported once and the stopped writer no longer changes optimization statistics. + expect(result.status).toEqual(['rejected']); + expect(result.first).toBe(0); + expect(result.noReplay).toBe(0); + expect(result.nativeCalls).toBe(1); + expect(result.originalReturn).toBe(true); + expect(result.keys).toEqual(['myTradingView.first', 'myTradingView.later']); + expect(result.before).toMatchObject({ failedBatches: 1, abortedTransactions: 1, committedWrites: 0, pendingBatches: 0 }); + expect(result.after).toEqual(result.before); +}); + +for (const kind of ['batch count', 'snapshot bytes']) { + test(`user drains native work over pending ${kind} capacity before stop releases later writes`, async ({ page }) => { + // Given a real write lock holds the only allowed optimization batch. + await openProductionMirrorWriterLab(page, { limits: kind === 'batch count' ? { maxPendingBatches: 1 } : { maxPendingBytes: 40 } }); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.ready(); }); + await holdMirrorWriteLock(page); + await page.evaluate(() => { + const state = window.__MIRROR_WRITER__; + window.__STORAGE_NATIVE__.reset(); + state.first = Promise.all(state.dispatch([['myTradingView.first', 1]])); + }); + await expect.poll(() => page.evaluate(() => window.__STORAGE_NATIVE__.snapshot().transactions)).toBe(1); + + // When another batch uses native storage and stop is followed by one final mirror. + const during = await page.evaluate(() => { + const state = window.__MIRROR_WRITER__; + state.second = Promise.all(state.dispatch([['myTradingView.second', 2]])); + state.stopped = false; + state.stopping = state.writer.stop().then(() => { state.stopped = true; }); + state.later = Promise.all(state.dispatch([['myTradingView.later', 3]])); + return { stats: state.writer.getStats(), nativeCalls: state.nativeCalls, stopped: state.stopped }; + }); + await releaseMirrorWriteLock(page); + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + await Promise.all([state.first, state.second, state.stopping, state.later]); + return { stats: state.writer.getStats(), nativeCalls: state.nativeCalls, stopped: state.stopped, + values: await Promise.all(['first', 'second', 'later'].map(key => window.__STORAGE_LAB__.native.getItem(`myTradingView.${key}`))) }; + }); + + // Then no save is discarded and the fence includes the native work accepted before stop. + expect(during.stopped).toBe(false); + expect(during.nativeCalls).toBe(1); + expect(during.stats).toMatchObject({ acceptedBatches: 1, rejectedBatches: 1, pendingBatches: 2, nativePassthroughBatches: 1 }); + expect(result.values).toEqual([1, 2, 3]); + expect(result.stopped).toBe(true); + expect(result.nativeCalls).toBe(2); + expect(result.stats).toMatchObject({ pendingBatches: 0, pendingBytes: 0, nativePassthroughBatches: 1, failedBatches: 0 }); + }); +} + +test('user preserves missing and explicit null chart keys without disturbing target-only records', async ({ page }) => { + // Given native storage contains an explicit null and records outside this mirror. + await openProductionMirrorWriterLab(page); + await page.evaluate(async () => { + const target = window.__STORAGE_LAB__.native; + await Promise.all([target.setItem('myTradingView.present', null), target.setItem('myTradingView.targetOnly', 7), target.setItem('candlestick-setting', 8)]); + window.__STORAGE_NATIVE__.reset(); + }); + + // When a missing null key is mirrored twice and an empty batch follows. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const entries = [['myTradingView.present', null], ['myTradingView.missing', null]]; + await Promise.all(state.dispatch(entries)); + await Promise.all(state.dispatch(entries)); + const before = state.writer.getStats(); + const empty = state.dispatch([]); + return { before, after: state.writer.getStats(), empty, nativeCalls: state.nativeCalls, + metrics: window.__STORAGE_NATIVE__.snapshot(), keys: (await window.__STORAGE_LAB__.native.keys()).sort(), + values: await Promise.all(['myTradingView.present', 'myTradingView.missing', 'myTradingView.targetOnly', 'candlestick-setting'].map(key => window.__STORAGE_LAB__.native.getItem(key))) }; + }); + + // Then one new null record is committed while repeats and the empty batch make no writes. + expect(result.values).toEqual([null, null, 7, 8]); + expect(result.keys).toEqual(['candlestick-setting', 'myTradingView.missing', 'myTradingView.present', 'myTradingView.targetOnly']); + expect(result.metrics).toMatchObject({ transactions: 2, puts: 1, completed: 2, aborted: 0 }); + expect(result.before).toMatchObject({ committedWrites: 1, skippedWrites: 3, pendingBatches: 0 }); + expect(result.after).toEqual(result.before); + expect(result.empty).toEqual([]); + expect(result.nativeCalls).toBe(0); +}); + +test('user drains accepted work when input inspection synchronously requests stop', async ({ page }) => { + // Given input inspection can synchronously call the exposed stop hook. + await openProductionMirrorWriterLab(page); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.ready(); }); + + // When inspecting the snapshot triggers stop during the original dispatch. + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + let stop; + const value = new Proxy({ revision: 1 }, { getPrototypeOf(target) { stop = state.writer.stop(); return Reflect.getPrototypeOf(target); } }); + const outcomes = await Promise.allSettled(state.dispatch([['myTradingView.reentrant', value]])); + await stop; + return { status: outcomes.map(outcome => outcome.status), nativeCalls: state.nativeCalls, + value: await window.__STORAGE_LAB__.native.getItem('myTradingView.reentrant'), stats: state.writer.getStats() }; + }); + + // Then the work accepted before inspection completes and stop does not discard its snapshot. + expect(result.status).toEqual(['fulfilled']); + expect(result.value).toEqual({ revision: 1 }); + expect(result.nativeCalls).toBe(0); + expect(result.stats).toMatchObject({ acceptedBatches: 1, committedWrites: 1, pendingBatches: 0 }); +}); + +test('user drains every native map member after one member rejects', async ({ page }) => { + // Given a real write lock delays a valid native save while another native value cannot clone. + await openProductionMirrorWriterLab(page, { limits: { maxEntries: 1 } }); + await page.evaluate(async () => { await window.__STORAGE_LAB__.native.ready(); }); + await holdMirrorWriteLock(page); + + // When a rejected native member settles before the other pending member and stop is requested. + const during = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + state.results = Promise.allSettled(state.dispatch([['myTradingView.invalid', () => 1], ['myTradingView.valid', 2]])); + state.results.then(outcomes => { state.earlyOutcome = outcomes[0].status; }); + state.stopped = false; + state.stopping = state.writer.stop().then(() => { state.stopped = true; }); + await window.__MIRROR_WRITE_LOCK__.progress(); + return { stopped: state.stopped, earlyOutcome: state.earlyOutcome, stats: state.writer.getStats(), nativeCalls: state.nativeCalls }; + }); + await releaseMirrorWriteLock(page); + const result = await page.evaluate(async () => { + const state = window.__MIRROR_WRITER__; + const outcomes = await state.results; + await state.stopping; + return { status: outcomes.map(outcome => outcome.status), stopped: state.stopped, nativeCalls: state.nativeCalls, + value: await window.__STORAGE_LAB__.native.getItem('myTradingView.valid'), stats: state.writer.getStats() }; + }); + + // Then stop waits for the successful member and neither native promise is replayed. + expect(during.stopped).toBe(false); + expect(during.earlyOutcome).toBe('rejected'); + expect(during.nativeCalls).toBe(1); + expect(during.stats.pendingBatches).toBe(1); + expect(result.status).toEqual(['rejected']); + expect(result.value).toBe(2); + expect(result.stopped).toBe(true); + expect(result.nativeCalls).toBe(1); + expect(result.stats).toMatchObject({ pendingBatches: 0, pendingBytes: 0 }); +}); diff --git a/e2e/binance-orderbook/specs/chart-storage-prototype.pw.js b/e2e/binance-orderbook/specs/chart-storage-prototype.pw.js new file mode 100644 index 0000000..07cea9e --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-prototype.pw.js @@ -0,0 +1,1132 @@ +import { test, expect } from '../test.js'; +import { writeFile } from 'node:fs/promises'; +import { openStorageLab, openStorageRuntime, initializeStorageLab, displayStorageResult, STORAGE_LAB_ORIGIN } from '../helpers/chart-storage-lab.js'; + +test('user repeats 290 historical chart saves with bounded transactions and no unchanged writes', async ({ page }, testInfo) => { + // Given the actual fixture library has persisted 290 synthetic historical chart records. + await openStorageLab(page, { install: process.env.STORAGE_PROTOTYPE_BASELINE !== '1' }); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.records = Array.from({ length: 290 }, (_, index) => ({ + key: `#TV_SYMBOL-LAB${index}USDT`, + value: { symbol: `LAB${index}USDT`, interval: '15', studies: [{ name: 'volume', inputs: [20, true] }] }, + })); + await Promise.all(lab.records.map(({ key, value }) => lab.native.setItem(key, value))); + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When eight complete save rounds read and write the same chart state through Promise and callback APIs. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const callbacks = []; + const values = []; + for (let round = 0; round < 8; round += 1) { + values.push(await Promise.all(lab.records.map(async ({ key, value }, index) => { + const callback = kind => (error, actual) => { + callbacks.push({ id: `${round}:${index}:${kind}`, error: error === null, value: actual }); + }; + const read = await lab.store.getItem(key, callback('get')); + const written = await lab.store.setItem(key, read, callback('set')); + return { readMatches: JSON.stringify(read) === JSON.stringify(value), writeMatches: JSON.stringify(written) === JSON.stringify(value) }; + }))); + } + const metrics = window.__STORAGE_NATIVE__.snapshot(); + const stats = lab.controller ? lab.controller.getStats() : null; + const persisted = await Promise.all(lab.records.map(({ key }) => lab.native.getItem(key))); + return { + metrics, stats, identity: lab.cacheIdentity(), + valuesMatch: values.flat().every(value => value.readMatches && value.writeMatches), + callbackCount: callbacks.length, + uniqueCallbacks: new Set(callbacks.map(({ id }) => id)).size, + callbackValuesMatch: callbacks.every(({ id, error, value }) => error + && JSON.stringify(value) === JSON.stringify(lab.records[Number(id.split(':')[1])].value)), + persistedMatches: persisted.every((value, index) => JSON.stringify(value) === JSON.stringify(lab.records[index].value)), + }; + }); + await displayStorageResult(page, result); + const measurementPath = testInfo.outputPath('storage-measurement.json'); + await writeFile(measurementPath, JSON.stringify(result, null, 2)); + await testInfo.attach('storage-measurement.json', { path: measurementPath, contentType: 'application/json' }); + const screenshotPath = testInfo.outputPath('storage-lab.png'); + await page.screenshot({ path: screenshotPath, fullPage: true }); + await testInfo.attach('storage-lab.png', { path: screenshotPath, contentType: 'image/png' }); + + // Then every value and callback is preserved while actual database work remains bounded. + expect(result.identity).toEqual({ sameExport: true, factoryExecutions: 1 }); + expect(result.valuesMatch).toBe(true); + expect(result.persistedMatches).toBe(true); + expect(result.callbackCount).toBe(290 * 8 * 2); + expect(result.uniqueCallbacks).toBe(290 * 8 * 2); + expect(result.callbackValuesMatch).toBe(true); + expect(result.metrics.transactions).toBeLessThanOrEqual(32); + expect(result.metrics.puts).toBe(0); +}); + +test('user captures the first registered storage export without executing its factory twice', async ({ page }) => { + // Given the observer is installed between the real runtime and first module registration. + await openStorageLab(page, { capture: true }); + + // When the host requires the module and the observer delivers the captured export. + const result = await page.evaluate(async () => ({ + capturedSameExport: await window.__STORAGE_CAPTURE__.captured === window.__STORAGE_LAB__.localforage, + pushRestored: self.webpackChunkfutures_trade_ui.push === window.__STORAGE_CAPTURE_ORIGINAL_PUSH__, + identity: window.__STORAGE_LAB__.cacheIdentity(), + })); + + // Then capture shares the host's exact export and real runtime cache identity. + expect(result).toEqual({ capturedSameExport: true, pushRestored: true, identity: { sameExport: true, factoryExecutions: 1 } }); +}); + +test('user receives an explicit refusal when module capture starts after registration', async ({ page }) => { + // Given the real storage module is already registered and required by the host. + await openStorageLab(page); + + // When a new observer attempts to capture the existing module. + const result = await page.evaluate(async () => { + const { observeChartStorageModule } = await import('/capture.js'); + try { + const observer = observeChartStorageModule(); + await observer.captured; + return { rejected: false }; + } catch (error) { + return { rejected: true, message: error.message, identity: window.__STORAGE_LAB__.cacheIdentity() }; + } + }); + + // Then the unavailable capture fails explicitly without re-executing the host factory. + expect(result.rejected).toBe(true); + expect(result.message).toMatch(/already|late|registered/i); + expect(result.identity).toEqual({ sameExport: true, factoryExecutions: 1 }); +}); + +test('user preserves cross-instance call order before readiness and native non-target storage', async ({ page }) => { + // Given fresh futures and delivery instances have not initialized their real databases. + await openStorageLab(page); + + // When callers issue interleaved operations before ready and another database stores a native Blob. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const first = lab.create(); + const second = first.createInstance({ name: 'chart_futures', storeName: 'keyvaluepairs', driver: lab.localforage.INDEXEDDB }); + const setter = first.setItem; + const delivery = lab.create('chart_delivery'); + const key = '#TV_SYMBOL-ORDER'; + const sequence = await Promise.all([ + first.setItem(key, { value: 'first' }), + second.getItem(key), + second.setItem(key, { value: 'second' }), + first.getItem(key), + delivery.setItem(key, { value: 'delivery' }), + ]); + await first.ready(); + const other = lab.create('chart_other'); + const blob = new Blob(['native-binary'], { type: 'text/plain' }); + await other.setItem(key, blob); + const binary = await other.getItem(key); + return { + sequence, wrapperPreserved: setter === first.setItem, + delivery: await delivery.getItem(key), nativeBlobText: await binary.text(), nativeBlobType: binary.type, + }; + }); + + // Then shared-store operations retain issue order while delivery and native databases stay independent. + expect(result).toEqual({ + sequence: [{ value: 'first' }, { value: 'first' }, { value: 'second' }, { value: 'second' }, { value: 'delivery' }], + wrapperPreserved: true, delivery: { value: 'delivery' }, nativeBlobText: 'native-binary', nativeBlobType: 'text/plain', + }); +}); + +test('user observes every A to B to A transition and ordered native barriers', async ({ page }) => { + // Given the real database initially contains chart state A. + await openStorageLab(page); + await page.evaluate(() => window.__STORAGE_LAB__.native.setItem('#TV_SYMBOL-ABA', 'A')); + + // When one caller interleaves state changes, reads, enumeration, deletion, and an unmanaged key. + const result = await page.evaluate(async () => { + const store = window.__STORAGE_LAB__.create(); + return Promise.all([ + store.getItem('#TV_SYMBOL-ABA'), + store.setItem('#TV_SYMBOL-ABA', 'B'), + store.getItem('#TV_SYMBOL-ABA'), + store.setItem('#TV_SYMBOL-ABA', 'A'), + store.getItem('#TV_SYMBOL-ABA'), + store.keys(), + store.removeItem('#TV_SYMBOL-ABA'), + store.getItem('#TV_SYMBOL-ABA'), + store.setItem('unmanaged-setting', { enabled: true }), + store.getItem('unmanaged-setting'), + store.clear(), + store.keys(), + ]); + }); + + // Then each intermediate result follows issue order and clear leaves no stored keys. + expect(result).toEqual([ + 'A', 'B', 'B', 'A', 'A', ['#TV_SYMBOL-ABA'], undefined, null, + { enabled: true }, { enabled: true }, undefined, [], + ]); +}); + +test('user creates a present null record and suppresses only its repeated null write', async ({ page }) => { + // Given the chart store is empty and its native connection is ready. + await openStorageLab(page); + await page.evaluate(async () => { + window.__STORAGE_LAB__.store = window.__STORAGE_LAB__.create(); + await window.__STORAGE_LAB__.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When a missing chart key receives two explicit null saves. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const values = await Promise.all([ + lab.store.getItem('myTradingView-null'), + lab.store.setItem('myTradingView-null', null), + lab.store.setItem('myTradingView-null', null), + ]); + const metrics = window.__STORAGE_NATIVE__.snapshot(); + return { values, metrics, keys: await lab.native.keys(), stats: lab.controller.getStats() }; + }); + + // Then null is a persisted present key and only the unchanged second save is skipped. + expect(result.values).toEqual([null, null, null]); + expect(result.keys).toEqual(['myTradingView-null']); + expect(result.metrics.puts).toBe(1); + expect(result.stats.skippedWrites).toBe(1); +}); + +test('user reads completed writes from another page and retains them after reload', async ({ page, context }) => { + // Given two isolated host pages share only the real same-origin IndexedDB database. + await openStorageLab(page); + const second = await context.newPage(); + await openStorageLab(second); + + // When the first page commits A and the second page reads A before committing B. + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.sharedStore = lab.create(); + await lab.sharedStore.setItem('#TV_SYMBOL-SHARED', { revision: 'A' }); + await lab.sharedStore.getItem('#TV_SYMBOL-SHARED'); + }); + const secondRead = await second.evaluate(async () => { + const store = window.__STORAGE_LAB__.create(); + const prior = await store.getItem('#TV_SYMBOL-SHARED'); + await store.setItem('#TV_SYMBOL-SHARED', { revision: 'B' }); + return prior; + }); + const firstRead = await page.evaluate(() => window.__STORAGE_LAB__.sharedStore.getItem('#TV_SYMBOL-SHARED')); + await page.reload(); + await initializeStorageLab(page); + const reloaded = await page.evaluate(() => window.__STORAGE_LAB__.create().getItem('#TV_SYMBOL-SHARED')); + + // Then the later completed write is visible across pages and survives a real document reload. + expect(secondRead).toEqual({ revision: 'A' }); + expect(firstRead).toEqual({ revision: 'B' }); + expect(reloaded).toEqual({ revision: 'B' }); + await second.close(); +}); + +test('user receives batch failures after a real transaction abort with every prior value restored', async ({ page }) => { + // Given two committed records exist and the next native put will trigger a real IDB transaction abort. + await openStorageLab(page); + const pageErrors = []; + page.on('pageerror', error => pageErrors.push(error.message)); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await Promise.all(['A', 'B'].map(key => lab.native.setItem(`#TV_SYMBOL-${key}`, { revision: 1 }))); + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + window.__STORAGE_NATIVE__.abortNextPut(); + }); + + // When two writes and a read are accepted into the aborting batch. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const callbacks = []; + const callback = (error, value) => callbacks.push({ error: error instanceof Error || error instanceof DOMException, value }); + const outcomes = await Promise.allSettled([ + lab.store.setItem('#TV_SYMBOL-A', { revision: 2 }, callback), + lab.store.setItem('#TV_SYMBOL-B', { revision: 2 }, callback), + lab.store.getItem('#TV_SYMBOL-A', callback), + ]); + await lab.controller.drain(); + const metrics = window.__STORAGE_NATIVE__.snapshot(); + return { + statuses: outcomes.map(value => value.status), names: outcomes.map(value => value.reason?.name), callbacks, metrics, + persisted: await Promise.all(['A', 'B'].map(key => lab.native.getItem(`#TV_SYMBOL-${key}`))), + }; + }); + + // Then every accepted request rejects once and the actual database has rolled the entire batch back. + expect(result.statuses).toEqual(['rejected', 'rejected', 'rejected']); + expect(result.names).toEqual(['AbortError', 'AbortError', 'AbortError']); + expect(result.callbacks).toEqual([ + { error: true, value: undefined }, { error: true, value: undefined }, { error: true, value: undefined }, + ]); + expect(result.persisted).toEqual([{ revision: 1 }, { revision: 1 }]); + expect(result.metrics.aborted).toBe(1); + expect(result.metrics.pending).toBe(0); + expect(pageErrors).toEqual([]); +}); + +test('user receives success only after all accepted writes commit to the real database', async ({ page }) => { + // Given the chart store is ready and native transaction completion is being observed. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When a batch is saved and each returned Promise records the real transaction state at resolution. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const completions = await Promise.all(Array.from({ length: 20 }, (_, index) => ( + lab.store.setItem(`#TV_SYMBOL-COMMIT${index}`, { index }).then(value => ({ + value, native: window.__STORAGE_NATIVE__.snapshot(), + })) + ))); + return { completions, keys: await lab.native.keys() }; + }); + + // Then all twenty results arrive after native completion and all twenty keys are durable. + expect(result.completions.map(({ value }) => value)).toEqual(Array.from({ length: 20 }, (_, index) => ({ index }))); + expect(result.completions.every(({ native }) => native.completed >= 1 && native.pending === 0)).toBe(true); + expect(result.keys).toHaveLength(20); +}); + +test('user receives explicit capacity rejection without losing any accepted save', async ({ page }) => { + // Given the isolated adapter accepts at most two outstanding chart operations. + await openStorageLab(page, { options: { maxPendingOperations: 2 } }); + + // When three writes arrive synchronously before database readiness can finish. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const store = lab.create(); + const outcomes = await Promise.allSettled([0, 1, 2].map(index => store.setItem(`#TV_SYMBOL-LIMIT${index}`, index))); + await lab.controller.drain(); + return { + outcomes: outcomes.map(outcome => ({ status: outcome.status, value: outcome.value, message: outcome.reason?.message })), + persisted: await Promise.all([0, 1, 2].map(index => lab.native.getItem(`#TV_SYMBOL-LIMIT${index}`))), + keys: await lab.native.keys(), stats: lab.controller.getStats(), + }; + }); + + // Then the first two writes remain durable and the third fails explicitly instead of disappearing. + expect(result.outcomes.map(({ status }) => status)).toEqual(['fulfilled', 'fulfilled', 'rejected']); + expect(result.outcomes[2].message).toMatch(/capacity|limit|pending|queue/i); + expect(result.persisted).toEqual([0, 1, null]); + expect(result.keys.sort()).toEqual(['#TV_SYMBOL-LIMIT0', '#TV_SYMBOL-LIMIT1']); + expect(result.stats.pendingOperations).toBe(0); + expect(result.stats.pendingBytes).toBe(0); + expect(result.stats.peakPendingOperations).toBe(2); + expect(result.stats.rejectedOperations).toBe(1); +}); + +test('user rejects oversized pending data while a subsequent bounded save remains durable', async ({ page }) => { + // Given pending JSON input is limited to sixty-four bytes. + await openStorageLab(page, { options: { maxPendingBytes: 64 } }); + + // When an oversized chart string is followed by a small valid chart number. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const store = lab.create(); + const outcomes = await Promise.allSettled([ + store.setItem('#TV_SYMBOL-BIG', 'x'.repeat(1024)), + store.setItem('#TV_SYMBOL-SMALL', 7), + ]); + await lab.controller.drain(); + return { + statuses: outcomes.map(outcome => outcome.status), + keys: await lab.native.keys(), value: await lab.native.getItem('#TV_SYMBOL-SMALL'), stats: lab.controller.getStats(), + }; + }); + + // Then only the bounded save exists and every pending accounting reservation is released. + expect(result.statuses).toEqual(['rejected', 'fulfilled']); + expect(result.keys).toEqual(['#TV_SYMBOL-SMALL']); + expect(result.value).toBe(7); + expect(result.stats.pendingBytes).toBe(0); + expect(result.stats.pendingOperations).toBe(0); +}); + +test('user drains accepted work during stop and retained facades continue with native storage', async ({ page }) => { + // Given two operations are accepted before the new chart database becomes ready. + await openStorageLab(page); + + // When stop is requested while the accepted write and read are still queued. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const store = lab.create(); + const accepted = [store.setItem('#TV_SYMBOL-STOP', { saved: true }), store.getItem('#TV_SYMBOL-STOP')]; + const stopping = lab.controller.stop(); + await stopping; + const values = await Promise.all(accepted); + const retained = await Promise.allSettled([ + store.getItem('#TV_SYMBOL-STOP'), store.setItem('#TV_SYMBOL-LATE', 1), store.keys(), + ]); + const restored = lab.localforage.createInstance === lab.nativeCreateInstance; + const fresh = lab.create(); + await fresh.setItem('#TV_SYMBOL-NATIVE', 2); + return { + values, restored, statuses: retained.map(outcome => outcome.status), + keys: await lab.native.keys(), stats: lab.controller.getStats(), + saved: await lab.native.getItem('#TV_SYMBOL-STOP'), nativeSaved: await lab.native.getItem('#TV_SYMBOL-NATIVE'), + }; + }); + + // Then stop preserves queued work, allows retained-facade calls, and restores native factory behavior. + expect(result.values).toEqual([{ saved: true }, { saved: true }]); + expect(result.restored).toBe(true); + expect(result.statuses).toEqual(['fulfilled', 'fulfilled', 'fulfilled']); + expect(result.keys.sort()).toEqual(['#TV_SYMBOL-LATE', '#TV_SYMBOL-NATIVE', '#TV_SYMBOL-STOP']); + expect(result.saved).toEqual({ saved: true }); + expect(result.nativeSaved).toBe(2); + expect(result.stats.pendingOperations).toBe(0); + expect(result.stats.pendingBytes).toBe(0); +}); + +test('user cannot mutate managed configuration or submit values outside the JSON contract', async ({ page }) => { + // Given a managed store exposes a configuration snapshot for inspection. + await openStorageLab(page); + + // When callers mutate a snapshot and attempt configuration, driver, database, and non-JSON changes. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const store = lab.create(); + const snapshot = store.config(); + snapshot.name = 'changed-outside'; + const cycle = {}; + cycle.self = cycle; + const actions = [ + () => store.config({ name: 'changed-inside' }), + () => store.setDriver(lab.localforage.LOCALSTORAGE), + () => store.dropInstance(), + ...[undefined, NaN, Infinity, new Date(0), new Blob(['binary']), cycle] + .map(value => () => store.setItem('#TV_SYMBOL-INVALID', value)), + ]; + const outcomes = await Promise.allSettled(actions.map(action => Promise.resolve().then(action))); + return { + name: store.config().name, + statuses: outcomes.map(outcome => outcome.status), + messages: outcomes.map(outcome => outcome.reason?.message), + keys: await lab.native.keys(), + }; + }); + + // Then invalid operations fail explicitly without changing database identity or creating a key. + expect(result.name).toBe('chart_futures'); + expect(result.statuses).toEqual(Array(9).fill('rejected')); + expect(result.messages.every(message => typeof message === 'string' && message.length > 0)).toBe(true); + expect(result.keys).toEqual([]); +}); + +test('user receives a closed-connection failure without an automatic reopen or retry', async ({ page }) => { + // Given the real connection used by a completed managed save is deliberately closed. + await openStorageLab(page); + const closed = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.setItem('#TV_SYMBOL-BEFORE-CLOSE', 1); + const count = window.__STORAGE_NATIVE__.closeTarget('chart_futures'); + window.__STORAGE_NATIVE__.reset(); + return count; + }); + + // When the retained facade attempts another chart save through that closed connection. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const outcomes = await Promise.allSettled([lab.store.setItem('#TV_SYMBOL-AFTER-CLOSE', 2)]); + await lab.controller.drain(); + return { + status: outcomes[0].status, name: outcomes[0].reason?.name, message: outcomes[0].reason?.message, + metrics: window.__STORAGE_NATIVE__.snapshot(), stats: lab.controller.getStats(), + }; + }); + + // Then the storage error is explicit and no hidden database open retries the failed write. + expect(closed).toBeGreaterThanOrEqual(1); + expect(result.status).toBe('rejected'); + expect(result.message).toMatch(/clos|connection|transaction|invalid/i); + expect(result.metrics.opens).toBe(0); + expect(result.metrics.puts).toBe(0); + expect(result.stats.pendingOperations).toBe(0); +}); + +test('user splits accepted work at the configured batch size without losing a result', async ({ page }) => { + // Given a ready real chart store limits each batch to two operations. + await openStorageLab(page, { options: { maxBatchOperations: 2 } }); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When five saves are submitted together through the public storage facade. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const values = await Promise.all(Array.from({ length: 5 }, (_, index) => ( + lab.store.setItem(`#TV_SYMBOL-BATCH${index}`, index) + ))); + return { values, metrics: window.__STORAGE_NATIVE__.snapshot(), stats: lab.controller.getStats() }; + }); + + // Then all results complete through exactly three real committed transactions. + expect(result.values).toEqual([0, 1, 2, 3, 4]); + expect(result.metrics.transactions).toBe(3); + expect(result.metrics.completed).toBe(3); + expect(result.metrics.puts).toBe(5); + expect(result.stats.pendingOperations).toBe(0); +}); + +test('user receives an explicit oversized-result failure and can still read a bounded value', async ({ page }) => { + // Given native storage contains a large value beyond the adapter's result-size contract. + await openStorageLab(page, { options: { maxResultBytes: 32 } }); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await lab.native.setItem('#TV_SYMBOL-LARGE-RESULT', 'x'.repeat(1024)); + await lab.native.setItem('#TV_SYMBOL-SMALL-RESULT', 3); + lab.store = lab.create(); + }); + + // When the caller reads the oversized record before reading the independent bounded record. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const outcome = await Promise.allSettled([lab.store.getItem('#TV_SYMBOL-LARGE-RESULT')]); + return { + status: outcome[0].status, message: outcome[0].reason?.message, + small: await lab.store.getItem('#TV_SYMBOL-SMALL-RESULT'), + originalLength: (await lab.native.getItem('#TV_SYMBOL-LARGE-RESULT')).length, + stats: lab.controller.getStats(), + }; + }); + + // Then the oversized read fails without corrupting storage or blocking later valid work. + expect(result.status).toBe('rejected'); + expect(result.message).toMatch(/result|size|byte|limit/i); + expect(result.small).toBe(3); + expect(result.originalLength).toBe(1024); + expect(result.stats.pendingOperations).toBe(0); +}); + +test('user saves an admission-time JSON snapshot without sharing mutable result objects', async ({ page }) => { + // Given a caller owns a nested JSON chart object. + await openStorageLab(page); + + // When that object and later returned objects are mutated around asynchronous storage calls. + const result = await page.evaluate(async () => { + const store = window.__STORAGE_LAB__.create(); + const input = { nested: [1] }; + const saving = store.setItem('#TV_SYMBOL-SNAPSHOT', input); + input.nested[0] = 2; + const written = await saving; + const savedResult = structuredClone(written); + written.nested[0] = 3; + const read = await store.getItem('#TV_SYMBOL-SNAPSHOT'); + const firstRead = structuredClone(read); + read.nested[0] = 4; + return { savedResult, firstRead, secondRead: await store.getItem('#TV_SYMBOL-SNAPSHOT') }; + }); + + // Then persistence and every result retain the original snapshot instead of caller mutations. + expect(result).toEqual({ savedResult: { nested: [1] }, firstRead: { nested: [1] }, secondRead: { nested: [1] } }); +}); + +test('user completes an external version upgrade and recovers storage only after a fresh page lifecycle', async ({ page }) => { + // Given a managed facade has committed through the library's shared native connection. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.setItem('#TV_SYMBOL-UPGRADE', 1); + window.__STORAGE_NATIVE__.reset(); + }); + + // When an external native connection upgrades the database and the old facade attempts another save. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const previousVersion = window.__STORAGE_NATIVE__.version('chart_futures'); + const upgrade = await new Promise((resolve, reject) => { + const request = indexedDB.open('chart_futures', previousVersion + 1); + let upgraded = false; + request.onupgradeneeded = () => { upgraded = true; }; + request.onerror = () => reject(request.error); + request.onblocked = () => reject(new Error('The external version upgrade was blocked')); + request.onsuccess = () => { + const version = request.result.version; + request.result.close(); + resolve({ upgraded, version }); + }; + }); + const nativeAfterUpgrade = window.__STORAGE_NATIVE__.snapshot(); + window.__STORAGE_NATIVE__.reset(); + const outcome = await Promise.allSettled([lab.store.setItem('#TV_SYMBOL-UPGRADE', 2)]); + await lab.controller.stop(); + return { + previousVersion, upgrade, nativeAfterUpgrade, status: outcome[0].status, + afterFailure: window.__STORAGE_NATIVE__.snapshot(), + factoryRestored: lab.localforage.createInstance === lab.nativeCreateInstance, + }; + }); + + // Then the actual upgrade completes, the stale facade fails without reopening, and stop restores the factory. + expect(result.upgrade).toEqual({ upgraded: true, version: result.previousVersion + 1 }); + expect(result.nativeAfterUpgrade.versionChanges).toBeGreaterThanOrEqual(1); + expect(result.status).toBe('rejected'); + expect(result.afterFailure.opens).toBe(0); + expect(result.afterFailure.puts).toBe(0); + expect(result.factoryRestored).toBe(true); + + // When a real reload creates a fresh library lifecycle against the upgraded database. + await page.reload(); + await initializeStorageLab(page); + const recovered = await page.evaluate(async () => { + const store = window.__STORAGE_LAB__.create(); + const before = await store.getItem('#TV_SYMBOL-UPGRADE'); + await store.setItem('#TV_SYMBOL-UPGRADE', 3); + return { before, after: await store.getItem('#TV_SYMBOL-UPGRADE') }; + }); + + // Then the last committed value survives and a fresh lifecycle can save again. + expect(recovered).toEqual({ before: 1, after: 3 }); +}); + +test('user installs storage synchronously before the host creates its first instance', async ({ page }) => { + // Given capture and its synchronous adapter callback precede module registration and execution. + await openStorageLab(page, { capture: true, captureInstall: true, initialize: false }); + + // When the real host requires storage and immediately creates and uses its first instance. + const result = await page.evaluate(async () => { + let hostExport; + let store; + let firstSave; + let pendingBeforeRequireReturns; + let factoryExecutions = 0; + self.webpackChunkfutures_trade_ui.push([ + ['codex-chart-storage-sync-host'], {}, require => { + const original = require.m[43917]; + require.m[43917] = function (...args) { + factoryExecutions += 1; + return original.apply(this, args); + }; + hostExport = require(43917); + store = hostExport.createInstance({ + name: 'chart_futures', storeName: 'keyvaluepairs', driver: hostExport.INDEXEDDB, + }); + firstSave = store.setItem('#TV_SYMBOL-SYNC-CAPTURE', { saved: true }); + pendingBeforeRequireReturns = window.__STORAGE_CAPTURE_CONTROLLER__.getStats().pendingOperations; + }, + ]); + await firstSave; + const capturedSameExport = await window.__STORAGE_CAPTURE__.captured === hostExport; + window.__STORAGE_NATIVE__.reset(); + const repeated = await Promise.all(Array.from({ length: 12 }, () => ( + store.setItem('#TV_SYMBOL-SYNC-CAPTURE', { saved: true }) + ))); + return { + pendingBeforeRequireReturns, factoryExecutions, capturedSameExport, repeated, + metrics: window.__STORAGE_NATIVE__.snapshot(), + saved: await store.getItem('#TV_SYMBOL-SYNC-CAPTURE'), + }; + }); + + // Then the first host instance performs real batching and unchanged-write suppression. + expect(result.pendingBeforeRequireReturns).toBe(1); + expect(result.factoryExecutions).toBe(1); + expect(result.capturedSameExport).toBe(true); + expect(result.repeated).toEqual(Array.from({ length: 12 }, () => ({ saved: true }))); + expect(result.metrics.transactions).toBe(1); + expect(result.metrics.puts).toBe(0); + expect(result.saved).toEqual({ saved: true }); +}); + +test('user keeps every accepted save when one callback throws a caller error', async ({ page }) => { + // Given a ready managed store has an event listener for the deliberately thrown caller error. + await openStorageLab(page); + const reportedError = page.waitForEvent('pageerror', { predicate: error => error.message === 'Storage lab callback failure' }); + + // When one callback throws while three valid saves are completing. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const store = lab.create(); + const callbacks = []; + const values = await Promise.all([0, 1, 2].map(index => store.setItem(`#TV_SYMBOL-CALLBACK${index}`, index, (error, value) => { + callbacks.push({ index, success: error === null, value }); + if (index === 0) throw new Error('Storage lab callback failure'); + }))); + return { + values, callbacks, + persisted: await Promise.all([0, 1, 2].map(index => lab.native.getItem(`#TV_SYMBOL-CALLBACK${index}`))), + pending: lab.controller.getStats().pendingOperations, + }; + }); + const error = await reportedError; + + // Then the error is reported separately and all three callbacks, promises, and committed values survive. + expect(error.message).toBe('Storage lab callback failure'); + expect(result.values).toEqual([0, 1, 2]); + expect(result.callbacks).toEqual([ + { index: 0, success: true, value: 0 }, { index: 1, success: true, value: 1 }, { index: 2, success: true, value: 2 }, + ]); + expect(result.persisted).toEqual([0, 1, 2]); + expect(result.pending).toBe(0); +}); + +test('user splits chart saves at the configured byte limit while retaining every value', async ({ page }) => { + // Given each individual JSON save fits a hundred-byte batch but two saves do not. + await openStorageLab(page, { options: { maxBatchBytes: 100 } }); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When three sixty-character records are accepted together. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const values = await Promise.all([0, 1, 2].map(index => lab.store.setItem(`#TV_SYMBOL-BYTES${index}`, 'x'.repeat(60)))); + const metrics = window.__STORAGE_NATIVE__.snapshot(); + return { values, metrics, persisted: await Promise.all([0, 1, 2].map(index => lab.native.getItem(`#TV_SYMBOL-BYTES${index}`))) }; + }); + + // Then three bounded transactions preserve all saved and returned values. + expect(result.values).toEqual(Array(3).fill('x'.repeat(60))); + expect(result.persisted).toEqual(Array(3).fill('x'.repeat(60))); + expect(result.metrics.transactions).toBe(3); + expect(result.metrics.completed).toBe(3); + expect(result.metrics.puts).toBe(3); +}); + +test('user counts native barriers against pending capacity and preserves their place in order', async ({ page }) => { + // Given only two pending operations may be accepted before fresh database initialization. + await openStorageLab(page, { options: { maxPendingOperations: 2 } }); + + // When a save and keys barrier occupy capacity before a third save arrives. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const store = lab.create(); + const outcomes = await Promise.allSettled([ + store.setItem('#TV_SYMBOL-BARRIER-FIRST', 1), store.keys(), store.setItem('#TV_SYMBOL-BARRIER-LATE', 2), + ]); + await lab.controller.drain(); + return { + statuses: outcomes.map(outcome => outcome.status), + barrierKeys: outcomes[1].value, persistedKeys: await lab.native.keys(), stats: lab.controller.getStats(), + }; + }); + + // Then the barrier sees the first committed key and the over-capacity save explicitly fails. + expect(result.statuses).toEqual(['fulfilled', 'fulfilled', 'rejected']); + expect(result.barrierKeys).toEqual(['#TV_SYMBOL-BARRIER-FIRST']); + expect(result.persistedKeys).toEqual(['#TV_SYMBOL-BARRIER-FIRST']); + expect(result.stats.peakPendingOperations).toBe(2); + expect(result.stats.pendingOperations).toBe(0); +}); + +test('user stops capture after registration and restores the unexecuted original factory', async ({ page }) => { + // Given module registration has completed but the host has not required the storage factory. + await openStorageLab(page, { capture: true, initialize: false }); + + // When capture stops before the first host require. + const stopped = await page.evaluate(async () => { + const capture = window.__STORAGE_CAPTURE__; + const completion = capture.captured.then(() => ({ status: 'fulfilled' }), error => ({ status: 'rejected', message: error.message })); + capture.stop(); + let restoredFactory; + self.webpackChunkfutures_trade_ui.push([ + ['codex-chart-storage-stopped-host'], {}, require => { + restoredFactory = require.m[43917] === window.__STORAGE_ORIGINAL_MODULE_FACTORY__; + }, + ]); + return { + completion: await completion, + restoredFactory, + restoredPush: self.webpackChunkfutures_trade_ui.push === window.__STORAGE_CAPTURE_ORIGINAL_PUSH__, + }; + }); + await initializeStorageLab(page, { install: false }); + const identity = await page.evaluate(() => window.__STORAGE_LAB__.cacheIdentity()); + + // Then capture rejects explicitly and the host can execute the restored native factory exactly once. + expect(stopped.completion.status).toBe('rejected'); + expect(stopped.completion.message).toMatch(/stop.*before.*execution/i); + expect(stopped.restoredFactory).toBe(true); + expect(stopped.restoredPush).toBe(true); + expect(identity).toEqual({ sameExport: true, factoryExecutions: 1 }); +}); + +test('user receives the original factory error and capture restores every owned runtime hook', async ({ page }) => { + // Given the real runtime will execute an explicitly failing test-owned module factory. + await openStorageRuntime(page); + + // When the host requires that factory through the capture observer. + const result = await page.evaluate(async () => { + const { observeChartStorageModule } = await import('/capture.js'); + const queue = self.webpackChunkfutures_trade_ui; + const originalPush = queue.push; + let executions = 0; + const originalFactory = () => { + executions += 1; + throw new Error('Storage lab original factory failed'); + }; + const capture = observeChartStorageModule(); + const captured = capture.captured.then(() => ({ status: 'fulfilled' }), error => ({ status: 'rejected', message: error.message })); + let runtimeRequire; + queue.push([['codex-chart-storage-failing-module'], { 43917: originalFactory }, require => { runtimeRequire = require; }]); + let hostError; + try { runtimeRequire(43917); } catch (error) { hostError = error.message; } + return { + hostError, captured: await captured, executions, + factoryRestored: runtimeRequire.m[43917] === originalFactory, pushRestored: queue.push === originalPush, + }; + }); + + // Then both consumers receive the original error and no interception hook remains installed. + expect(result).toEqual({ + hostError: 'Storage lab original factory failed', + captured: { status: 'rejected', message: 'Storage lab original factory failed' }, + executions: 1, factoryRestored: true, pushRestored: true, + }); +}); + +test('user receives a capture callback error and the real storage factory remains restored', async ({ page }) => { + // Given a capture callback deliberately fails after the actual localForage factory returns. + await openStorageRuntime(page); + await page.evaluate(async () => { + const { observeChartStorageModule } = await import('/capture.js'); + const queue = self.webpackChunkfutures_trade_ui; + window.__CAPTURE_ERROR_ORIGINAL_PUSH__ = queue.push; + const capture = observeChartStorageModule(queue, { onCapture() { throw new Error('Storage lab onCapture failed'); } }); + window.__CAPTURE_ERROR_RESULT__ = capture.captured.then(() => ({ status: 'fulfilled' }), error => ({ status: 'rejected', message: error.message })); + const observedPush = queue.push; + window.__CAPTURE_ERROR_OBSERVED_PUSH__ = observedPush; + queue.push = function (chunk) { + if (Object.hasOwn(chunk[1], '43917')) window.__CAPTURE_ERROR_ORIGINAL_FACTORY__ = chunk[1][43917]; + return observedPush.call(this, chunk); + }; + }); + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/localforage-module.js` }); + + // When the host first requires the real module and handles the callback error explicitly. + const result = await page.evaluate(async () => { + const queue = self.webpackChunkfutures_trade_ui; + queue.push = window.__CAPTURE_ERROR_OBSERVED_PUSH__; + let runtimeRequire; + queue.push([['codex-chart-storage-capture-error'], {}, require => { runtimeRequire = require; }]); + let hostError; + try { runtimeRequire(43917); } catch (error) { hostError = error.message; } + return { + hostError, captured: await window.__CAPTURE_ERROR_RESULT__, + factoryRestored: runtimeRequire.m[43917] === window.__CAPTURE_ERROR_ORIGINAL_FACTORY__, + pushRestored: queue.push === window.__CAPTURE_ERROR_ORIGINAL_PUSH__, + }; + }); + + // Then the callback failure rejects capture and leaves the real runtime's factory and queue restored. + expect(result).toEqual({ + hostError: 'Storage lab onCapture failed', + captured: { status: 'rejected', message: 'Storage lab onCapture failed' }, + factoryRestored: true, pushRestored: true, + }); +}); + +test('user rejects an oversized unmanaged save before its native barrier can persist data', async ({ page }) => { + // Given a managed store limits any result to thirty-two bytes. + await openStorageLab(page, { options: { maxResultBytes: 32 } }); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When an unmanaged key attempts a native-barrier save whose returned value exceeds the limit. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const outcome = await Promise.allSettled([lab.store.setItem('unmanaged-large-value', 'x'.repeat(100))]); + return { + status: outcome[0].status, message: outcome[0].reason?.message, + metrics: window.__STORAGE_NATIVE__.snapshot(), keys: await lab.native.keys(), + value: await lab.native.getItem('unmanaged-large-value'), + }; + }); + + // Then explicit rejection precedes every native put and no rejected value is stored. + expect(result.status).toBe('rejected'); + expect(result.message).toMatch(/result|size|byte|limit/i); + expect(result.metrics.puts).toBe(0); + expect(result.keys).toEqual([]); + expect(result.value).toBe(null); +}); + +test('user completes eight simultaneous 290-key save rounds with every callback and bounded native work', async ({ page }, testInfo) => { + // Given the native library has committed 290 historical chart snapshots before observation begins. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.concurrentRecords = Array.from({ length: 290 }, (_, index) => ({ + key: `#TV_SYMBOL-CONCURRENT${index}`, value: { symbol: `CONCURRENT${index}`, drawings: [index, { visible: true }] }, + })); + await Promise.all(lab.concurrentRecords.map(({ key, value }) => lab.native.setItem(key, value))); + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When all eight rounds begin together and each read is followed by its corresponding unchanged save. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const callbacks = []; + const rounds = Array.from({ length: 8 }, (_, round) => Promise.all(lab.concurrentRecords.map(async ({ key, value }, index) => { + const callback = kind => (error, actual) => callbacks.push({ + id: `${round}:${index}:${kind}`, success: error === null, matches: JSON.stringify(actual) === JSON.stringify(value), + }); + const read = await lab.store.getItem(key, callback('get')); + const saved = await lab.store.setItem(key, read, callback('set')); + return JSON.stringify(read) === JSON.stringify(value) && JSON.stringify(saved) === JSON.stringify(value); + }))); + const matches = await Promise.all(rounds); + const metrics = window.__STORAGE_NATIVE__.snapshot(); + const stats = lab.controller.getStats(); + const persisted = await Promise.all(lab.concurrentRecords.map(({ key }) => lab.native.getItem(key))); + return { + matches: matches.flat().every(Boolean), metrics, stats, + callbacks: callbacks.length, uniqueCallbacks: new Set(callbacks.map(({ id }) => id)).size, + callbackValuesMatch: callbacks.every(({ success, matches }) => success && matches), + persistedMatches: persisted.every((value, index) => JSON.stringify(value) === JSON.stringify(lab.concurrentRecords[index].value)), + }; + }); + await displayStorageResult(page, result); + const measurementPath = testInfo.outputPath('concurrent-storage-measurement.json'); + await writeFile(measurementPath, JSON.stringify(result, null, 2)); + await testInfo.attach('concurrent-storage-measurement.json', { path: measurementPath, contentType: 'application/json' }); + + // Then all 4640 distinct callbacks and results match while concurrent work remains batched and unchanged writes vanish. + expect(result.matches).toBe(true); + expect(result.callbackValuesMatch).toBe(true); + expect(result.persistedMatches).toBe(true); + expect(result.callbacks).toBe(4640); + expect(result.uniqueCallbacks).toBe(4640); + expect(result.metrics.transactions).toBeLessThanOrEqual(32); + expect(result.metrics.puts).toBe(0); + expect(result.stats.pendingOperations).toBe(0); + expect(result.stats.pendingBytes).toBe(0); +}); + +test('user retains upstream native connection recovery for an unmanaged barrier save', async ({ page }) => { + // Given the real connection closes after a managed chart save has committed. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.setItem('#TV_SYMBOL-BEFORE-BARRIER', 1); + window.__STORAGE_NATIVE__.closeTarget('chart_futures'); + window.__STORAGE_NATIVE__.reset(); + }); + + // When an unmanaged key uses localForage's native barrier path through the closed connection. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const value = await lab.store.setItem('unmanaged-recovery', { recovered: true }); + return { + value, metrics: window.__STORAGE_NATIVE__.snapshot(), + persisted: await lab.native.getItem('unmanaged-recovery'), + previous: await lab.native.getItem('#TV_SYMBOL-BEFORE-BARRIER'), + }; + }); + + // Then native localForage reopens and commits the barrier while preserving the earlier chart value. + expect(result.value).toEqual({ recovered: true }); + expect(result.persisted).toEqual({ recovered: true }); + expect(result.previous).toBe(1); + expect(result.metrics.opens).toBeGreaterThanOrEqual(1); + expect(result.metrics.puts).toBe(1); +}); + +test('user retains native barrier rejection even when its abort reason is null', async ({ page }) => { + // Given an unmanaged native record exists and its next put will abort a real transaction. + await openStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await lab.native.setItem('unmanaged-abort', 1); + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + window.__STORAGE_NATIVE__.abortNextPut(); + }); + + // When the native barrier attempts to replace that record and localForage rejects with a null reason. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const outcome = await Promise.allSettled([lab.store.setItem('unmanaged-abort', 2)]); + return { + status: outcome[0].status, reason: outcome[0].reason, + persisted: await lab.native.getItem('unmanaged-abort'), metrics: window.__STORAGE_NATIVE__.snapshot(), + }; + }); + + // Then a null rejection reason cannot become success and rollback preserves the original value. + expect(result.status).toBe('rejected'); + expect(result.reason).toBe(null); + expect(result.persisted).toBe(1); + expect(result.metrics.aborted).toBe(1); +}); + +test('user receives real quota failures for every batched request while prior values remain durable', async ({ page, context }) => { + // Given the isolated browser has two committed records and a real sixty-four-KiB origin quota. + await openStorageRuntime(page); + const pageErrors = []; + page.on('pageerror', error => pageErrors.push(error.message)); + const session = await context.newCDPSession(page); + try { + // Chromium caches bucket space from its first writes, so override quota before initialization. + await session.send('Storage.overrideQuotaForOrigin', { origin: STORAGE_LAB_ORIGIN, quotaSize: 64 * 1024 }); + const quota = await session.send('Storage.getUsageAndQuota', { origin: STORAGE_LAB_ORIGIN }); + expect(quota.overrideActive).toBe(true); + expect(quota.quota).toBe(64 * 1024); + await page.addScriptTag({ url: `${STORAGE_LAB_ORIGIN}/localforage-module.js` }); + await initializeStorageLab(page); + await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + await Promise.all([ + lab.native.setItem('#TV_SYMBOL-QUOTA-A', { revision: 1 }), + lab.native.setItem('#TV_SYMBOL-QUOTA-B', { revision: 1 }), + ]); + lab.store = lab.create(); + await lab.store.ready(); + window.__STORAGE_NATIVE__.reset(); + }); + + // When a one-MiB JSON string and two related requests enter the same real transaction. + const result = await page.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + const chunks = Array.from({ length: 16 }, () => { + const bytes = crypto.getRandomValues(new Uint8Array(65536)); + return Array.from(bytes, byte => alphabet[byte & 63]).join(''); + }); + const large = chunks.join(''); + const callbacks = []; + const callback = id => (error, value) => callbacks.push({ id, name: error?.name, value }); + const outcomes = await Promise.allSettled([ + lab.store.setItem('#TV_SYMBOL-QUOTA-A', large, callback('large')), + lab.store.setItem('#TV_SYMBOL-QUOTA-B', { revision: 2 }, callback('small')), + lab.store.getItem('#TV_SYMBOL-QUOTA-A', callback('read')), + ]); + await lab.controller.drain(); + return { + inputBytes: new TextEncoder().encode(JSON.stringify(large)).byteLength, + statuses: outcomes.map(outcome => outcome.status), names: outcomes.map(outcome => outcome.reason?.name), + callbacks, metrics: window.__STORAGE_NATIVE__.snapshot(), stats: lab.controller.getStats(), + persisted: await Promise.all([ + lab.native.getItem('#TV_SYMBOL-QUOTA-A'), lab.native.getItem('#TV_SYMBOL-QUOTA-B'), + ]), + }; + }); + + // Then real QuotaExceededError rejects every request once and rollback preserves both original records. + expect(result.inputBytes).toBe(1024 * 1024 + 2); + expect(result.statuses).toEqual(['rejected', 'rejected', 'rejected']); + expect(result.names).toEqual(['QuotaExceededError', 'QuotaExceededError', 'QuotaExceededError']); + expect(result.callbacks).toEqual([ + { id: 'large', name: 'QuotaExceededError', value: undefined }, + { id: 'small', name: 'QuotaExceededError', value: undefined }, + { id: 'read', name: 'QuotaExceededError', value: undefined }, + ]); + expect(result.persisted).toEqual([{ revision: 1 }, { revision: 1 }]); + expect(result.metrics.transactions).toBe(1); + expect(result.metrics.aborted).toBe(1); + expect(result.stats.pendingOperations).toBe(0); + expect(result.stats.pendingBytes).toBe(0); + expect(pageErrors).toEqual([]); + } finally { + try { + await session.send('Storage.overrideQuotaForOrigin', { origin: STORAGE_LAB_ORIGIN }); + } finally { + await session.detach(); + } + } +}); + +test('user competing across two pages writes the same new chart state only once', async ({ page, context }) => { + // Given both pages are ready and a native readwrite transaction holds their shared chart store lock. + await openStorageLab(page); + const second = await context.newPage(); + await openStorageLab(second); + await page.evaluate(() => window.__STORAGE_LAB__.native.setItem('#TV_SYMBOL-CONTENDED', 'A')); + await Promise.all([page, second].map(target => target.evaluate(async () => { + const lab = window.__STORAGE_LAB__; + lab.store = lab.create(); + await lab.store.ready(); + }))); + await page.evaluate(() => { + const database = window.__STORAGE_LAB__.native._dbInfo.db; + const transaction = database.transaction('keyvaluepairs', 'readwrite'); + const store = transaction.objectStore('keyvaluepairs'); + const control = { release: false, reads: 0 }; + const completion = new Promise((resolve, reject) => { + transaction.addEventListener('complete', () => resolve('complete'), { once: true }); + transaction.addEventListener('abort', () => reject(transaction.error), { once: true }); + }); + function continueHolding() { + const request = store.get('#TV_SYMBOL-CONTENDED'); + request.onsuccess = () => { + control.reads += 1; + if (!control.release) continueHolding(); + }; + } + window.__STORAGE_CONTENTION_LOCK__ = { control, completion }; + continueHolding(); + }); + try { + await expect.poll(() => page.evaluate(() => window.__STORAGE_CONTENTION_LOCK__.control.reads)).toBeGreaterThan(0); + await Promise.all([page, second].map(target => target.evaluate(() => window.__STORAGE_NATIVE__.reset()))); + + // When both pages create pending write transactions for B before the lock is explicitly released. + await Promise.all([page, second].map(target => target.evaluate(() => { + const lab = window.__STORAGE_LAB__; + lab.contendingSave = lab.store.setItem('#TV_SYMBOL-CONTENDED', 'B').then( + value => ({ status: 'fulfilled', value }), error => ({ status: 'rejected', name: error?.name }), + ); + }))); + await expect.poll(() => Promise.all([page, second].map(target => target.evaluate(() => ( + window.__STORAGE_NATIVE__.snapshot().pending + ))))).toEqual([1, 1]); + await page.evaluate(async () => { + const lock = window.__STORAGE_CONTENTION_LOCK__; + lock.control.release = true; + await lock.completion; + }); + const results = await Promise.all([page, second].map(target => target.evaluate(async () => ({ + outcome: await window.__STORAGE_LAB__.contendingSave, + metrics: window.__STORAGE_NATIVE__.snapshot(), + })))); + const persisted = await page.evaluate(() => window.__STORAGE_LAB__.native.getItem('#TV_SYMBOL-CONTENDED')); + + // Then IndexedDB serializes both comparisons and only one physical put is needed for both successful results. + expect(results.map(({ outcome }) => outcome)).toEqual([ + { status: 'fulfilled', value: 'B' }, { status: 'fulfilled', value: 'B' }, + ]); + expect(results.map(({ metrics }) => metrics.transactions)).toEqual([1, 1]); + expect(results.map(({ metrics }) => metrics.completed)).toEqual([1, 1]); + expect(results.reduce((sum, { metrics }) => sum + metrics.puts, 0)).toBe(1); + expect(results.map(({ metrics }) => metrics.pending)).toEqual([0, 0]); + expect(persisted).toBe('B'); + } finally { + try { + await page.evaluate(async () => { + const lock = window.__STORAGE_CONTENTION_LOCK__; + lock.control.release = true; + await lock.completion; + }); + } finally { + await second.close(); + } + } +}); diff --git a/experiments/binance-chart-storage/README.md b/experiments/binance-chart-storage/README.md new file mode 100644 index 0000000..b09b754 --- /dev/null +++ b/experiments/binance-chart-storage/README.md @@ -0,0 +1,632 @@ +# Binance chart-storage experiment + +The reviewed production successor is now maintained independently in +[`src/binance-orderbook-trade/chart-storage/`](../../src/binance-orderbook-trade/chart-storage/), with its +[current contract](../../docs/binance-chart-storage-development.md). The files +below retain the historical experiments and native baselines; their former +production restrictions and remaining-work lists describe those experiments. + +This directory contains isolated experiments and an unpublished native-only +preflight review artifact. No production userscript imports this code. The current +candidate changes only a pinned mirror-write expression in module `70940`. +The earlier global localForage facade is retained as historical research: it +changes native mixed-operation ordering and is not the production candidate. +The mirror writer defaults to `scope: 'lab'`, which rejects every origin except +`https://chart-storage.test`. Its explicit `isolated-ususdt-probe` scope permits +only the exact top-level `https://www.binance.com/zh-CN/futures/USUSDT` page. +That guard cannot establish an anonymous profile: the separate probe harness +must own a fresh Chrome process/profile with no login or extensions. The +historical adapter and original capture remain lab-only. Bootstrap and passive +preflight also permit the exact top-level USUSDT page; neither enables a writer +by itself. No production entrypoint imports this experiment. + +## Current candidate: one mirror destination batch + +The global facade orders calls before waiting for localForage initialization. +The captured native `setItem` prepares its value in an additional Promise stage, +so an immediately following `getItem`, `removeItem` or `clear` can create its +transaction first. The facade's stronger issue order changes returned values, +callback order and final records. The original Basic clear workflow reproduces +this difference. That is a compatibility failure for a transparent storage +replacement, even when both implementations preserve their own intended order. + +`generate-mirror-module.js` instead generates a static replacement for the exact +public `70940` method in `TradingView.99bc5074.js`. Acorn identifies exactly one +mirror destination expression, and generation verifies the complete source hash, +factory hash, UTF-16 ranges and absence of a colliding dispatch identifier. The +only change replaces the original `Wr=fr.map(...)` RHS with: + +```js +__dispatchChartMirror(Sr,fr,()=>ORIGINAL_FR_MAP_EXPRESSION) +``` + +The complete original method occupies `[6665,36819)` and has SHA-256 +`37d249d1ceec4a41b54a959c1ab2c4b52b3061e504cf8cd81a824962af8c6c88`. +The replaced RHS occupies `[33783,34025)`. Reversing that insertion must reproduce +the complete original method byte for byte. There is no `eval` or `new Function`. +`replaceChartMirrorFactory` synchronously compares the full original +`Function.prototype.toString` result before returning the static candidate. +See the [scoped fixture provenance](../../test/fixtures/binance-chart-storage/mirror-scoped.md). + +`bootstrap.js` selects `70940` only when an explicit `replaceMirrorFactory` +callback is supplied; its default `43917` capture remains unchanged. Rejection +leaves the original module executable and does not poison its cache. A callback +that synchronously stops observation must also leave the original registration +untouched. Both loading orders first failed that regression; rechecking the +active state immediately after the callback now makes them pass. Cleanup restores +queue and factory slots. Already executed exports remain cached normally. + +The unchanged host code still owns its guards and catch, source `keys()` and +per-key `getItem()` calls, target namespace exclusion, ordinary TradingView save, +Basic settings and reset/clear paths. The writer never replaces localForage's +`createInstance` or instance methods. It reuses only the JSON utility exports +from `adapter.js`; importing those exports does not install the historical facade. + +`mirror-writer.js` accepts one explicit destination batch and compares current +values inside one IndexedDB read/write transaction, putting only changed or +missing records. Stored null is distinguished from a missing key. Empty entries +perform no initialization or transaction. Values are copied as validated JSON +before admission. Defaults bound each batch to 512 entries and 4 MiB of input, +with a separate 4 MiB stored-read budget; pending work is limited to 16 batches +and 64 MiB. Count/input limits reject before any entry is written; a read-budget +failure aborts the whole destination transaction. +The explicit page-probe scope changes only origin/path/frame admission; it does +not broaden the database, driver, key, JSON or resource-limit contracts. + +Stop closes admission and drains the finite set of accepted batches. A later +mirror dispatch during drain waits for that fence, then evaluates its retained +original async per-key map; after stop it invokes that native thunk directly. +Later calls do not extend the optimized queue. Ordinary native save, Basic and +clear calls are not intercepted or placed behind this mirror fence. Accepted +failures are not replayed, and neither cleanup nor stop closes shared library +connections. + +This is deliberately not native mirror transaction equivalence. The original +per-key writes can partially commit; the candidate commits or aborts the entire +destination batch and changes interleaving with concurrent native transactions. +Limits introduce explicit rejection conditions. The outer native catch remains +unchanged. Source reads are still a non-atomic sequence, and two databases cannot +share this transaction. Neither lost-update protection nor blank-page recovery +follows from this change. + +## Current scoped evidence + +The scoped suite contains 19 scenarios and the dedicated writer suite contains +18. They cover exact source identity and inverse replacement, the original save +and mirror callbacks, guards, empty/null records, native API identity and the +same-stack set/get/remove/clear matrix, strict registration refusal, both runtime +orders, stop and callback reentry, real abort/quota rollback, limits, blocked +initialization, concurrent native clear and another tab's changed target values. +The scoped and existing startup-integration suites first passed a targeted +33-scenario run after the reentrant-stop fix. Before the explicit page-probe +scope was added, the combined experiment run passed all 105 Chromium scenarios, +all eight Node origin/path/frame cases and the complete test-policy lint. The browser report is +`/tmp/chart-storage-final-20261006.json`. +The scope change then expanded the Node suite to 18 cases: three new cases first +failed, then all 18 passed. Both mirror browser suites passed all 37 scenarios, +and the full test-policy lint passed again. Default production-origin refusal +and the explicit scope's exact origin/path/top-frame boundaries are separate +assertions. +The reusable anonymous-probe contract and native-only preflight then added four +and 18 Node scenarios respectively. Together with the 18 scope/origin cases, +all 40 Node cases passed; the 37 mirror browser scenarios also passed. The +preflight build and its dependency boundary were independently reviewed. + +For the current 32-chain workload, each database starts with 290 historical +drawing records. Eight sequential rounds each complete four concurrent original +save/mirror chains with the same changed revision. Both databases retain all +293 expected keys, historical values and the final chart/drawing data. + +| Current 32-chain workload | Transactions | Puts | Gets | +| --- | ---: | ---: | ---: | +| Native localForage | 18,912 | 9,472 | 9,408 | +| Scoped mirror writer | 9,568 | 112 | 18,784 | + +Setup and final verification I/O are excluded. The scoped path retains the +source save/read work: its 112 puts comprise 96 ordinary source writes and 16 +changed destination writes. Target comparisons add 9,376 gets. The two measured +workloads ended with zero pending or aborted transactions and all 32 chains +complete. The final extracted metrics are +`/tmp/chart-storage-mirror-measurements-20261006.json`; a separate fresh two-case +measurement in `/tmp/chart-storage-mirror-scoped-doc-metrics.json` confirmed the +same counts. These counts do not measure +sustained production CPU, latency, competing revisions or blank-page recovery. + +The offline evidence separates full-module static identity, exact callback +execution with original dependencies and real IndexedDB, and synthetic module +lifecycle in the actual captured runtime. Offline callback fixtures do not +execute the complete public React module or establish rendered chart readiness. + +A separate anonymous Chrome `154.0.8037.98` run then injected the complete static +`70940` candidate before registration in a temporary profile with no login or +extensions. The dispatch service was native-only: it would evaluate the exact +original thunk, and the lab writer was not installed. Strict validation attempted +once, matched once and executed the replaced module once. Capture occurred at +5,975.4 ms. At 10,015.6 ms, `activeChart().dataReady()` was true, and the inspected +screenshot showed USUSDT candles, moving averages, volume and a populated orderbook. +The observer stopped, no page errors were reported, and the browser was closed. + +That run recorded `dispatches: 0`: anonymous startup did not trigger the mirror. +It proves full-module registration/execution and rendered startup with the +native-only service, not execution of the injected mirror branch, real batched +writes, production compatibility or blank-page recovery. The result, injection +and screenshot are in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-native-preflight-HeCAar/`. +The earlier anonymous `43917` observation below remains a separate historical +result. + +A later, independent anonymous Chrome `154.0.8037.98` shape probe exercised the +real mirror using only its original native thunk. In the bounded interval-change +stage, switching the native interval from `60` to `15` advanced completed mirrors +from 6 to 7. Earlier activity had already produced mirrors; the final total of +seven is not attributed to that one action. All seven completed destinations +were `chart_delivery` using IndexedDB, with zero mirror or JSON-shape failures. +Observed maxima were four entries, 49,003 encoded batch bytes and 24,504 bytes +for one record. These values establish this anonymous sample's compatibility +with the JSON and size contracts, not every saved chart's compatibility. + +The native chart remained data-ready at resolution `15`, and the inspected +screenshot showed the chart and populated orderbook. Observation stopped and +the temporary browser closed. Evidence is in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-shape-probe-yDfadK/`. +This supplies real native mirror invocation and shape evidence beyond the +earlier zero-dispatch startup. It does not establish real-page bulk execution. + +## Anonymous bulk execution: 2026-10-07 Beijing time + +An independently launched anonymous Chrome `154.0.8037.98` then enabled the real +batch writer through the explicit USUSDT probe scope. The retained report starts +at `2026-10-06T16:24:24.572Z`, which is October 7 in Beijing. The exact factory +matched and executed once. Five accepted batches committed in five destination +transactions, with eight writes and eleven unchanged writes skipped. Rejected, +failed, aborted and pending counts were zero; peak admission was two batches and +98,006 bytes. + +| Verified stage | Completed mirror calls | Bulk commits | Readback | +| --- | ---: | ---: | --- | +| One-hour chart interval stage | 0 → 4 | 4 | Four captured entries matched | +| One hour → fifteen minutes | 4 → 5 | 5 total | Four captured entries matched | +| After writer stop, four-hour interval | 5 → 6 | Still 5 | Four captured entries matched | + +The first stage's four calls include the activity observed during that interval +stage and are not four saves attributed to one click. Each readback compares +destination records with the corresponding captured dispatch snapshot. After +`writer.stop()`, the four-hour stage increased `nativeCalls` from zero to one; +the complete writer statistics remained unchanged. This verifies native thunk +continuation through an already mounted transformed callback. + +The runner required native chart data readiness for every completed interval +stage. The inspected final screenshot showed four-hour USUSDT candles, moving +averages, volume and a populated orderbook. There were no page errors, observation +stopped, and the temporary browser closed. The report and screenshot are in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-bulk-probe-3YcdXL/`. +This is actual batch execution in an independent anonymous browser. It is not +enablement in the user's logged-in profile or proof of long-running stability, +Tampermonkey delivery or blank-page recovery. + +Three earlier attempts stopped at the anonymous-UI check before any factory +match or mirror dispatch. The registration text matched both a button and a +link; selecting the unique button by its accessible role fixed that diagnostic +premise. Those attempts are not storage failures. + +The independently runnable `run-anonymous-probe.js`, `anonymous-probe-entry.js` +and `anonymous-probe-contract.js` retain this bounded workflow. The runner owns +the fresh Chrome context, rejects navigation away from the fixed URL, checks +the anonymous page, records failure counts and closes its browser. The entry +also checks the exact runtime URL and top frame when capturing each batch; +diagnostic rejection is counted even when the host's original catch resolves. +For an authorized isolated live probe, run: + +```sh +node experiments/binance-chart-storage/run-anonymous-probe.js +``` + +## Tampermonkey acceptance conditions + +Tampermonkey's official [Content Script API documentation](https://www.tampermonkey.net/documentation.php?q=content_script_api) +says the default **Content Script** mode has no real `document-start` support. +On Chrome, ordinary **UserScripts API** mode also retrieves scripts through +background messaging and has no real `document-start` support. **UserScripts API +Dynamic** injects the wrapper and script together and supports `document-start`. +The ordinary UserScripts API statement differs on Firefox; this experiment +targets Chrome. + +The official [`@sandbox` documentation](https://www.tampermonkey.net/documentation.php?q=sandbox) +states that `raw` requests page `MAIN_WORLD` access, but injection can move to +another enabled sandbox when MAIN_WORLD injection is unavailable, for example +because of CSP. Therefore `@run-at document-start` and `@sandbox raw` metadata +alone do not prove timely access to the actual page queue. + +A future Tampermonkey entrypoint must first pass a separate native-only probe on +an authorized test page: verify the effective injection mode, actual MAIN_WORLD +execution, exact source pin before first module execution, real native mirror +completion and clean stop. Unverified timing/world/source must refuse +replacement. Chrome `addInitScript` evidence does not satisfy this extension +gate. The anonymous runs changed no installed script or extension setting. + +`native-preflight-core.js`, `native-preflight-entry.user.js` and +`build-native-preflight.js` now produce the self-contained +`native-preflight.user.js` review artifact. Its verified build input graph contains +only the bootstrap, pinned module, native-only core and entry; no writer or +runtime loader is included. The core keeps bounded in-memory statistics for +30 seconds and forwards the original thunk synchronously, preserving its returned +array and thrown error identities without inspecting stored values or adding +storage calls. Manual stop, pagehide, scope change and the deadline retire +observation. The entry exposes only snapshot and stop. Its metadata alone does +not establish Tampermonkey injection timing or MAIN_WORLD behavior. It remains +unpublished; the authorized local installation is recorded below. + +```sh +node experiments/binance-chart-storage/build-native-preflight.js +``` + +On October 7 Beijing time, the generated artifact itself was then executed with +`addInitScript` in a fresh anonymous Chrome `154.0.8037.98` context. The report +started at `2026-10-06T16:30:27.651Z`: attempts, source matches and factory +executions were each one; factory capture completed, and four dispatches counted +15 entries. There was no diagnostic failure or page error. Manual stop changed +`active` to false, and the temporary browser closed. The inspected screenshot +showed candles, moving averages, volume and the populated orderbook. Evidence is +in `/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-native-artifact-probe-JSBjWC/`. +Here `completed: true` means module capture completed; `dispatches` counts native +thunk invocation, not asynchronous write commits. This validates the built +artifact through Chrome initialization-script injection, not Tampermonkey +delivery. The artifact was still unpublished and uninstalled at that stage. + +The subsequent authorized Tampermonkey test began at +`2026-10-06T18:39:55.733Z` (October 7 Beijing time) in a new USUSDT tab. Installed +source matched all 86,589 artifact bytes; the loaded extension wrapper contained +one identical copy in the top-level Binance default execution world. The +preflight observed one exact factory match and execution, then three native +dispatches counting 885 entries. At page ages 45.1 and 94.2 seconds it was retired +with the same counters, `stopReason: 'deadline'` and no diagnostic failure. +The queue accessor was restored. Native chart data readiness and the inspected +candle crop confirmed a rendered 15-minute chart. + +Only the statistics script was installed. It remains restricted to the exact +USUSDT page and starts a new observation on a fresh matching load. The test and +installer tabs closed; the target-local leverage-endpoint block was disposed +with the test tab. Existing user tabs and extension settings were unchanged. +No bulk writer was enabled or published. Native asynchronous commit completion, +repeatable injection timing, long-running concurrency and blank-page recovery +remain unverified. See the [diagnosis record](../../docs/binance-futures-blank-page-diagnosis.md#tampermonkey-native-only-acceptance-2026-10-07-beijing) +for the sampling limits and `/tmp/binance-tampermonkey-native-preflight-hlIxL9/` +for the report, loaded source and inspected chart crop. + +## Historical global-facade research + +The following sections preserve earlier results for `adapter.js`, including its +different scheduling contract. Their transaction counts, installation lifecycle +and API restrictions do not describe the current scoped mirror candidate. + +### Original question and acceptance + +Can the actual Binance localForage export be observed at first module execution, +and can a narrowly scoped adapter reduce the transaction and unchanged-write +amplification without dropping accepted calls? + +The browser tests use a fresh Chromium context, intercepted `.test` routes, the +captured Binance runtime and localForage module, and real IndexedDB. No Binance +login, existing profile, production database or trading action is used. See the +[fixture provenance](../../test/fixtures/binance-chart-storage/README.md). + +Acceptance covers 290 historical keys and repeated saves, callback delivery, +initialization, ordered mixed operations, null versus missing records, barriers, +two pages, persistence after reload, transaction rollback, limits and cleanup. +The native workflow must first fail the optimized transaction/write assertions. + +### Historical facade restrictions + +- Only newly wrapped instances for `chart_futures` and `chart_delivery`, using + `keyvaluepairs`, are managed. Only `#TV_SYMBOL-` and `myTradingView` keys are + optimized. Other operations pass through ordered native barriers. +- Managed values are JSON values. They are validated and copied when accepted. + Invalid data and exceeded limits are explicit errors, not silent truncation. +- Queued and running operations share count and byte budgets. A batch has its + own operation and byte limits. Read results have a separate maximum size. +- Operations are ordered before waiting for localForage initialization. A + transaction advances only through native request events. Its promises settle + after commit or abort, and equal values are compared inside the transaction. +- Optimized batch failure rolls back the whole batch. Reads also wait for commit. + Optimized batches do not retry connection errors. Native barriers retain + localForage's independent transactions and one-time connection recovery; they + are not covered by optimized-batch rollback or transaction counters. +- Connections are obtained from the initialized instance for every batch. The + experiment never closes the library's shared connections during cleanup. +- Stop closes optimized admission, drains accepted work and then restores the + exact original factory descriptor. Retained facades and factories resume native + behavior. Calls arriving during drain wait behind its finite completion fence; + they cannot overtake accepted writes or extend the optimized drain queue. +- Each export permits one installation per page lifecycle. After stop, reload is + required before another installation can own a new queue. +- While active, `config()` returns a copy; configuration changes, driver changes, database + removal and iteration are explicitly unsupported on managed facades. Managed + non-chart values are also restricted to JSON, even when passed to a barrier. + After stop, the original native API and value types apply. Synchronous config + changes remain prohibited while accepted work drains. + +### Historical facade evidence boundaries + +The original capture entrypoint's first-registration interception and cache +identity are tested only in isolation. The later bootstrap also has the anonymous +startup observation described below. The runtime does not expose its module +cache. Requiring a registered module is +not proof that its factory was already executed. Empty chunk-ID arrays do not +execute a runtime callback in this captured runtime; the host uses a unique +test-owned chunk ID. + +`observeChartStorageModule(queue, { onCapture })` observes module `43917` and +calls `onCapture` synchronously after its factory finishes, before the host's +`require` returns. A Promise-only consumer is too late to intercept instances +created in that same host call stack. Both success and failure restore the +observer's factory and chunk-queue changes. Starting after registration is an +explicit error; it never probes the private cache by executing the factory. +If another owner replaces the queue hook, capture rejects and preserves that +owner's hook while restoring its own module registration. It does not claim +that an external wrapper containing the observer can be removed safely. + +The adapter is not a transparent replacement for the complete localForage API. +Batch atomicity, read completion timing, capacity errors and connection failure +behavior differ. Tests do not establish safety for production installation. +Two-page tests do not establish atomic synchronization across two databases or +fix an application's separate `getItem` then `setItem` lost-update race. + +No real trading page adapter is installed by this experiment. Existing queued +transactions are not drained, cancelled or repaired by running these tests. + +### Historical validation + +Use the repository Node version and run: + +```sh +npm run test:ui -- e2e/binance-orderbook/specs/chart-storage-*.pw.js +node --test test/unit/binance-chart-storage-origin.test.js +npm run lint:tests +``` + +Earlier-stage validation on 2026-10-06: all 68 real Chromium scenarios and all eight +origin/path/frame-guard Node tests passed. The complete test-policy lint, experiment syntax +checks and `git diff --check` passed. Fixture file hashes match their manifest. +The native baseline was rerun in a separate output directory and failed the +bounded-transaction assertion as intended (`4,640 > 32`). + +The quota scenario sets a 64-KiB CDP quota before the first library execution or +database write. A one-MiB incompressible JSON value triggers the real +`QuotaExceededError`; both writes and the read in its batch reject, all callbacks +complete, and prior values remain unchanged. Setting the override after seed +writes did not trigger this failure: the matching +[Chromium bucket implementation](https://github.com/chromium/chromium/blob/151.0.7922.34/content/browser/indexed_db/instance/bucket_context.cc#L463-L523) +can use an already allotted space budget before asking the quota manager again. +An active reported override alone does not prove write-time enforcement. + +### Historical synthetic transaction work + +The controlled workload prepopulates 290 synthetic JSON chart records, then +performs eight rounds of `getItem` followed by an unchanged `setItem`. Setup and +final verification reads are excluded from the counters. All 4,640 Promise +results and all 4,640 distinct callbacks are verified against expected values. + +| Workload | Native transactions | Native puts | Native gets | +| --- | ---: | ---: | ---: | +| Native localForage, eight sequential rounds | 4,640 | 2,320 | 2,320 | +| Prototype, eight sequential rounds | 16 | 0 | 4,640 | +| Prototype, eight simultaneous rounds | 10 | 0 | 4,640 | + +The native baseline fails the same bounded-transaction assertion as intended. +The sequential comparison reduces transaction creation by about 99.66% and +skips all 2,320 unchanged writes. It adds comparison reads inside transactions; +it does not reduce the number of read requests. The concurrent test has at most +2,320 outstanding operations and finishes with zero pending operations/bytes. +These are work counts, not a browser CPU, latency or blank-page recovery +benchmark. Changed values are still written and checked by separate scenarios. + +The browser runtime was Playwright Chromium `151.0.7922.34`, verified by launching +the same bundled browser and reading `browser.version()`. This is not the +user's Chrome `154.0.8037.98` profile. The rendered lab report was inspected; +the later anonymous Chrome observation is described separately below. + +### Historical integration follow-up + +Two additional regressions were reproduced before the integration changes: + +- Installing the original capture before the real runtime produced one factory + execution but zero capture callbacks. Rspack saves the prior `push` as a parent + append callback, invoked after registration and synchronous host execution. +- A real same-origin iframe's `JSON.parse` chart object was rejected by the + adapter's same-realm prototype identity check. The native TradingView iframe + produces its save callback's JSON in precisely that separate realm. + +`bootstrap.js` observes the runtime's first `push` assignment. The pre-runtime +parent callback becomes permanently append-only after takeover or stop. Queue, +factory and cache behavior are tested across both script orders, late entry, +stopping, diagnostic exceptions and host registrations that the observer refuses. +An in-flight append cannot lose its newly created global queue during cleanup. +Diagnostic callback failure rejects observation without poisoning a successful +host module's cached export. Original factory failures keep their host behavior. + +The JSON validator now recognizes native plain-object prototypes across realms +by constructor and constructor.prototype identity. It still rejects class +instances, Date, Map, forged/null prototypes, symbols and accessor properties. +Snapshots retain nested data and own `__proto__` keys without mutating the input. + +`preflight.js` wraps only the actual export's `createInstance`, returning each +original instance. It records up to sixteen chart-instance creation timestamps +and fixed DB/default-store metadata. It never wraps instance methods, calls +ready/config/get/set, records keys/values or starts the adapter. Stop, pagehide +and a 30-second deadline restore the original factory descriptor; an inherited +factory is restored by removing the observer's own property. Retained wrappers +then forward without observation. Tests establish zero added opens, transactions, +gets or puts during passive observation. Dedicated scenarios verify manual stop +and the deadline; they do not separately exercise pagehide, changed factory +ownership, metadata failure or the sixteen-entry recording limit. + +The current user-page module factory was read without requiring it. Its SHA-256 +was `f33d0f78c185ff592f1f0781e21baab63e7db937054866c37afdfda028e9e647`, matching the +fixture's complete module property. The existing page was not refreshed or +modified by the observation. + +An independently launched, anonymous Chrome `154.0.8037.98` with a temporary +profile, no extensions and no login loaded the real USUSDT page. Synchronous +`addInitScript` injection captured the actual module at about 3,236.9 ms and the +first `chart_futures` / `keyvaluepairs` instance at 4,400.5 ms. The observer was +stopped (`active=false`, no failure), and the temporary browser was closed. +This first capture stopped before the business UI mounted and establishes only +the ordering. Its evidence is in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-storage-preflight-b4mWbr/`. + +Further anonymous observations separated module capture from rendered readiness: + +- Waiting for a TradingView iframe on the default Basic chart timed out. The + inspected screenshot showed a working Basic chart and populated orderbook; + it was not a blank-page reproduction. +- The first mode-selection attempt used `TradingView`, but the native label is + `Trading View`. That selector timeout did not click the control or establish + a chart failure. +- After using the correct label, a visible iframe and seven canvases appeared + at about 6.8 seconds. The screenshot still showed the loading animation. That + run's raw `outcome: ui_ready` field is an inadequate machine criterion and + must not be interpreted as rendered chart readiness. Its evidence is in + `/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-storage-preflight-tv-verified-CrQy6W/`. +- The final observation waited for the native `activeChart().dataReady()` and + then two animation frames. The inspected screenshot showed USUSDT candles, + moving averages, volume bars and the orderbook. The captured library defines + `dataReady()` without a callback as a read-only check for a model and nonempty + main-series data; the screenshot supplies the separate rendering evidence. + +In that final Chrome `154.0.8037.98` run, capture occurred at 2,184.1 ms and the +first chart instance at 3,008.7 ms. At 7,429.2 ms the page had a populated +orderbook, one TradingView iframe, twelve canvases and `nativeChartDataReady: +true`. Twenty `chart_futures` instances were observed (only the first sixteen +timestamps retained), with no `chart_delivery` instance and no page errors. +The observer was stopped (`active=false`, `failure=null`) and the temporary +browser was closed. Structured results and the inspected screenshot are in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-storage-preflight-chart-ready-1nRAMB/`. +All anonymous probes used separate temporary profiles and closed their browsers. +They did not enable the adapter. Successful `addInitScript` observation does not +validate Tampermonkey document-start/page-world injection or the affected user's +logged-in profile, and does not establish blank-page recovery. + +The actual source contract is broader than the initial historical-key workload: +module `76535` in [chunk 36648](https://bin.bnbstatic.com/static/chunks/36648.dd8ae7dc.js) +creates instances during component rendering and retains them. Its main save, +layout read/modify/write, drawing saves and cross-database mirror are separate +stages. Module `15426` in +[chunk 28928](https://bin.bnbstatic.com/static/chunks/28928.9aeec892.js) also uses +the same databases for Basic candlestick settings, including `clear()`. Those +non-TradingView calls are affected by the prototype's native-barrier restrictions. +Those retained references motivated the stop lifecycle correction described below; +restoring only the factory cannot update an already mounted component's instance. + +### Historical facade continuation after stop + +The initial stop implementation rejected all retained-facade requests and exposed +the native factory before accepted work drained. Six new regressions reproduced +these failures before the lifecycle change. The adapter now moves from active to +draining to stopped. While draining, its factory still returns managed facades; +their later calls wait for the finite set of accepted operations to complete. +Afterwards it restores the original descriptor and forwards retained calls with +their complete arguments and native receiver. Native callbacks retain their +method-specific signatures. Failed accepted transactions are reported once and +are not retried; they do not prevent a later native operation after stop. + +The nine lifecycle scenarios cover a real write lock, a real blocked database +version upgrade, callback reentry, abort delivery, native Blob/API usage, factory +ownership changes, exact descriptor restoration and repeated stop/install calls. +JSON validation can execute Proxy traps; a separate failing regression showed a +trap could stop the adapter before the request was accepted. Admission now checks +the lifecycle again after validation, so that request rejects without entering a +queue or starting a transaction. An external factory replacement during drain +causes stop to reject explicitly and preserves the replacement. + +Concurrent native `setItem` and `getItem` do not guarantee read-your-write. The +captured driver's set operation prepares the value in an additional Promise +stage before creating its transaction. An independent native baseline therefore +owns the post-stop ordering assertions; reading the prior value before that +concurrent write commits is not data loss. The final persisted value is checked +separately. Stop does not promise to complete while accepted native storage work +itself remains blocked, and its drain statistics do not include subsequent native +calls. These are isolated lifecycle proofs, not a live stop of mounted Binance UI. + +### Historical original-host persistence workflows + +The [host fixture](../../test/fixtures/binance-chart-storage/host-persistence.md) +preserves 273 exact source fragments from seven public bundles, including 203 +original dependency factories. It executes the actual TradingView save/load +functions and mirror callback, and the actual Basic storage functions and old-key +migration. Source byte hashes and UTF-16 ranges were independently checked. +Only the documented storage, widget and enabled-state references are supplied by +the harness; the business algorithms are not reimplemented. + +The same scenarios run through native localForage and the adapter. They check +layout retention, drawing restoration and deletion, cross-database mirror contents, +all six Basic storage categories, original key migration and shared-store clear. +The Basic React subscriptions, debounce and upstream JSON serialization are not +executed; their storage boundary receives synthetic JSON values. Default `{name}` +instances select the IndexedDB driver in these browser tests. + +Four original save/mirror calls with revisions `[1, 1, 2, 2]` produced 64 native +transactions and 36 puts, compared with 28 transactions and 11 puts through the +adapter. Both retained the expected source and destination records. Setup and +final verification are excluded. This small workload includes changed values; +its reduction is different from the unchanged 290-record benchmark above. + +A bounded load scenario prepopulates both databases with the same 290 historical +drawing records, then completes eight rounds of four concurrent original save +and mirror callbacks. Every round changes the current snapshot and drawing. +All 32 callbacks, both databases' 293 keys, all 580 historical values and final +snapshot/layout/drawing/load results are checked. No failures, rejections, +aborted transactions or pending operations remained. + +| Thirty-two original save/mirror chains | Transactions | Puts | Gets | +| --- | ---: | ---: | ---: | +| Native localForage | 18,912 | 9,472 | 9,408 | +| Adapter | 112 | 32 | 18,880 | + +The adapter count comprises 80 optimized transactions and 32 native barriers; +9,440 unchanged writes were skipped. Setup and final verification I/O are +excluded. Controller workload counters use before/after differences; peak gauges +are separately labeled as installation-lifetime measurements. The measured peak +was 1,172 pending operations and 181,767 pending bytes, within configured limits. +Recorded elapsed times are individual lab samples, not a sustained production +latency or CPU benchmark. The increased reads are the cost of comparing values +inside transactions, and this bounded workload does not prove long-term stability. +The four concurrent chains within each round share one revision, with rounds +awaited in sequence; competing different revisions are not covered. The retained +JSON report and extracted measurements are `/tmp/chart-storage-load-retained-20261006.json` +and `/tmp/chart-storage-load-measurements-20261006.json`. + +An additional native/adapter comparison exposes a real ordering difference: +concurrent native sets can create their transactions after a later-invoked Basic +`clear()`, whereas the adapter's barriers enforce issue order and clear those +earlier writes. Both delete records that were already committed. The separate +shared-clear conformance scenario awaits completed saves before clearing; the +concurrent difference has its own explicit assertions. Stronger issue order is +not transparent emulation of native scheduling and remains a production contract +decision, alongside whole-batch rollback and delayed read completion. + +## Current remaining production boundary + +- Validate Tampermonkey delivery under the conditions above. Keep the earlier + zero-dispatch startup, later 6-to-7 native mirror and October 7 actual anonymous + bulk/stop-continuation evidence distinct; none proves extension delivery. + Exact source mismatch must refuse + replacement and preserve native operation; the captured build is not a stable API. +- Validate the real mirror's saved JSON shapes, destination driver/configuration, + limits and the user's logged-in mounted caller lifecycle. The anonymous probe + demonstrated one mounted stop continuation only. Default scope still rejects production + origins; the explicit exact-page scope depends on the harness for profile + isolation. Ordinary native APIs remain outside its control. +- Decide whether destination-wide rollback, read-comparison cost, capacity or + connection failures and changed interleaving with native clear/other tabs are + acceptable. A stopped writer restores the retained native mirror expression; + stopping the bootstrap alone does not alter an already cached module export. +- Measure sustained changed snapshots and concurrent different revisions. Source + reads and layout read/modify/write remain native and non-atomic across calls; + cross-database atomicity and previously queued transaction repair remain absent. +- Validate actual blank-page recovery and long-running stability on the affected + Chrome version. Passing this experiment does not establish a deployed fix. + +Neither historical storage optimization is imported by production entrypoints. +The native-only preflight was installed for the separate acceptance above and +subsequently replaced locally by the production successor. The historical global +facade is not the production candidate. diff --git a/experiments/binance-chart-storage/adapter.js b/experiments/binance-chart-storage/adapter.js new file mode 100644 index 0000000..ebaf516 --- /dev/null +++ b/experiments/binance-chart-storage/adapter.js @@ -0,0 +1,380 @@ +const LAB_ORIGIN = 'https://chart-storage.test'; +const DATABASES = new Set(['chart_futures', 'chart_delivery']); +const DEFAULT_LIMITS = Object.freeze({ + maxBatchOperations: 512, + maxBatchBytes: 4 * 1024 * 1024, + maxPendingOperations: 8192, + maxPendingBytes: 64 * 1024 * 1024, + maxResultBytes: 4 * 1024 * 1024, +}); +const installations = new WeakSet(); +const encoder = new TextEncoder(); +const nativeObjectSource = Function.prototype.toString.call(Object); + +export { snapshotJson, byteLength, equalJson }; + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +/** TradingView creates snapshots in an iframe, whose Object.prototype differs. */ +function isPlainJsonObject(value) { + const prototype = Object.getPrototypeOf(value); + if (prototype === null || Object.getPrototypeOf(prototype) !== null) return false; + const constructor = Object.getOwnPropertyDescriptor(prototype, 'constructor'); + if (!constructor || !Object.hasOwn(constructor, 'value') || typeof constructor.value !== 'function') return false; + if (Function.prototype.toString.call(constructor.value) !== nativeObjectSource) return false; + const constructorPrototype = Object.getOwnPropertyDescriptor(constructor.value, 'prototype'); + return !!constructorPrototype && Object.hasOwn(constructorPrototype, 'value') && constructorPrototype.value === prototype; +} + +/** Reject values whose JSON encoding would silently change their meaning. */ +function snapshotJson(value, ancestors = new Set()) { + if (value === null || typeof value === 'string' || typeof value === 'boolean') return value; + if (typeof value === 'number') { + assert(Number.isFinite(value), 'Chart JSON numbers must be finite'); + return value; + } + assert(typeof value === 'object', 'Chart values must be JSON'); + assert(!ancestors.has(value), 'Chart JSON must not contain cycles'); + const array = Array.isArray(value); + assert(array || isPlainJsonObject(value), 'Chart values must be plain JSON'); + const descriptors = Object.getOwnPropertyDescriptors(value); + assert(Object.getOwnPropertySymbols(value).length === 0, 'Chart JSON must not contain symbol properties'); + const keys = Object.keys(descriptors).filter(key => !(array && key === 'length')); + if (array) assert(keys.length === value.length && keys.every((key, index) => key === String(index)), 'Chart JSON arrays must be dense'); + ancestors.add(value); + const result = array ? [] : {}; + for (const key of keys) { + const descriptor = descriptors[key]; + assert(descriptor.enumerable && Object.hasOwn(descriptor, 'value'), 'Chart JSON requires enumerable data properties'); + Object.defineProperty(result, key, { + value: snapshotJson(descriptor.value, ancestors), enumerable: true, writable: true, configurable: true, + }); + } + ancestors.delete(value); + return result; +} + +function byteLength(value) { + return encoder.encode(JSON.stringify(value)).byteLength; +} + +/** Compare JSON structure without changing the property order of stored snapshots. */ +function equalJson(first, second) { + if (first === second) return true; + if (first === null || second === null || typeof first !== 'object' || typeof second !== 'object') return false; + if (Array.isArray(first) !== Array.isArray(second)) return false; + const keys = Object.keys(first); + if (keys.length !== Object.keys(second).length) return false; + return keys.every(key => Object.hasOwn(second, key) && equalJson(first[key], second[key])); +} + +function isChartKey(key) { + return key.startsWith('#TV_SYMBOL-') || key.startsWith('myTradingView'); +} + +/** Callback exceptions are caller errors; they cannot abandon other accepted calls. */ +function withCallback(promise, callback) { + if (callback !== undefined) { + assert(typeof callback === 'function', 'Storage callback must be a function'); + const invoke = (error, value) => { + try { + callback(error, value); + } catch (callbackError) { + queueMicrotask(() => { throw callbackError; }); + } + }; + promise.then(value => invoke(null, value), error => invoke(error, undefined)); + } + return promise; +} + +/** + * Isolated facade for the captured localForage export. The library owns its + * connections; this experiment owns only accepted operations and their batches. + * Batch atomicity and connection-failure behavior deliberately differ from native. + */ +export function installChartStoragePrototype(localforage, options = {}) { + assert(location.origin === LAB_ORIGIN, 'Chart storage prototype is restricted to the isolated lab origin'); + assert(typeof localforage.createInstance === 'function', 'Expected the actual localForage export'); + assert(!installations.has(localforage), 'Chart storage prototype is already installed'); + const limits = { ...DEFAULT_LIMITS, ...options }; + for (const [name, value] of Object.entries(limits)) { + assert(Object.hasOwn(DEFAULT_LIMITS, name), `Unknown chart storage limit: ${name}`); + assert(Number.isSafeInteger(value) && value > 0, `${name} must be a positive safe integer`); + } + const nativeCreateInstance = localforage.createInstance; + const originalFactoryDescriptor = Object.getOwnPropertyDescriptor(localforage, 'createInstance'); + const coordinators = new Map(); + const waiters = []; + let phase = 'active'; + let stopping; + const stats = { + acceptedOperations: 0, rejectedOperations: 0, failedOperations: 0, + pendingOperations: 0, pendingBytes: 0, peakPendingOperations: 0, peakPendingBytes: 0, + transactions: 0, committedTransactions: 0, abortedTransactions: 0, + committedWrites: 0, skippedWrites: 0, barriers: 0, + }; + + function finish(operation, succeeded, value) { + stats.pendingOperations -= 1; + stats.pendingBytes -= operation.bytes; + if (!succeeded) { + stats.failedOperations += 1; + operation.reject(value); + } else { + operation.resolve(value); + } + } + + function notifyDrained() { + if (stats.pendingOperations === 0) { + for (const resolve of waiters.splice(0)) resolve(); + } + } + + function drain() { + return stats.pendingOperations === 0 ? Promise.resolve() : new Promise(resolve => waiters.push(resolve)); + } + + /** Native request events keep the transaction active; no Promise is awaited inside it. */ + function transact(database, batch) { + return new Promise((resolve, reject) => { + const transaction = database.transaction('keyvaluepairs', batch.some(op => op.method === 'setItem') ? 'readwrite' : 'readonly'); + stats.transactions += 1; + const store = transaction.objectStore('keyvaluepairs'); + const results = []; + let index = 0; + let resultBytes = 0; + let writes = 0; + let skipped = 0; + let failure; + transaction.oncomplete = () => { + stats.committedTransactions += 1; + stats.committedWrites += writes; + stats.skippedWrites += skipped; + resolve(results); + }; + transaction.onabort = () => { + stats.abortedTransactions += 1; + reject(failure || transaction.error || new DOMException('Chart storage transaction aborted', 'AbortError')); + }; + transaction.onerror = event => { failure = event.target.error; }; + + function guarded(action) { + try { + action(); + } catch (error) { + try { + transaction.abort(); + } catch (abortError) { + // Another native listener may already have aborted this transaction. + // Its abort event owns the error; do not replace it with a late request failure. + if (abortError instanceof DOMException && abortError.name === 'InvalidStateError') return; + throw abortError; + } + failure = error; + } + } + + function completeOperation(value) { + const snapshot = snapshotJson(value); + resultBytes += byteLength(snapshot); + assert(resultBytes <= limits.maxResultBytes, 'Chart batch result byte limit exceeded'); + results.push(snapshot); + index += 1; + advance(); + } + + function advance() { + if (index === batch.length) return; + const operation = batch[index]; + const request = store.get(operation.key); + request.onsuccess = () => guarded(() => { + if (operation.method === 'getItem') { + completeOperation(request.result === undefined ? null : request.result); + return; + } + function compareAndWrite(present) { + const current = request.result === undefined ? null : snapshotJson(request.result); + if (present && equalJson(current, operation.value)) { + skipped += 1; + completeOperation(operation.value); + } else { + const put = store.put(operation.value === null ? undefined : operation.value, operation.key); + writes += 1; + put.onsuccess = () => guarded(() => completeOperation(operation.value)); + } + } + if (request.result === undefined) { + const existence = store.getKey(operation.key); + existence.onsuccess = () => guarded(() => compareAndWrite(existence.result !== undefined)); + } else { + compareAndWrite(true); + } + }); + } + guarded(advance); + }); + } + + /** Each store has one queue across instances, including operations awaiting ready. */ + async function pump(coordinator) { + while (coordinator.queue.length > 0) { + const first = coordinator.queue.shift(); + const batch = [first]; + if (!first.barrier) { + let bytes = first.bytes; + while (coordinator.queue.length > 0 && batch.length < limits.maxBatchOperations) { + const next = coordinator.queue[0]; + if (next.barrier || bytes + next.bytes > limits.maxBatchBytes) break; + batch.push(coordinator.queue.shift()); + bytes += next.bytes; + } + } + try { + for (const raw of new Set(batch.map(operation => operation.raw))) { + await raw.ready(); + assert(raw.driver() === localforage.INDEXEDDB, 'Managed chart storage requires the IndexedDB driver'); + assert(raw._dbInfo.name === coordinator.name && raw._dbInfo.storeName === 'keyvaluepairs', 'Managed chart database configuration changed'); + } + let values; + if (first.barrier) { + stats.barriers += 1; + const result = await first.raw[first.method](...first.args); + if (result !== undefined) assert(byteLength(snapshotJson(result)) <= limits.maxResultBytes, 'Chart barrier result byte limit exceeded'); + values = [result]; + } else { + values = await transact(first.raw._dbInfo.db, batch); + } + batch.forEach((operation, index) => finish(operation, true, values[index])); + } catch (error) { + // The batch boundary settles every accepted caller on validation, connection or IDB failure. + batch.forEach(operation => finish(operation, false, error)); + } + } + coordinator.running = false; + notifyDrained(); + } + + function submit(raw, method, args, callback) { + if (callback !== undefined) assert(typeof callback === 'function', 'Storage callback must be a function'); + let promise; + try { + let key; + let value; + if (['getItem', 'setItem', 'removeItem'].includes(method)) { + key = args[0]; + assert(typeof key === 'string' && key.length > 0, 'Chart storage requires a nonempty string key'); + } + if (method === 'setItem') value = snapshotJson(args[1]); + if (method === 'key') assert(Number.isSafeInteger(args[0]) && args[0] >= 0, 'Chart key index must be a nonnegative safe integer'); + const copiedArgs = method === 'setItem' ? [key, value] : [...args]; + const bytes = byteLength(copiedArgs); + const barrier = !((method === 'getItem' || method === 'setItem') && isChartKey(key)); + if (method === 'setItem') assert(byteLength(value) <= limits.maxResultBytes, 'Chart set result byte limit exceeded'); + assert(barrier || bytes <= limits.maxBatchBytes, 'Chart operation exceeds batch byte limit'); + assert(stats.pendingOperations < limits.maxPendingOperations, 'Chart pending operation capacity exceeded'); + assert(stats.pendingBytes + bytes <= limits.maxPendingBytes, 'Chart pending byte capacity exceeded'); + const name = raw.config('name'); + // Value inspection can run Proxy traps that synchronously stop admission. + assert(phase === 'active', 'Chart storage admission stopped during validation'); + let coordinator = coordinators.get(name); + if (!coordinator) { + coordinator = { name, queue: [], running: false }; + coordinators.set(name, coordinator); + } + promise = new Promise((resolve, reject) => { + coordinator.queue.push({ raw, method, args: copiedArgs, key, value, bytes, barrier, resolve, reject }); + }); + stats.acceptedOperations += 1; + stats.pendingOperations += 1; + stats.pendingBytes += bytes; + stats.peakPendingOperations = Math.max(stats.peakPendingOperations, stats.pendingOperations); + stats.peakPendingBytes = Math.max(stats.peakPendingBytes, stats.pendingBytes); + if (!coordinator.running) { + coordinator.running = true; + queueMicrotask(() => { void pump(coordinator); }); + } + } catch (error) { + stats.rejectedOperations += 1; + promise = Promise.reject(error); + } + return withCallback(promise, callback); + } + + /** + * Retained host instances outlive the optimization. The finite stop fence + * prevents new native calls from overtaking accepted batches during teardown. + * Native callbacks have method-specific signatures and receive the original args. + */ + function forwardNative(raw, method, args) { + const invoke = () => Reflect.apply(raw[method], raw, args); + return phase === 'stopped' ? invoke() : stopping.then(invoke); + } + + function callStorage(raw, method, arity, args) { + if (phase !== 'active') return forwardNative(raw, method, args); + return submit(raw, method, args.slice(0, arity), args[arity]); + } + + function createInstance(...args) { + const raw = Reflect.apply(nativeCreateInstance, localforage, args); + if (phase === 'stopped' || !DATABASES.has(raw.config('name')) || raw.config('storeName') !== 'keyvaluepairs') return raw; + const facade = { + getItem: (...args) => callStorage(raw, 'getItem', 1, args), + setItem: (...args) => callStorage(raw, 'setItem', 2, args), + removeItem: (...args) => callStorage(raw, 'removeItem', 1, args), + clear: (...args) => callStorage(raw, 'clear', 0, args), + keys: (...args) => callStorage(raw, 'keys', 0, args), + key: (...args) => callStorage(raw, 'key', 1, args), + length: (...args) => callStorage(raw, 'length', 0, args), + ready(...args) { + return phase === 'active' ? raw.ready(...args) : forwardNative(raw, 'ready', args); + }, + config(option) { + if (phase === 'stopped') return raw.config(option); + assert(option === undefined || typeof option === 'string', 'Managed chart configuration is immutable'); + return structuredClone(raw.config(option)); + }, + driver() { return raw.driver(); }, + createInstance, + }; + for (const name of ['setDriver', 'dropInstance', 'defineDriver', 'iterate', 'getDriver', 'getSerializer']) { + facade[name] = (...args) => phase === 'active' + ? Promise.reject(new Error(`${name} is unsupported by the chart storage experiment`)) + : forwardNative(raw, name, args); + } + return Object.freeze(facade); + } + + localforage.createInstance = createInstance; + const installedFactoryDescriptor = Object.getOwnPropertyDescriptor(localforage, 'createInstance'); + function assertFactoryOwnership() { + const descriptor = Object.getOwnPropertyDescriptor(localforage, 'createInstance'); + assert(descriptor && ['value', 'writable', 'enumerable', 'configurable'].every( + key => descriptor[key] === installedFactoryDescriptor[key], + ), 'Chart factory ownership changed before restoration'); + } + + // A stopped facade cannot join a new installation's queue; require a fresh export lifecycle. + installations.add(localforage); + return Object.freeze({ + getStats: () => ({ ...stats }), + drain, + stop() { + if (phase === 'active') { + assertFactoryOwnership(); + phase = 'draining'; + stopping = drain().then(() => { + assertFactoryOwnership(); + if (originalFactoryDescriptor) Object.defineProperty(localforage, 'createInstance', originalFactoryDescriptor); + else delete localforage.createInstance; + phase = 'stopped'; + }); + } + return stopping; + }, + }); +} diff --git a/experiments/binance-chart-storage/anonymous-probe-contract.js b/experiments/binance-chart-storage/anonymous-probe-contract.js new file mode 100644 index 0000000..35ff2b4 --- /dev/null +++ b/experiments/binance-chart-storage/anonymous-probe-contract.js @@ -0,0 +1,14 @@ +import { snapshotJson } from './adapter.js'; + +/** Host catch may resolve failures, so diagnostic admission must account for its own rejection. */ +export function captureAnonymousMirrorBatch(target, entries, state) { + try { + if (self !== top || location.href !== 'https://www.binance.com/zh-CN/futures/USUSDT') { + throw new Error('Anonymous mirror probe left its fixed top-level URL'); + } + return { target, entries: entries.map(([key, value]) => [key, snapshotJson(value)]) }; + } catch (error) { + state.failed += 1; + throw error; + } +} diff --git a/experiments/binance-chart-storage/anonymous-probe-entry.js b/experiments/binance-chart-storage/anonymous-probe-entry.js new file mode 100644 index 0000000..0be5d6e --- /dev/null +++ b/experiments/binance-chart-storage/anonymous-probe-entry.js @@ -0,0 +1,51 @@ +import { observeChartStorageBootstrap } from './bootstrap.js'; +import { replaceChartMirrorFactory } from './mirror-module.js'; +import { createChartMirrorWriter } from './mirror-writer.js'; +import { snapshotJson, equalJson } from './adapter.js'; +import { captureAnonymousMirrorBatch } from './anonymous-probe-contract.js'; + +/** Only the fresh-profile runner imports this entry; it is not a userscript. */ +if (self === top && location.origin === 'https://www.binance.com' && location.pathname === '/zh-CN/futures/USUSDT') { + const writer = createChartMirrorWriter({ scope: 'isolated-ususdt-probe' }); + const state = { matched: 0, executed: 0, dispatched: 0, completed: 0, failed: 0, nativeCalls: 0, stopped: false, capture: 'waiting' }; + let last; + const observer = observeChartStorageBootstrap({ + replaceMirrorFactory(original) { + const factory = replaceChartMirrorFactory(original, (target, entries, nativeThunk) => { + const batch = captureAnonymousMirrorBatch(target, entries, state); + last = batch; + state.dispatched += 1; + const promises = writer.dispatch(target, entries, () => { + state.nativeCalls += 1; + return nativeThunk(); + }); + batch.completion = Promise.all(promises).then( + () => { state.completed += 1; return true; }, + () => { state.failed += 1; return false; }, + ); + return promises; + }); + state.matched += 1; + return factory; + }, + onCapture() { state.executed += 1; }, + }); + observer.captured.then(() => { state.capture = 'captured'; }, () => { state.capture = 'rejected'; }); + + self.__ANONYMOUS_MIRROR_PROBE__ = Object.freeze({ + snapshot: () => ({ ...state, writer: writer.getStats() }), + async verifyLast() { + if (!last) throw new Error('No mirror batch has executed'); + const batch = last; + if (!await batch.completion) throw new Error('Observed mirror batch failed'); + const values = await Promise.all(batch.entries.map(([key]) => batch.target.getItem(key))); + if (last !== batch) throw new Error('Mirror changed during verification'); + return { entries: batch.entries.length, valuesMatch: values.every((value, index) => equalJson(snapshotJson(value), batch.entries[index][1])) }; + }, + async stop() { + await writer.stop(); + observer.stop(); + state.stopped = true; + }, + }); +} diff --git a/experiments/binance-chart-storage/bootstrap.js b/experiments/binance-chart-storage/bootstrap.js new file mode 100644 index 0000000..3acf063 --- /dev/null +++ b/experiments/binance-chart-storage/bootstrap.js @@ -0,0 +1,162 @@ +const QUEUE_NAME = 'webpackChunkfutures_trade_ui'; + +/** + * Observe first module execution before Rspack installs its chunk dispatcher. + * By default this only captures localForage. An explicit mirror replacer selects + * module 70940 and runs before registration; it never calls a storage method. + * The live scope is one incident page; the general experiment stays in the lab. + */ +export function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } = {}) { + const lab = location.origin === 'https://chart-storage.test'; + const incidentPage = location.origin === 'https://www.binance.com' + && location.pathname === '/zh-CN/futures/USUSDT' && self === top; + if (!lab && !incidentPage) throw new Error('Storage bootstrap requires the lab or the top-level USUSDT incident page'); + if (onCapture !== undefined && typeof onCapture !== 'function') throw new Error('onCapture must be a synchronous function'); + if (replaceMirrorFactory !== undefined && typeof replaceMirrorFactory !== 'function') throw new Error('replaceMirrorFactory must be a synchronous function'); + const moduleId = replaceMirrorFactory === undefined ? '43917' : '70940'; + + const globalDescriptor = Object.getOwnPropertyDescriptor(self, QUEUE_NAME); + if (globalDescriptor && !Object.hasOwn(globalDescriptor, 'value')) throw new Error('Storage bootstrap requires a native queue property'); + const queue = globalDescriptor && globalDescriptor.value !== undefined ? globalDescriptor.value : []; + const nativeAppend = Array.prototype.push; + if (!Array.isArray(queue) || queue.length !== 0 || queue.push !== nativeAppend || Object.hasOwn(queue, 'push')) { + throw new Error('Storage bootstrap must start before runtime on a native empty queue'); + } + if (globalDescriptor && globalDescriptor.value === undefined && !globalDescriptor.configurable) { + throw new Error('Storage bootstrap cannot own the existing global queue property'); + } + const createdQueue = !globalDescriptor || globalDescriptor.value === undefined; + if (createdQueue) Object.defineProperty(self, QUEUE_NAME, { value: queue, writable: true, configurable: true, enumerable: true }); + + let runtimePush; + let inFlightAppends = 0; + let active = true; + let originalFactory; + let observedFactory; + let observedChunk; + let runtimeRequire; + let resolveCapture; + let rejectCapture; + const captured = new Promise((resolve, reject) => { resolveCapture = resolve; rejectCapture = reject; }); + + function restore() { + if (runtimeRequire && runtimeRequire.m[moduleId] === observedFactory) runtimeRequire.m[moduleId] = originalFactory; + if (observedChunk && observedChunk[1][moduleId] === observedFactory) observedChunk[1][moduleId] = originalFactory; + const descriptor = Object.getOwnPropertyDescriptor(queue, 'push'); + if (!descriptor || descriptor.get !== getPush || descriptor.set !== setPush || !descriptor.configurable) { + throw new Error('Storage bootstrap lost queue accessor ownership'); + } + if (runtimePush) { + Object.defineProperty(queue, 'push', { value: runtimePush, writable: true, configurable: true, enumerable: true }); + } else { + delete queue.push; + if (createdQueue && inFlightAppends === 0 && queue.length === 0 && Object.getOwnPropertyDescriptor(self, QUEUE_NAME)?.value === queue) { + if (globalDescriptor) Object.defineProperty(self, QUEUE_NAME, globalDescriptor); + else delete self[QUEUE_NAME]; + } + } + } + + /** Observational failure is isolated from Rspack's persistent module-error cache. */ + function finish(succeeded, result) { + if (!active) return; + active = false; + try { + restore(); + } catch (cleanupError) { + if (succeeded) { + succeeded = false; + result = cleanupError; + } + } + if (succeeded) resolveCapture(result); + else rejectCapture(result); + } + + function prepareChunk(chunk) { + if (!active) return chunk; + try { + if (!Array.isArray(chunk) || !Array.isArray(chunk[0]) || !chunk[1] || typeof chunk[1] !== 'object') { + throw new Error('Storage bootstrap observed an unsupported chunk shape'); + } + if (!Object.hasOwn(chunk[1], moduleId)) return chunk; + if (originalFactory) throw new Error('Storage module registered twice before capture'); + originalFactory = chunk[1][moduleId]; + if (typeof originalFactory !== 'function') throw new Error('Storage module factory must be a function'); + const hostRuntime = chunk[2]; + if (hostRuntime !== undefined && typeof hostRuntime !== 'function') throw new Error('Storage chunk runtime callback must be a function'); + const executionFactory = replaceMirrorFactory === undefined ? originalFactory : replaceMirrorFactory(originalFactory); + if (!active) return chunk; + if (typeof executionFactory !== 'function') throw new Error('Mirror replacement must return a factory synchronously'); + observedFactory = function (module, exports, require) { + runtimeRequire = require; + let returned; + try { + returned = executionFactory.call(this, module, exports, require); + } catch (hostError) { + finish(false, hostError); + throw hostError; + } + if (active) { + try { + const result = onCapture === undefined ? undefined : onCapture(module.exports); + if (result && typeof result.then === 'function') throw new Error('Storage observation callback must complete synchronously'); + finish(true, module.exports); + } catch (observationError) { + finish(false, observationError); + } + } + return returned; + }; + observedChunk = [chunk[0], { ...chunk[1], [moduleId]: observedFactory }, require => { + runtimeRequire = require; + if (hostRuntime !== undefined) return hostRuntime(require); + }]; + return observedChunk; + } catch (observationError) { + // Invalid or duplicate registrations still reach the host's original dispatcher. + finish(false, observationError); + return chunk; + } + } + + /** Rspack retains this parent callback; after takeover it must only append. */ + function preRuntimePush(...chunks) { + if (!active || runtimePush) return nativeAppend.apply(this, chunks); + inFlightAppends += 1; + try { + return nativeAppend.apply(this, chunks.map(prepareChunk)); + } finally { + inFlightAppends -= 1; + } + } + + function runtimeObservingPush(chunk) { + return runtimePush.call(this, prepareChunk(chunk)); + } + + function getPush() { + return runtimePush ? runtimeObservingPush : preRuntimePush; + } + + function setPush(dispatcher) { + if (!active) { + Object.defineProperty(queue, 'push', { value: dispatcher, writable: true, configurable: true, enumerable: true }); + return; + } + if (runtimePush || typeof dispatcher !== 'function') { + finish(false, new Error('Storage bootstrap observed an unexpected push replacement')); + Object.defineProperty(queue, 'push', { value: dispatcher, writable: true, configurable: true, enumerable: true }); + return; + } + runtimePush = dispatcher; + } + + Object.defineProperty(queue, 'push', { get: getPush, set: setPush, enumerable: true, configurable: true }); + return Object.freeze({ + captured, + stop() { + finish(false, new Error('Storage bootstrap stopped before module execution')); + }, + }); +} diff --git a/experiments/binance-chart-storage/build-native-preflight.js b/experiments/binance-chart-storage/build-native-preflight.js new file mode 100644 index 0000000..29fdd23 --- /dev/null +++ b/experiments/binance-chart-storage/build-native-preflight.js @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { build } from 'esbuild'; + +const directory = dirname(fileURLToPath(import.meta.url)); +const root = resolve(directory, '../..'); +const entry = resolve(directory, 'native-preflight-entry.user.js'); +const output = resolve(directory, 'native-preflight.user.js'); + +/** Keep the review artifact self-contained and prove its complete dependency boundary. */ +export async function compileNativePreflight() { + const source = await readFile(entry, 'utf8'); + const metadata = source.match(/^\/\/ ==UserScript==[\s\S]*?\/\/ ==\/UserScript==/)?.[0]; + assert(metadata, 'Native preflight metadata is required'); + const result = await build({ + absWorkingDir: root, + banner: { js: metadata }, + bundle: true, + charset: 'utf8', + format: 'iife', + legalComments: 'none', + minify: false, + platform: 'browser', + sourcemap: false, + target: ['es2020'], + metafile: true, + stdin: { + contents: source.replace(metadata, '').trimStart(), loader: 'js', + resolveDir: directory, sourcefile: entry, + }, + write: false, + }); + const inputs = Object.keys(result.metafile.inputs).sort(); + assert.deepEqual(inputs, [ + 'experiments/binance-chart-storage/bootstrap.js', + 'experiments/binance-chart-storage/mirror-module.js', + 'experiments/binance-chart-storage/native-preflight-core.js', + 'experiments/binance-chart-storage/native-preflight-entry.user.js', + ], 'Native preflight must not include a writer or runtime loader'); + assert.equal(result.outputFiles.length, 1, 'Native preflight must be one static script'); + return { code: result.outputFiles[0].text, inputs }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const { code } = await compileNativePreflight(); + await writeFile(output, code); +} diff --git a/experiments/binance-chart-storage/capture.js b/experiments/binance-chart-storage/capture.js new file mode 100644 index 0000000..df5fdf9 --- /dev/null +++ b/experiments/binance-chart-storage/capture.js @@ -0,0 +1,71 @@ +const MODULE_ID = '43917'; + +/** + * Observe the host's first module execution, never require a possibly unexecuted + * factory ourselves. onCapture runs synchronously before require returns so the + * host cannot create its initial instances before the adapter is installed. + */ +export function observeChartStorageModule(queue = self.webpackChunkfutures_trade_ui, { onCapture } = {}) { + if (location.origin !== 'https://chart-storage.test') throw new Error('Module capture is restricted to the isolated lab origin'); + if (!Array.isArray(queue)) throw new Error('Expected the captured chart runtime chunk queue'); + if (queue.some(chunk => Object.hasOwn(chunk[1], MODULE_ID))) throw new Error('Chart storage module is already registered; late capture is unsupported'); + if (onCapture !== undefined && typeof onCapture !== 'function') throw new Error('onCapture must be a function'); + const originalPush = queue.push; + let originalFactory; + let observedFactory; + let observedChunk; + let runtimeRequire; + let active = true; + let resolveCapture; + let rejectCapture; + const captured = new Promise((resolve, reject) => { resolveCapture = resolve; rejectCapture = reject; }); + + function restore() { + const ownsQueue = queue.push === observingPush; + if (ownsQueue) queue.push = originalPush; + if (runtimeRequire && runtimeRequire.m[MODULE_ID] === observedFactory) runtimeRequire.m[MODULE_ID] = originalFactory; + if (observedChunk) observedChunk[1][MODULE_ID] = originalFactory; + active = false; + if (!ownsQueue) throw new Error('Chart module observer lost chunk queue ownership'); + } + + function observingPush(chunk) { + if (!Object.hasOwn(chunk[1], MODULE_ID)) return originalPush.call(this, chunk); + if (originalFactory) throw new Error('Chart storage module was registered twice'); + originalFactory = chunk[1][MODULE_ID]; + observedFactory = function (module, exports, require) { + runtimeRequire = require; + try { + originalFactory.call(this, module, exports, require); + const actualExport = module.exports; + if (onCapture) { + const result = onCapture(actualExport); + if (result && typeof result.then === 'function') throw new Error('onCapture must install synchronously'); + } + restore(); + resolveCapture(actualExport); + } catch (error) { + rejectCapture(error); + if (active) restore(); + throw error; + } + }; + const hostRuntime = chunk[2]; + observedChunk = [chunk[0], { ...chunk[1], [MODULE_ID]: observedFactory }, require => { + runtimeRequire = require; + if (hostRuntime) return hostRuntime(require); + }]; + return originalPush.call(this, observedChunk); + } + + queue.push = observingPush; + return Object.freeze({ + captured, + stop() { + if (active) { + rejectCapture(new Error('Chart storage module capture stopped before execution')); + restore(); + } + }, + }); +} diff --git a/experiments/binance-chart-storage/generate-mirror-module.js b/experiments/binance-chart-storage/generate-mirror-module.js new file mode 100644 index 0000000..257c378 --- /dev/null +++ b/experiments/binance-chart-storage/generate-mirror-module.js @@ -0,0 +1,107 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile, writeFile } from 'node:fs/promises'; +import { parse } from 'acorn'; + +const input = process.argv[2]; +const moduleOutput = process.argv[3] || new URL('mirror-module.js', import.meta.url); +assert(input, 'Supply the public TradingView.99bc5074.js response file'); +const source = await readFile(input, 'utf8'); +const sha = value => createHash('sha256').update(value).digest('hex'); +assert.equal(sha(source), 'b6a2cddd017fb4517abb4ea458e19905c59f955a3f99efa18beb1e82f3938c1c', 'Public TradingView source changed'); +const ast = parse(source, { ecmaVersion: 'latest', ranges: true }); +function visit(node, action) { + if (!node || typeof node !== 'object') return; + action(node); + for (const value of Object.values(node)) { + if (Array.isArray(value)) value.forEach(child => visit(child, action)); + else if (value?.type) visit(value, action); + } +} +const factories = []; +visit(ast, node => { + if (node.type === 'Property' && node.key.value === 70940 && node.method) factories.push(node); +}); +assert.equal(factories.length, 1, 'Expected one 70940 method factory'); +const factory = factories[0]; +assert.deepEqual([factory.start, factory.end], [6665, 36819]); +const original = source.slice(factory.start, factory.end); +assert.equal(sha(original), '37d249d1ceec4a41b54a959c1ab2c4b52b3061e504cf8cd81a824962af8c6c88'); +const targets = []; +visit(factory, node => { + assert(node.type !== 'Identifier' || node.name !== '__dispatchChartMirror', 'Dispatch service name is shadowed'); + if (node.type === 'AssignmentExpression' && node.operator === '=' && node.left.name === 'Wr' + && node.right.type === 'CallExpression' && node.right.callee.type === 'MemberExpression' + && node.right.callee.object.name === 'fr' && node.right.callee.property.name === 'map' + && source.slice(node.right.start, node.right.end).includes('Sr.setItem(Ot,At)')) targets.push(node.right); +}); +assert.equal(targets.length, 1, 'Expected one original mirror write map'); +const target = targets[0]; +assert.deepEqual([target.start, target.end], [33783, 34025]); +const raw = source.slice(target.start, target.end); +const inserted = `__dispatchChartMirror(Sr,fr,()=>${raw})`; +const first = target.start - factory.start; +const last = target.end - factory.start; +const transformed = original.slice(0, first) + inserted + original.slice(last); +assert.equal(transformed.slice(0, first) + raw + transformed.slice(first + inserted.length), original, 'Reverse replacement must reproduce the whole factory'); +parse(`({${transformed}})`, { ecmaVersion: 'latest' }); + +const moduleSource = `/* Generated from the pinned public 70940 factory; see mirror-scoped-manifest.json. */ +const originalFactorySource = ${JSON.stringify(original)}; + +/** Static factory generation preserves the host lexical body except its mirror write expression. */ +export function createMirrorFactory(__dispatchChartMirror) { + if (typeof __dispatchChartMirror !== 'function') throw new Error('Mirror dispatch must be a function'); + return {${transformed}}[70940]; +} + +/** Registration rejects source drift before any replacement can reach the runtime cache. */ +export function replaceChartMirrorFactory(originalFactory, dispatch) { + if (typeof originalFactory !== 'function' || Function.prototype.toString.call(originalFactory) !== originalFactorySource) { + throw new Error('Mirror factory source does not match the pinned public module'); + } + return createMirrorFactory(dispatch); +} +`; +const fixtureRoot = new URL('../../test/fixtures/binance-chart-storage/', import.meta.url); +const hostManifest = JSON.parse(await readFile(new URL('host-persistence-manifest.json', fixtureRoot), 'utf8')); +const declarations = hostManifest.fragments.filter(fragment => fragment.label.startsWith('declaration:70940:')); +const callbackFragment = hostManifest.fragments.find(fragment => fragment.label === 'callback:70940:z'); +assert(callbackFragment, 'Missing original mirror callback provenance'); +for (const fragment of [...declarations, callbackFragment]) { + assert.equal(sha(source.slice(...fragment.source_utf16_range)), fragment.sha256, fragment.label); +} +const callback = source.slice(...callbackFragment.source_utf16_range); +const callbackStart = target.start - callbackFragment.source_utf16_range[0]; +const callbackEnd = target.end - callbackFragment.source_utf16_range[0]; +const patchedCallback = callback.slice(0, callbackStart) + inserted + callback.slice(callbackEnd); +assert.equal(transformed.slice(callbackFragment.source_utf16_range[0] - factory.start, + callbackFragment.source_utf16_range[1] - factory.start + inserted.length - raw.length), patchedCallback); +const fixtureSource = `/* Exact native and single-expression transformed mirror callbacks. Dependencies come from host-persistence.js. */ +export function createNativeMirrorFactory() { return {${original}}[70940]; } +export function createMirrorCallbacks(e,__dispatchChartMirror) { +${declarations.map(fragment => `var ${source.slice(...fragment.source_utf16_range)};`).join('\n')} +return { +native(a,Cr={current:false},_r={current:true}) { return ${callback}; }, +patched(a,Cr={current:false},_r={current:true}) { return ${patchedCallback}; } +}; +} +`; +parse(moduleSource, { ecmaVersion: 'latest', sourceType: 'module' }); +parse(fixtureSource, { ecmaVersion: 'latest', sourceType: 'module' }); +const manifest = { + description: 'Static 70940 factory with one pinned mirror-write RHS replacement; exact original callback fixture uses original host dependencies.', + source: { file: 'TradingView.99bc5074.js', url: 'https://bin.bnbstatic.com/static/chunks/TradingView.99bc5074.js', sha256: sha(source) }, + offset_contract: 'Zero-based UTF-16 String.slice offsets; exclusive ends; SHA-256 uses UTF-8 bytes.', + original_factory: { source_utf16_range: [factory.start, factory.end], sha256: sha(original) }, + replacement: { source_utf16_range: [target.start, target.end], factory_utf16_range: [first, last], original_sha256: sha(raw), inserted_sha256: sha(inserted) }, + original_callback: callbackFragment, + transformed_callback_sha256: sha(patchedCallback), + outputs: Object.fromEntries([['mirror-module.js', moduleSource], ['mirror-scoped-callback.js', fixtureSource]] + .map(([file, value]) => [file, { sha256: sha(value), bytes: Buffer.byteLength(value) }])), + evidence_limits: 'Whole module source and inverse replacement are statically verified. Offline behavior executes exact callbacks with original dependency factories; full React module initialization is not executed.', +}; +await writeFile(moduleOutput, moduleSource); +await writeFile(new URL('mirror-scoped-callback.js', fixtureRoot), fixtureSource); +await writeFile(new URL('mirror-scoped-manifest.json', fixtureRoot), `${JSON.stringify(manifest, null, 2)}\n`); +console.log(JSON.stringify({ originalFactorySha256: sha(original), replacementRange: [target.start, target.end], outputs: manifest.outputs })); diff --git a/experiments/binance-chart-storage/mirror-module.js b/experiments/binance-chart-storage/mirror-module.js new file mode 100644 index 0000000..48b4c61 --- /dev/null +++ b/experiments/binance-chart-storage/mirror-module.js @@ -0,0 +1,16 @@ +/* Generated from the pinned public 70940 factory; see mirror-scoped-manifest.json. */ +const originalFactorySource = "70940(Z,x,e){\"use strict\";e.r(x),e.d(x,{TradingView:()=>Dt,default:()=>Cn});var o=e(49253),v=e(77135),d=e(75510),i=e(42288),p=e(31085),r=e(41594),M=e(43917),h=e.n(M),P=e(81936),b=e.n(P),V=e(7980),U=e.n(V),D=e(76469),c=e(4260),k=e(53837),B=e(27571),C=e(45250),y=e(87646),A=e.n(y),q=e(80817);const E=(0,C.range)(1,1380),m=(0,C.range)(1,365).map(t=>`${t}D`),R=(0,C.range)(1,52).map(t=>`${t}W`),I=(0,C.range)(1,12).map(t=>`${t}M`),F=[\"1S\",...E,...m,...R,...I],L=({symbol:t,params:n})=>{const a=new URLSearchParams(n);return`${t}@${a.toString()}`},X=t=>{const[n,a]=t.split(\"@\"),s=new URLSearchParams(a).entries(),g=A()(Array.from(s));return{symbol:n,params:g}},ue=()=>{const t=new Map;let n={},a=[],u={onReady:()=>null,resolveSymbol:()=>null,searchSymbols:()=>null,getBars:()=>null,subscribeBars:()=>null,unsubscribeBars:()=>null};return({config:s,onSymbolResolving:g,onSymbolResolved:S,onFetchSymbolsInfo:K,onFetchBars:Y,onSubscribeBars:H,onUnsubscribeBars:j,onFetchServerTimeApi:$,onTransformSymbolInfo:f=({template:T})=>T,onSearchSymbols:l=()=>!0})=>(u={onReady:T=>{setTimeout(async()=>{const _=(await K()).map(O=>({...O,ticker:L({symbol:O.symbol,params:O.params||{}})}));a=_.map(({ticker:O,symbol:G,description:te=\"\",exchange:J=\"\",type:oe=\"\"})=>({symbol:G,ticker:O,description:te,exchange:J,type:oe})),n=(0,C.keyBy)(_,\"ticker\"),T({exchanges:[],symbols_types:[],supported_resolutions:F,supports_marks:!1,supports_timescale_marks:!1,supports_time:!0,...s})})},resolveSymbol:async(T,W,_)=>{const O=n[T];if(!O){_(`cannot resolve symbol - ${T}`);return}const G=f({info:O,template:{description:O.description||\"\",fractional:!1,has_seconds:!0,seconds_multipliers:[\"1\"],has_intraday:!0,intraday_multipliers:[\"1\",\"3\",\"5\",\"15\",\"30\",\"60\",\"120\",\"240\",\"360\",\"480\",\"720\"],has_daily:!0,daily_multipliers:[\"1\"],has_weekly_and_monthly:!0,weekly_multipliers:[\"1\"],monthly_multipliers:[\"1\"],minmov:1,minmove2:0,name:O.symbol,ticker:O.ticker,timezone:\"Etc/UTC\",pricescale:O.tickSize,session:\"24x7\",type:O.type||\"\",exchange:O.exchange||\"\",listed_exchange:O.exchange||\"\",format:\"price\",supported_resolutions:F,visible_plots_set:\"ohlcv\",volume_precision:3}});if(g){const te=X(G.ticker||\"\");if(!await g({symbol:T,info:G,tickerInfo:te})){_(`symbol resolving error - ${T}`);return}}setTimeout(()=>{W(G),S?.(G)})},searchSymbols:(T,W,_,O)=>{const G=new RegExp(T,\"i\"),te=new RegExp(W,\"i\"),J=new RegExp(_,\"i\"),oe=a.filter(Te=>{const{symbol:N,ticker:w,description:re=\"\",exchange:ie=\"\",type:le=\"\"}=Te;return[N,w,re].some(de=>G.test(de||\"\"))&&te.test(ie)&&J.test(le)&&l?.({item:Te})});O(oe)},getBars:async(T,W,_,O,G)=>{const{from:te,to:J,firstDataRequest:oe}=_,Te=(0,q.Cz)(W);try{if(J>=0){const N=X(T.ticker||\"\"),w=await Y({symbolInfo:T,resolution:W,rangeStartDate:te,rangeEndDate:J,tickerInfo:N,binanceInterval:Te,firstDataRequest:oe});O(w,{noData:w.length===0})}else O([],{noData:!0})}catch(N){G(N.toString())}},subscribeBars:(T,W,_,O,G)=>{const te=X(T.ticker||\"\"),J=(0,q.Cz)(W),oe=H({symbolInfo:T,resolution:W,callback:_,onResetCacheNeededCallback:G,tickerInfo:te,binanceInterval:J});t.set(O,oe)},unsubscribeBars:T=>{t?.get(T)?.(),t?.delete(T),j?.(T)},getServerTime:async T=>{try{const W=await $();T(W)}catch(W){console.warn(W),T(Date.now()/1e3)}}},{datafeed:u,observers:t,symbolsInfo:n})},Le=({namespace:t,type:n})=>{const a=(0,r.useMemo)(()=>h().createInstance({name:t}),[t]);return(0,r.useCallback)(()=>{n===\"tv\"&&a.removeItem(\"myTradingView\")},[a,n])};var Ue=e(76535),pr=e(26531),Ye=e(17409),Qe=e(94652),Ae=e(76830),he=e(48651),Fe=e(79344),se=e(51029),Je=e(36307),or=e(25379),yr=e(44745),Ne=e(15727),rr=e(45358),sr=e(82071),Ve=e(85411),ge=e(94889),hr=e(94547),tr=e(80686),Ar=e(61875);function Fr(t){return(0,ge._)(t)||(0,hr._)(t)||(0,Ar._)(t)||(0,tr._)()}var Nr=e(92873),zr=e(35630),Vr=e(12055),Qr=e(79515),Br=e(80737),Jr=e(40545),Zr=e(98107),Kr=e(51846),Xr=\"Chart.Crosshair.PlusButton.DrawHorizontalLine\",wr=function(t){var n=t.namespace,a=t.symbol,u=t.priceType,s=t.crosshairPrice,g=(0,Nr.o)(\"\",\"kline-ui\").getI18n,S=(0,Vr.r)().format,K=k.y$(n),Y=(0,Br.S)({namespace:n}),H=(0,Jr.hw)(),j=(0,Zr.c)(),$=(0,Qr.nH)(),f=(0,se.Ye)(),l=(0,Kr.E)({symbol:a}),T=(0,Fe.f0)(a),W=T.isCM,_=(0,r.useRef)(s);(0,r.useEffect)(function(){_.current=s},[s]);var O=(0,r.useRef)(H);(0,r.useEffect)(function(){O.current=H},[H]);var G=(0,r.useRef)(j);(0,r.useEffect)(function(){G.current=j},[j]);var te=(0,r.useRef)(u);(0,r.useEffect)(function(){te.current=u},[u]);var J=(0,r.useRef)($);(0,r.useEffect)(function(){J.current=$},[$]);var oe=(0,r.useRef)(f);(0,r.useEffect)(function(){oe.current=f},[f]);var Te=(0,r.useRef)(l);(0,r.useEffect)(function(){Te.current=l},[l]);var N=(0,r.useRef)(W);return(0,r.useEffect)(function(){N.current=W},[W]),{items_processor:(function(){var w=(0,o._)(function(re,ie,le){var de,Be,ve,Ce,ce,Ke,Ze,Ge,Xe,Ie,He,ur,Tr,_r,Cr;return(0,i._)(this,function(Er){if(Be=K.getState().tradingViewReference,!Be)return console.warn(\"[context-menu] trading view reference is not found\"),[2,re];if(Be.chartsCount()>1)return[2,re];if(ve=Be.activeChart().symbolExt(),!ve)return[2,re];switch(Ce=ve.name,ce=_.current,Ke=le.menuName===\"CrosshairMenuView\"?\"chart-trading\":\"context-menu\",Ze={price:\"\".concat(ce.value),formattedPrice:ce.formatted,source:Ke,chartType:\"tradingview\"},Ge=(de=oe.current[Ce])===null||de===void 0?void 0:de.baseAsset,Xe=g(\"trade\",{defaultValue:\"Trade\"}),Ie=S(ce.formatted),He=G.current&&te.current===c.SJ.Last&&J.current&&Ge?[ie.createAction({actionId:\"Chart.PlaceOrder.BuyLimit\",label:\"\".concat(Xe,\" \").concat(Ge,\" @ \").concat(Ie,\" \").concat(g(\"limit\",{defaultValue:\"Limit\"})),onExecute:function(){return Te.current.onClickBuyLimit(Ze)}}),ie.createAction({actionId:\"Chart.PlaceOrder.BuyStop\",label:\"\".concat(Xe,\" \").concat(Ge,\" @ \").concat(Ie,\" \").concat(g(\"stop\",{defaultValue:\"Stop\"})),onExecute:function(){return Te.current.onClickBuyStop(Ze)}})]:[],ur=O.current?ie.createAction({actionId:\"Chart.PriceAlert.CreateAtCrosshair\",label:g(\"create-alert-at\",{defaultValue:\"Create Alert at {{price}}\",price:ce.formatted}),onExecute:function(){Y({symbol:Ce,price:ce.formatted,mode:zr.n.CREATE})}}):null,re.forEach(function(we){if(!(!(\"execute\"in we)||!(\"getState\"in we))){var Dr=we.getState();if(Dr.actionId===Xr){var z=we.execute.bind(we);we.execute=function(){(0,Kr.A)({elementId:\"draw_horizontal_line\",isDelivery:N.current,chartType:\"tradingview\"}),z()}}}}),le.menuName){case\"CrosshairMenuView\":return ur?[2,[ur].concat((0,d._)(He),(0,d._)(re.slice()))]:[2,(0,d._)(He).concat((0,d._)(re.slice()))];case\"ChartContextMenu\":return ur?(Tr=Fr(re),_r=Tr[0],Cr=Tr.slice(1),[2,[_r,ur].concat((0,d._)(He),(0,d._)(Cr))]):[2,(0,d._)(He).concat((0,d._)(re.slice()))];default:return[2,re]}return[2]})});return function(re,ie,le){return w.apply(this,arguments)}})()}},qr=function(){var t=(0,r.useState)({value:NaN,formatted:\"\"}),n=t[0],a=t[1],u=(0,r.useCallback)(function(s){var g=s.crosshairData,S=s.activeChart;S&&a({value:g.price,formatted:S.priceFormatter().format(g.price)})},[]);return{onCrosshairMoved:u,crosshairPrice:n}};const Hr=t=>Math.floor(Math.log10(t)),et=t=>{const[n=\"1\"]=t.split(\",\");return Hr(+n)},pe=({symbolInfo:t,minTick:n})=>{const{pricescale:a=2}=t||{};return n===\"default\"||n===void 0?Hr(a):et(n)},br=t=>(n,a)=>{try{return n?{format:u=>{const s=pe({symbolInfo:n,minTick:a});return t.format({symbolInfo:n,minTick:a,value:u,precision:s})}}:null}catch{return null}},Mr=10**12,rt=10**9,tt=10**6,jr=10**3,gr=t=>{const n=t.toString(),[,a=\"\"]=n.split(\".\");return Math.min(a.length,3)},nt=({value:t})=>{const n=Math.abs(t);if(n>=Mr){const a=t/Mr;return{value:a,unit:\"T\",precision:gr(a)}}if(n>=rt){const a=t/rt;return{value:a,unit:\"B\",precision:gr(a)}}if(n>=tt){const a=t/tt;return{value:a,unit:\"M\",precision:gr(a)}}if(n>=jr){const a=t/jr;return{value:a,unit:\"K\",precision:gr(a)}}return{value:t,unit:\"\",precision:gr(t)}},ut=t=>(n,a,u)=>{try{return a&&n.type===\"volume\"?{format:s=>s===void 0?\"\":t.formatVolume({format:n,symbolInfo:a,precision:u,value:s,volumeDetail:nt({value:s})})}:null}catch{return null}};var lt=function(){var t=(0,Vr.r)(),n=t.format,a=(0,r.useMemo)(function(){return br({format:function(s){var g=s.value,S=s.precision;return n(g,{precision:S})}})},[n]),u=(0,r.useMemo)(function(){return ut({formatVolume:function(s){var g=s.value,S=s.volumeDetail;if(g===void 0||!S)return\"\";var K=n(S.value,{precision:S.precision});return\"\".concat(K).concat(S.unit)}})},[n]);return{priceFormatterFactory:a,studyFormatterFactory:u}},dt=e(74069),ir=function(t){var n=(0,dt.K)(t),a=(0,r.useMemo)(function(){return n.map(function(u){var s=u.id,g=u.time,S=u.isBuy,K=u.price,Y=u.tooltip;return{id:s,text:\"\",price:K,quantity:\"\",time:Math.round(g/1e3),isBuy:S,tooltip:Y}})},[n]);return{executionOrders:a}},at=function(t){var n=t.namespace,a=t.symbol,u=(0,Br.j)({namespace:n,symbol:a});return(0,r.useMemo)(function(){return u.map(function(s){var g=s.id,S=s.price,K=s.formattedPrice,Y=s.sideText,H=s.showCloseButton,j=s.isPriceChangeable,$=s.cancelTooltipText,f=s.onClick,l=s.onCancel,T=s.onPriceChanged;return{id:g,price:+S,text:\"\".concat(Y,\" \").concat(K),editable:j,cancellable:H,cancelTooltip:$,type:c.ND.PriceAlert,onCancel:H?l:void 0,onMove:j?function(W){var _=W.price;T({currentPrice:_})}:void 0,onClick:f}})},[u])},vt=e(90664),ot=function(t){var n=t.symbol,a=(0,vt.w)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){var s=u.id,g=u.text,S=u.price;return{id:s,text:g,price:+S,type:c.ND.BreakEvenPrice}})},[a])},cr=e(82508),ft=e(91025),mt=e(36077),Rr=function(t){var n=t.symbol,a=(0,mt.c)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){return(0,ft._)((0,cr._)({},u),{type:c.ND.LiquidationPrice})})},[a])},pt=function(t){var n=t.namespace,a=t.symbol,u=at({namespace:n,symbol:a}),s=ot({symbol:a}),g=Rr({symbol:a});return(0,r.useMemo)(function(){return(0,d._)(u).concat((0,d._)(s),(0,d._)(g))},[u,s,g])},st=e(42902),Ir=e(90170),yt=e(63345),ht=e(48210),Pr=e(51289),xr=e(24326),$r=e(90922),kr=e(34235),it=e(63762),Ft=function(t){var n=(0,Fe.f0)(t),a=n.pricePrecision,u=n.isCM,s=(0,st.yD)(t)||{},g=s.lastPrice,S=g===void 0?\"0\":g,K=(0,kr.Bp)(u),Y=(0,it.HY)(t),H=(0,kr.gv)(u),j=(0,kr.WF)(u);return(0,r.useMemo)(function(){return(0,$r.qF)({orders:(0,d._)(K).concat((0,d._)(H),(0,d._)(j)),symbol:t}).map(function($){return Y({order:$,lastPrice:S,pricePrecision:a})})},[S,K,H,a,t,Y])},gt=e(31542),Tt=function(t){var n=t.type;(0,gt.u4)(\"webClick\",{module:\"chart\",elementId:n})},Nt=function(t){var n=t.symbol,a=t.namespace,u=(0,r.useState)(0),s=u[0],g=u[1],S=(0,r.useCallback)(function(){return g(function(N){return N+1})},[]),K=Ft(n),Y=(0,xr.IT)({namespace:a}),H=(0,Fe.f0)(n),j=H.isDelivery,$=H.contractType,f=(0,st.yD)(n)||{},l=f.lastPrice,T=l===void 0?\"0\":l,W=(0,yt.s)(n),_=(0,it.YZ)({symbol:n,afterConfirmationModalIsShown:S}),O=(0,it.u8)(n),G=(0,it.jn)(n),te=(0,r.useCallback)(function(N){return(0,ht.D)({order:N,isDelivery:j,symbol:j?N.symbol:\"\".concat(N.symbol,\"_\").concat($),module:\"tradingview_kline\"})},[$,j]),J=(0,r.useMemo)(function(){return K.map(function(N){var w=N.order,re=N.price,ie=N.quantity,le=N.quantityText,de=N.isBuy,Be=N.text,ve=N.priceTooltipText,Ce=N.pnlText,ce=(0,Pr.$L)(w),Ke=(0,Pr.OX)(w),Ze=(0,Pr.xL)(w),Ge=ce===\"LIMIT\",Xe=(0,Ir.Y)(w);return{id:String(Ke),text:Be,price:+re,quantity:le,isBuy:de,editable:Xe&&G,tooltip:ve,modifyTooltip:w.closePosition||!Ge?\"\":void 0,onMove:G?function(Ie){var He=Ie.price;te(w),Tt({type:\"TV_vertical\"}),_({order:w,price:+He,quantity:+Ze,lastPrice:T})}:void 0,onModify:w.closePosition||!Ge||!Xe?void 0:function(){Tt({type:\"TV_amountclick\"}),O({order:w,price:re.toString(),quantity:ie,lastPrice:T})},onCancel:function(){Tt({type:\"TV_cancel\"}),W(w)}}})},[G,K,T,te,_,O,W,s]),oe=(0,r.useMemo)(function(){return Y.map(function(N){var w=N.id,re=N.value,ie=N.text,le=N.onChange,de=N.isBuy,Be=N.type;return{id:w,price:+re,quantity:\"\",text:ie,isBuy:de,editable:!0,limitOrderType:c.aA.OrderPreview,onMove:function(ve){var Ce=ve.price;(0,xr.eP)({namespace:a,orderType:Be}),le({id:w,newPrice:Ce})},onMoving:function(ve){var Ce=ve.price;return le({id:w,newPrice:Ce})}}})},[Y,a]),Te=(0,r.useMemo)(function(){return(0,d._)(J).concat((0,d._)(oe))},[J,oe]);return{orderLines:Te}},Vt=e(17855),Bt=function(t,n){var a=(0,r.useState)(t()),u=a[0],s=a[1];return(0,Vt.A)(function(){return s(t())},n),u},Kt=e(58656),Ht=function(t){var n=t.symbol,a=(0,Kt.Q)({symbol:n})||[],u=Bt(function(){return a.map(function(s){var g=s.position,S=s.pnl,K=s.pnlInNumber,Y=s.pnlText,H=s.price,j=s.quantity,$=s.reverseText,f=s.isBuy,l=s.displayReverseOrderModal,T=s.displayClosePositionModal;return{id:g.id||\"\".concat(g.symbol,\"-\").concat(g.positionSide),PNL:K,text:\"\".concat(Y,\" \").concat(S),price:H,quantity:j,tooltip:\"\",reverseTooltip:$,isBuy:f,onReverse:l,onClose:function(){T(),(0,gt.u4)(\"$WebClick\",{item:1,module:\"xxx\",$element_id:\"kline_close_position\"})}}})},[a]);return{positionLines:u}},jt=e(47354),bt=e.n(jt),$t=e(87017),Ut=e(99168),Yt=e(3742),Wt=e(43335),Gt=e(15540),zt=e(20163),Qt=e(47255);const Jt=t=>({endTime:n})=>(n??0)===t;var Zt=e(36488),Ct=e(48018),Xt=e(82911),We=e(69578),wt=e(53208),Rt=e(19862),qt=e(8874),en=e(26679),rn=e(66547),It=(function(){var t=(0,o._)(function(n){var a,u,s,g,S,K,Y,H,j;return(0,i._)(this,function($){switch($.label){case 0:a=n.onFetchKlineData,u=n.startTime,s=n.endTime,g=n.interval,S=n.limit,K=S===void 0?1e3:S,Y=[],H=[],j=s,$.label=1;case 1:return[4,a((0,cr._)({interval:g,limit:K},j===0?{}:{endTime:j}))];case 2:if(H=$.sent(),H.length===0)return[3,4];H.forEach(function(f){var l=(0,v._)(f,8),T=l[0],W=l[1],_=l[2],O=l[3],G=l[4],te=l[5],J=l[7];Y.push({time:+T,open:+W,high:+_,low:+O,close:+G,volume:+te,quantity:+J})}),j=H[0][0]-1,$.label=3;case 3:if(j>u)return[3,1];$.label=4;case 4:return Y.sort(function(f,l){return f.time-l.time}),[2,Y]}})});return function(a){return t.apply(this,arguments)}})(),tn=function(t){var n=t.firstDataRequest,a=t.hasRequested,u=t.rangeEndDate;return n&&!a?0:u*1e3},nn=e(78142),an=e(16238),St=e.n(an),on=e(98087),xt=e.n(on),sn=e(55009),cn=e.n(sn),kt=e(56477);const Et=t=>cn()(t.chartsCount(),n=>t.chart(n)),un=async t=>{try{const{schema:n,data:a}=await t.exportData({includedStudies:[]}),u=xt()(a)?.[n.findIndex(s=>s.type===\"time\")];return u!==void 0&&Number.isFinite(u)?u*1e3:void 0}catch{return}},ln=({getWidget:t,...n})=>{const a=new Map,u=new Map,s=()=>{try{const f=t();return f?St()(Et(f).map(l=>l.symbolExt()?.ticker)):[]}catch{return[]}},g=f=>{const l=s();return!l.length||l.includes(f)},S=f=>{const l=u.get(f);if(l)return l;const T={controller:new AbortController,claimedAsMain:!1};return u.set(f,T),T},K=f=>{const l=u.get(f);l&&(l.controller.abort(),u.delete(f))},Y=()=>{const f=s();f.length&&Array.from(u.keys()).forEach(l=>{const T=u.get(l);if(T){if(f.includes(l)){T.claimedAsMain=!0;return}a.has(l)||T.claimedAsMain&&K(l)}})},H=(f,l)=>{f.closed||l.time{f.held=[];let T=l;try{const _=(await f.options.fetchSince(l)).filter(O=>O.time>=l&&!f.closed);_.forEach(O=>{f.highWater=Math.max(f.highWater,O.time),f.options.onTick(O)}),T=xt()(_)?.time??l}finally{const W=f.held??[];f.held=void 0,W.filter(_=>_.time>=T).forEach(_=>{H(f,_)})}},$=(0,kt.k)({...n,onCheck:Y,repair:async()=>{const f=t();if(f)try{const l=Et(f),T=St()(l.map(_=>{const O=a.get(_.symbolExt()?.ticker??\"\");return O&&{chart:_,feed:O}}));if(!T.length){f.resetCache(),l.forEach(_=>{_.resetData()});return}const W=St()(await Promise.all(T.map(async({chart:_,feed:O})=>{const G=await un(_);return G===void 0?void 0:{feed:O,fromMs:G}})));await Promise.allSettled(W.map(({feed:_,fromMs:O})=>j(_,O)))}catch{}}});return{openFeed:f=>{const l={options:f,held:void 0,highWater:0,closed:!1,detector:void 0};return l.detector=(0,kt.k)({...n,subscribeSignals:f.subscribeSignals,repair:async()=>{l.closed||!l.highWater||g(f.ticker)||await j(l,l.highWater)}}),a.set(f.ticker,l),g(f.ticker)&&$.resubscribeSignals(f.subscribeSignals),{push:T=>{l.held?l.held.push(T):H(l,T)},close:()=>{l.closed=!0,l.detector?.dispose(),a.get(f.ticker)===l&&a.delete(f.ticker)}}},subscribeWake:$.subscribeWake,getSignal:f=>{const l=S(f);return s().includes(f)&&(l.claimedAsMain=!0),l.controller.signal},dispose:()=>{u.forEach(({controller:f})=>{f.abort()}),u.clear(),a.forEach(f=>{f.detector?.dispose()}),a.clear(),$.dispose()}}};var dn=e(71730),vn=e(36620),fn=function(t){var n=t.namespace,a=k.y$(n),u=(0,vn.l)().getSocketSignals,s=(0,r.useRef)();(0,r.useEffect)(function(){return function(){var S;(S=s.current)===null||S===void 0||S.instance.dispose(),s.current=void 0}},[]);var g=(0,r.useCallback)(function(S){var K,Y,H=S;if(((K=s.current)===null||K===void 0?void 0:K.key)===H)return s.current.instance;(Y=s.current)===null||Y===void 0||Y.instance.dispose();var j=nn.wr(S),$=ln({getWidget:function(){return a.getState().tradingViewReference},checkPeriodMs:dn.D(j)});return s.current={key:H,instance:$},$},[a]);return(0,r.useMemo)(function(){return{ensureRecovery:g,getSocketSignals:u}},[g,u])},mn=\"tv_web_error\",pn=[Ye.bt.TRADING,Ye.bt.DELIVERING],Re;(function(t){t.Futures=\"futures\",t.FuturesMark=\"futures-mark\",t.FuturesIndex=\"futures-index\",t.Delivery=\"delivery\",t.DeliveryMark=\"delivery-mark\",t.DeliveryIndex=\"delivery-index\"})(Re||(Re={}));var Mt=function(t){var n=t.symbol,a=t.description,u=t.productType,s=t.pricePrecision,g=t.params,S=t.supportedResolutions,K=S===void 0?F:S;return{symbol:n,description:a,tickSize:Math.pow(10,s),exchange:\"Binance\",supportedResolutions:K,type:u,params:g}},yn=function(t){var n=t.namespace,a=t.useMiniTicker,u=a===void 0?!1:a,s=t.isCM,g=c.y$(n),S=k.y$(n),K=(0,$t.useQueryClient)(),Y=fn({namespace:n}),H=Y.ensureRecovery,j=Y.getSocketSignals,$=(0,r.useRef)(0),f=(0,r.useRef)(!1),l=(0,r.useMemo)(function(){return bt()(function(){return(0,We.HP)({eventId:\"trd_chart_tv_load_timeout\",stepId:\"getBarsStart\",catch:function(Q){var ee=Q.error,ae=Q.track;ae({GET_BAR_START_TYPE:ee})},log:function(Q){var ee=Q.track;ee({GET_BAR_START_TYPE:\"SUCCESS\"})}})})},[]),T=(0,r.useMemo)(function(){return bt()(function(z){return(0,We.HP)({eventId:\"trd_chart_tv_load_timeout\",stepId:\"getBarsEnd\",catch:function(ee){var ae=ee.error,Se=ee.track;Se({GET_BAR_END_TYPE:ae,duration_:(0,We.Pu)(z-$.current)})},log:function(ee){var ae=ee.track;ae({GET_BAR_END_TYPE:\"SUCCESS\",duration_:(0,We.Pu)(z-$.current)})}})})},[]),W=(0,Nr.o)(\"trd-chart\",\"trade-ui\"),_=W.getI18n,O=(0,he.ud)(),G=O.getI18n,te=(0,v._)(g(c.Ir),1),J=te[0],oe=(0,v._)(S(k.A7),2),Te=oe[1],N=(0,se.Ye)(),w=(0,Zt.JD)(s).isContUnit,re=(0,se.y_)(),ie=(0,r.useRef)(N),le=(0,r.useRef)(J),de=(0,r.useRef)(re);(0,r.useEffect)(function(){ie.current=N},[N]),(0,r.useEffect)(function(){le.current=J},[J]),(0,r.useEffect)(function(){de.current=re},[re]);var Be=(0,r.useMemo)(function(){return w?Rt.b:Rt.S},[w]),ve=(0,r.useMemo)(function(){return G(\"usdtFutures-13\",{defaultValue:\"Futures\"})||\"\"},[G]),Ce=(0,r.useMemo)(function(){return G(\"inverseFutures-13\",{defaultValue:\"Delivery\"})||\"\"},[G]),ce=(0,r.useMemo)(function(){return _(\"indexPrice\",{defaultValue:\"Index Price\"})||\"\"},[_]),Ke=(0,r.useMemo)(function(){return _(\"markPrice\",{defaultValue:\"Mark Price\"})||\"\"},[_]),Ze=(0,r.useMemo)(function(){return _(\"lastPrice\",{defaultValue:\"Last Price\"})||\"\"},[_]),Ge=(0,r.useMemo)(function(){return{exchanges:[{name:\"Binance\",value:\"Binance\",desc:\"\"}],symbols_types:[{name:ve,value:Re.Futures},{name:\"\".concat(ve,\" \").concat(Ke),value:Re.FuturesMark},{name:\"\".concat(ve,\" \").concat(ce),value:Re.FuturesIndex},{name:Ce,value:Re.Delivery},{name:\"\".concat(Ce,\" \").concat(Ke),value:Re.DeliveryMark},{name:\"\".concat(Ce,\" \").concat(ce),value:Re.DeliveryIndex}]}},[Ce,ve,ce,Ke]),Xe=(0,r.useCallback)((0,o._)(function(){var z,Q,ee;return(0,i._)(this,function(ae){switch(ae.label){case 0:return ae.trys.push([0,2,,3]),[4,(0,en.c)(K)];case 1:return ae.sent(),[3,3];case 2:return z=ae.sent(),(0,gt.u4)(mn,{type:\"onFetchSymbolsInfo\",message:z.message,business:\"futures\"}),[3,3];case 3:return Q=ie.current,ee=Object.keys(Q).reduce(function(Se,xe){var ne=Q[xe]||Ct.o,ke=ne.symbol,Me=ne.pricePrecision,fe=ne.isDelivery,Pe=ne.contractStatus;if(!pn.includes(Pe))return Se;var _e=(0,Yt.S)(ne,G),je=_e.symbol,Ee=_e.contractTypeI18n,me=_e.deliveryDateFormat,ye=\"\".concat(je,\" \").concat(Ee).concat(me?\" \".concat(me):\"\"),$e=\"\".concat(ye,\" \").concat(Ze),Oe=\"\".concat(ye,\" \").concat(ce),lr=\"\".concat(ye,\" \").concat(Ke);return Se.concat([Mt({symbol:ke,description:$e,productType:fe?Re.Delivery:Re.Futures,pricePrecision:Me,params:{priceType:c.SJ.Last}}),Mt({symbol:ke,description:Oe,productType:fe?Re.DeliveryIndex:Re.FuturesIndex,pricePrecision:Me,params:{priceType:c.SJ.Index}}),Mt({symbol:ke,description:lr,productType:fe?Re.DeliveryMark:Re.FuturesMark,pricePrecision:(0,Xt.X)(Me),params:{priceType:c.SJ.Mark}})])},[]),[2,ee]}})}),[G,ce,Ke,Ze,K]),Ie=(0,r.useCallback)(function(z){var Q=z.ticker,ee=z.symbol,ae=z.interval,Se=z.priceType,xe=z.retryUntil,ne=ie.current[ee]||Ct.o,ke=ne.pair,Me=ne.contractType,fe=ne.contractVal,Pe=ne.isDelivery,_e=Pe?Be(fe):function(Oe){return Oe},je=(0,wt.h)({symbol:ee,pair:ke,contractType:Me,isDelivery:Pe,priceType:Se}),Ee,me,ye=function(Oe){return zt.y(\"tv\",\"\".concat(Pe?\"cm\":\"um\",\"|\").concat(ee,\"|\").concat(Se,\"|\").concat(Oe.interval,\"|\").concat((Ee=Oe.limit)!==null&&Ee!==void 0?Ee:\"\",\"|\").concat((me=Oe.endTime)!==null&&me!==void 0?me:\"\"))};if(xe===void 0)return{transform:_e,onFetchKlineData:je};var $e=H(ae);return{transform:_e,onFetchKlineData:Qt.b(je,{enabled:Jt(xe),key:ye,signal:$e.getSignal(Q),subscribeWake:$e.subscribeWake})}},[H,Be]),He=(0,r.useCallback)((function(){var z=(0,o._)(function(Q){var ee,ae,Se,xe,ne,ke,Me,fe,Pe,_e,je,Ee,me,ye,$e,Oe,lr,Or;return(0,i._)(this,function(dr){switch(dr.label){case 0:return ee=Q.binanceInterval,ae=Q.rangeStartDate,Se=Q.rangeEndDate,xe=Q.tickerInfo,ne=Q.symbolInfo,ke=Q.firstDataRequest,$.current=Date.now(),l(),Me=xe.symbol,fe=Me===void 0?\"\":Me,Pe=xe.params,_e=Pe.priceType,je=_e===void 0?le.current:_e,de.current({symbol:fe,priceType:je})?(T(Date.now()),[2,[]]):(Ee=Math.max(0,ae*1e3),me=tn({firstDataRequest:ke,hasRequested:f.current,rangeEndDate:Se}),f.current=!0,$e=Ie({ticker:(ye=ne.ticker)!==null&&ye!==void 0?ye:\"\",symbol:fe,interval:ee,priceType:je,retryUntil:ke?me:void 0}),Oe=$e.transform,lr=$e.onFetchKlineData,[4,It({onFetchKlineData:lr,startTime:Ee,endTime:me,interval:ee})]);case 1:return Or=dr.sent(),T(Date.now()),[2,Or.map(Oe)]}})});return function(Q){return z.apply(this,arguments)}})(),[Ie,T,l]),ur=(0,r.useCallback)(function(z){var Q=z.binanceInterval,ee=z.callback,ae=z.tickerInfo,Se=z.symbolInfo,xe=ae.symbol,ne=xe===void 0?\"\":xe,ke=ae.params,Me=ke.priceType,fe=Me===void 0?le.current:Me;if(de.current({symbol:ne,priceType:fe}))return function(){return null};var Pe=ie.current[ne]||Ct.o,_e=Pe.pair,je=Pe.contractType,Ee=Pe.isDelivery,me,ye=(me=Se.ticker)!==null&&me!==void 0?me:\"\",$e=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe}).transform,Oe=H(Q),lr=Oe.openFeed({ticker:ye,onTick:ee,subscribeSignals:j(Ee),fetchSince:(function(){var dr=(0,o._)(function(Lr){var Ur,Yr,ct,De;return(0,i._)(this,function(ze){switch(ze.label){case 0:return Ur=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe,retryUntil:0}),Yr=Ur.transform,ct=Ur.onFetchKlineData,[4,It({onFetchKlineData:ct,startTime:Lr,endTime:0,interval:Q})];case 1:return De=ze.sent(),[2,De.map(Yr)]}})});return function(Lr){return dr.apply(this,arguments)}})()}),Or=(0,rn.B)({symbol:ne,pair:_e,contractType:je,interval:Q,priceType:fe,isDelivery:Ee,useMiniTicker:u,callback:function(dr){lr.push($e(dr))}});return function(){lr.close(),Or()}},[Ie,H,j,u]),Tr=(0,r.useCallback)((0,o._)(function(){var z;return(0,i._)(this,function(Q){switch(Q.label){case 0:return[4,K.fetchQuery(Wt.Bz.SERVER_TIME(s),function(){return(0,Gt.l)(s)},{})];case 1:return z=Q.sent(),[2,Math.floor(z/1e3)]}})}),[K,s]),_r=(0,r.useCallback)(function(z){return(0,cr._)({},z.template,(0,qt.sp)(s))},[s]),Cr=(0,r.useMemo)(function(){return ue()},[]),Er=Cr({config:Ge,onFetchSymbolsInfo:Xe,onFetchBars:He,onSubscribeBars:ur,onFetchServerTimeApi:Tr,onTransformSymbolInfo:_r}),we=Er.datafeed,Dr=Er.symbolsInfo;return(0,Ut.A)(function(){return Te()},[w]),{datafeed:we,symbolsInfo:Dr}},Pt=\"trd_chart_tv_load_timeout\",hn=function(){var t=(0,We.PM)({id:\"chartReady\"}),n=(0,We.PM)({id:\"getBarsStart\",timeout:3}),a=(0,We.PM)({id:\"getBarsEnd\",timeout:6}),u=(0,We.PM)({id:\"chartResponsive\",timeout:3});We.HP.init({step:t._(n)._(a)._(u),eventId:Pt}),(0,We.HP)({eventId:Pt,stepId:\"chartReady\",catch:function(g){var S=g.error,K=g.track;K({CHART_READY_TYPE:S,connection:JSON.stringify(navigator.connection)})},log:function(g){var S=g.track;S({CHART_READY_TYPE:\"SUCCESS\",connection:JSON.stringify(navigator.connection)})}})},gn=function(t){(0,We.HP)({eventId:Pt,stepId:\"chartResponsive\",catch:function(a){var u=a.error,s=a.track;s({CHART_RESPONSIVE_TYPE:u,connection:JSON.stringify(navigator.connection)})},log:function(a){var u=a.track;u({CHART_RESPONSIVE_TYPE:\"SUCCESS\",duration_:(0,We.Pu)(Date.now()-t)})}})},Tn=[Qe.ZE,Qe.F3],Dt=function(t){var n=t.symbol,a=t.namespace,u=t.useMiniTicker,s=u===void 0?!1:u,g=c.y$(a),S=k.y$(a),K=(0,v._)(g(c.A8),1),Y=K[0],H=(0,v._)(g(c.Ir),2),j=H[0],$=H[1],f=(0,v._)(S(k.yj),1),l=f[0],T=(0,v._)(g(c.e6),1),W=T[0],_=(0,v._)(g(c._b),1),O=_[0],G=(0,he.ot)(),te=(0,pr.K7)(),J=te.pathname.includes(Ae.Q2),oe=te.pathname.includes(Ae.eq),Te=(0,r.useMemo)(function(){return J?(0,d._)(B.oT).concat([\"hide_left_toolbar_by_default\"]):B.oT},[J]),N=(0,se.Ye)(),w=(0,se.v3)({symbol:n}),re=(0,Fe.es)(n),ie=yn({namespace:a,useMiniTicker:s,isCM:re}),le=ie.datafeed,de=ie.symbolsInfo,Be=(0,Je.DP)(),ve=Be.theme,Ce=ve===\"light\"||ve===\"light_glacier\"?\"light\":\"dark\",ce=(0,yr.Z)(Ce),Ke=(0,r.useRef)(-1),Ze=Ht({symbol:n}).positionLines,Ge=Nt({symbol:n,namespace:a}).orderLines,Xe=ir({symbol:n}).executionOrders,Ie=pt({namespace:a,symbol:n}),He=(0,se.y0)(),ur=(0,r.useRef)(He),Tr=(0,r.useRef)(N),_r=(0,r.useRef)(l),Cr=(0,r.useRef)(J),Er=(0,r.useRef)(oe),we=(0,r.useRef)(de);(0,r.useEffect)(function(){we.current=de},[de]),(0,r.useEffect)(function(){ur.current=He},[He]),(0,r.useEffect)(function(){Tr.current=N},[N]),(0,r.useEffect)(function(){_r.current=l},[l]),(0,r.useEffect)(function(){Cr.current=J},[J]),(0,r.useEffect)(function(){Er.current=oe},[oe]),(0,Ve.a)(n,s),(0,sr.A)({symbol:n,interval:Y,priceType:j});var Dr=(0,r.useCallback)(function(){Ke.current=Date.now()},[]),z=(0,r.useCallback)((0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})()),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er==\"string\"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}),[a]),Q=(0,r.useMemo)(function(){return z},[z]),ee=(0,or.R)(),ae=(0,Ne.fQ)(a),Se=(0,Ne.m3)(a),xe=(0,Ne.S_)(a),ne=(0,Ne.Ds)(a),ke=(0,r.useRef)(n),Me=(0,r.useRef)(Y),fe=(0,r.useCallback)(function(){return ee(\"tradingView\"),xe({panel_index:0,symbol:n,interval:Y}),hn()},[Y,ee,n,xe]),Pe=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be,vr,fr,er=(fr=(vr=(be=qe.symbolExt())===null||be===void 0?void 0:be.name)!==null&&vr!==void 0?vr:qe.symbol())!==null&&fr!==void 0?fr:\"\";ae({panel_index:ze,symbol:er,previous_symbol:ke.current}),ke.current=er},[ae]),_e=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be=qe.resolution();Se({panel_index:ze,interval:be,previous_interval:Me.current}),Me.current=be},[Se]),je=(0,r.useCallback)(function(){gn(Ke.current)},[]),Ee=(0,r.useCallback)(function(){},[]),me=(0,r.useMemo)(function(){return J?Ee:void 0},[Ee,J]),ye=(0,r.useCallback)((function(){var De=(0,o._)(function(ze){var qe,be,vr,fr,er,nr,mr,ar,Sr;return(0,i._)(this,function(Wr){switch(Wr.label){case 0:return qe=ze.chart,be=qe.symbolExt(),be?(vr=b()(we.current,function(Gr){return Gr.ticker===be.ticker||Gr.symbol===be.ticker}),fr=vr||{},er=fr.params,nr=er===void 0?{}:er,mr=nr.priceType,ar=mr===void 0?\"\":mr,ar&&$(ar),Sr=Tr.current[be.name],Sr?Er.current||Cr.current?[2]:[4,z()]:[2]):[2];case 1:return Wr.sent(),G({stateSymbol:Sr.symbol,isDeliveryColumn:Sr.isDelivery}),[2]}})});return function(ze){return De.apply(this,arguments)}})(),[G,z,$]),$e=qr(),Oe=$e.onCrosshairMoved,lr=$e.crosshairPrice,Or=wr({namespace:a,symbol:n,priceType:j,crosshairPrice:lr}),dr=lt(),Lr=(0,r.useMemo)(function(){return L({symbol:n,params:{priceType:j}})},[n,j]),Ur=(0,r.useMemo)(function(){return{symbol:Lr,theme:ce,limitOrders:Ge,positionOrders:Ze,executionOrders:Xe,labelLines:Ie,initialConfig:{tvConfig:{symbol:Lr,library_path:\"/\".concat(Ye.u_,\"/trading-platform-30/\"),theme:ce,datafeed:le,enabled_features:Te,context_menu:Or,custom_formatters:dr},onCrosshairMoved:Oe,onScriptLoaded:Dr,onSave:Q,onChartReadyDone:fe,onFirstTimeDataLoadedDone:je,onInitChart:me,onActiveChartChanged:ye,onSymbolChanged:Pe,onIntervalChanged:_e}}},[Lr,le,Te,Xe,Ie,Or,dr,Oe,ye,fe,je,me,_e,Q,Dr,Pe,Ge,Ze,ce]),Yr=Le({namespace:a,type:\"tv\"}),ct=(0,r.useCallback)(function(){ne({panel_index:0,symbol:n}),Yr()},[Yr,n,ne]);return(0,p.jsxs)(D.t,{name:\"trading_view\",onReset:ct,children:[(0,p.jsx)(Ue.A,{namespace:a,tradingViewProps:Ur}),w&&j===\"index\"&&O===c.Ev.Single&&W===c.tU.TradingView&&(0,p.jsx)(rr.B,{})]})};const Cn=Dt}"; + +/** Static factory generation preserves the host lexical body except its mirror write expression. */ +export function createMirrorFactory(__dispatchChartMirror) { + if (typeof __dispatchChartMirror !== 'function') throw new Error('Mirror dispatch must be a function'); + return {70940(Z,x,e){"use strict";e.r(x),e.d(x,{TradingView:()=>Dt,default:()=>Cn});var o=e(49253),v=e(77135),d=e(75510),i=e(42288),p=e(31085),r=e(41594),M=e(43917),h=e.n(M),P=e(81936),b=e.n(P),V=e(7980),U=e.n(V),D=e(76469),c=e(4260),k=e(53837),B=e(27571),C=e(45250),y=e(87646),A=e.n(y),q=e(80817);const E=(0,C.range)(1,1380),m=(0,C.range)(1,365).map(t=>`${t}D`),R=(0,C.range)(1,52).map(t=>`${t}W`),I=(0,C.range)(1,12).map(t=>`${t}M`),F=["1S",...E,...m,...R,...I],L=({symbol:t,params:n})=>{const a=new URLSearchParams(n);return`${t}@${a.toString()}`},X=t=>{const[n,a]=t.split("@"),s=new URLSearchParams(a).entries(),g=A()(Array.from(s));return{symbol:n,params:g}},ue=()=>{const t=new Map;let n={},a=[],u={onReady:()=>null,resolveSymbol:()=>null,searchSymbols:()=>null,getBars:()=>null,subscribeBars:()=>null,unsubscribeBars:()=>null};return({config:s,onSymbolResolving:g,onSymbolResolved:S,onFetchSymbolsInfo:K,onFetchBars:Y,onSubscribeBars:H,onUnsubscribeBars:j,onFetchServerTimeApi:$,onTransformSymbolInfo:f=({template:T})=>T,onSearchSymbols:l=()=>!0})=>(u={onReady:T=>{setTimeout(async()=>{const _=(await K()).map(O=>({...O,ticker:L({symbol:O.symbol,params:O.params||{}})}));a=_.map(({ticker:O,symbol:G,description:te="",exchange:J="",type:oe=""})=>({symbol:G,ticker:O,description:te,exchange:J,type:oe})),n=(0,C.keyBy)(_,"ticker"),T({exchanges:[],symbols_types:[],supported_resolutions:F,supports_marks:!1,supports_timescale_marks:!1,supports_time:!0,...s})})},resolveSymbol:async(T,W,_)=>{const O=n[T];if(!O){_(`cannot resolve symbol - ${T}`);return}const G=f({info:O,template:{description:O.description||"",fractional:!1,has_seconds:!0,seconds_multipliers:["1"],has_intraday:!0,intraday_multipliers:["1","3","5","15","30","60","120","240","360","480","720"],has_daily:!0,daily_multipliers:["1"],has_weekly_and_monthly:!0,weekly_multipliers:["1"],monthly_multipliers:["1"],minmov:1,minmove2:0,name:O.symbol,ticker:O.ticker,timezone:"Etc/UTC",pricescale:O.tickSize,session:"24x7",type:O.type||"",exchange:O.exchange||"",listed_exchange:O.exchange||"",format:"price",supported_resolutions:F,visible_plots_set:"ohlcv",volume_precision:3}});if(g){const te=X(G.ticker||"");if(!await g({symbol:T,info:G,tickerInfo:te})){_(`symbol resolving error - ${T}`);return}}setTimeout(()=>{W(G),S?.(G)})},searchSymbols:(T,W,_,O)=>{const G=new RegExp(T,"i"),te=new RegExp(W,"i"),J=new RegExp(_,"i"),oe=a.filter(Te=>{const{symbol:N,ticker:w,description:re="",exchange:ie="",type:le=""}=Te;return[N,w,re].some(de=>G.test(de||""))&&te.test(ie)&&J.test(le)&&l?.({item:Te})});O(oe)},getBars:async(T,W,_,O,G)=>{const{from:te,to:J,firstDataRequest:oe}=_,Te=(0,q.Cz)(W);try{if(J>=0){const N=X(T.ticker||""),w=await Y({symbolInfo:T,resolution:W,rangeStartDate:te,rangeEndDate:J,tickerInfo:N,binanceInterval:Te,firstDataRequest:oe});O(w,{noData:w.length===0})}else O([],{noData:!0})}catch(N){G(N.toString())}},subscribeBars:(T,W,_,O,G)=>{const te=X(T.ticker||""),J=(0,q.Cz)(W),oe=H({symbolInfo:T,resolution:W,callback:_,onResetCacheNeededCallback:G,tickerInfo:te,binanceInterval:J});t.set(O,oe)},unsubscribeBars:T=>{t?.get(T)?.(),t?.delete(T),j?.(T)},getServerTime:async T=>{try{const W=await $();T(W)}catch(W){console.warn(W),T(Date.now()/1e3)}}},{datafeed:u,observers:t,symbolsInfo:n})},Le=({namespace:t,type:n})=>{const a=(0,r.useMemo)(()=>h().createInstance({name:t}),[t]);return(0,r.useCallback)(()=>{n==="tv"&&a.removeItem("myTradingView")},[a,n])};var Ue=e(76535),pr=e(26531),Ye=e(17409),Qe=e(94652),Ae=e(76830),he=e(48651),Fe=e(79344),se=e(51029),Je=e(36307),or=e(25379),yr=e(44745),Ne=e(15727),rr=e(45358),sr=e(82071),Ve=e(85411),ge=e(94889),hr=e(94547),tr=e(80686),Ar=e(61875);function Fr(t){return(0,ge._)(t)||(0,hr._)(t)||(0,Ar._)(t)||(0,tr._)()}var Nr=e(92873),zr=e(35630),Vr=e(12055),Qr=e(79515),Br=e(80737),Jr=e(40545),Zr=e(98107),Kr=e(51846),Xr="Chart.Crosshair.PlusButton.DrawHorizontalLine",wr=function(t){var n=t.namespace,a=t.symbol,u=t.priceType,s=t.crosshairPrice,g=(0,Nr.o)("","kline-ui").getI18n,S=(0,Vr.r)().format,K=k.y$(n),Y=(0,Br.S)({namespace:n}),H=(0,Jr.hw)(),j=(0,Zr.c)(),$=(0,Qr.nH)(),f=(0,se.Ye)(),l=(0,Kr.E)({symbol:a}),T=(0,Fe.f0)(a),W=T.isCM,_=(0,r.useRef)(s);(0,r.useEffect)(function(){_.current=s},[s]);var O=(0,r.useRef)(H);(0,r.useEffect)(function(){O.current=H},[H]);var G=(0,r.useRef)(j);(0,r.useEffect)(function(){G.current=j},[j]);var te=(0,r.useRef)(u);(0,r.useEffect)(function(){te.current=u},[u]);var J=(0,r.useRef)($);(0,r.useEffect)(function(){J.current=$},[$]);var oe=(0,r.useRef)(f);(0,r.useEffect)(function(){oe.current=f},[f]);var Te=(0,r.useRef)(l);(0,r.useEffect)(function(){Te.current=l},[l]);var N=(0,r.useRef)(W);return(0,r.useEffect)(function(){N.current=W},[W]),{items_processor:(function(){var w=(0,o._)(function(re,ie,le){var de,Be,ve,Ce,ce,Ke,Ze,Ge,Xe,Ie,He,ur,Tr,_r,Cr;return(0,i._)(this,function(Er){if(Be=K.getState().tradingViewReference,!Be)return console.warn("[context-menu] trading view reference is not found"),[2,re];if(Be.chartsCount()>1)return[2,re];if(ve=Be.activeChart().symbolExt(),!ve)return[2,re];switch(Ce=ve.name,ce=_.current,Ke=le.menuName==="CrosshairMenuView"?"chart-trading":"context-menu",Ze={price:"".concat(ce.value),formattedPrice:ce.formatted,source:Ke,chartType:"tradingview"},Ge=(de=oe.current[Ce])===null||de===void 0?void 0:de.baseAsset,Xe=g("trade",{defaultValue:"Trade"}),Ie=S(ce.formatted),He=G.current&&te.current===c.SJ.Last&&J.current&&Ge?[ie.createAction({actionId:"Chart.PlaceOrder.BuyLimit",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("limit",{defaultValue:"Limit"})),onExecute:function(){return Te.current.onClickBuyLimit(Ze)}}),ie.createAction({actionId:"Chart.PlaceOrder.BuyStop",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("stop",{defaultValue:"Stop"})),onExecute:function(){return Te.current.onClickBuyStop(Ze)}})]:[],ur=O.current?ie.createAction({actionId:"Chart.PriceAlert.CreateAtCrosshair",label:g("create-alert-at",{defaultValue:"Create Alert at {{price}}",price:ce.formatted}),onExecute:function(){Y({symbol:Ce,price:ce.formatted,mode:zr.n.CREATE})}}):null,re.forEach(function(we){if(!(!("execute"in we)||!("getState"in we))){var Dr=we.getState();if(Dr.actionId===Xr){var z=we.execute.bind(we);we.execute=function(){(0,Kr.A)({elementId:"draw_horizontal_line",isDelivery:N.current,chartType:"tradingview"}),z()}}}}),le.menuName){case"CrosshairMenuView":return ur?[2,[ur].concat((0,d._)(He),(0,d._)(re.slice()))]:[2,(0,d._)(He).concat((0,d._)(re.slice()))];case"ChartContextMenu":return ur?(Tr=Fr(re),_r=Tr[0],Cr=Tr.slice(1),[2,[_r,ur].concat((0,d._)(He),(0,d._)(Cr))]):[2,(0,d._)(He).concat((0,d._)(re.slice()))];default:return[2,re]}return[2]})});return function(re,ie,le){return w.apply(this,arguments)}})()}},qr=function(){var t=(0,r.useState)({value:NaN,formatted:""}),n=t[0],a=t[1],u=(0,r.useCallback)(function(s){var g=s.crosshairData,S=s.activeChart;S&&a({value:g.price,formatted:S.priceFormatter().format(g.price)})},[]);return{onCrosshairMoved:u,crosshairPrice:n}};const Hr=t=>Math.floor(Math.log10(t)),et=t=>{const[n="1"]=t.split(",");return Hr(+n)},pe=({symbolInfo:t,minTick:n})=>{const{pricescale:a=2}=t||{};return n==="default"||n===void 0?Hr(a):et(n)},br=t=>(n,a)=>{try{return n?{format:u=>{const s=pe({symbolInfo:n,minTick:a});return t.format({symbolInfo:n,minTick:a,value:u,precision:s})}}:null}catch{return null}},Mr=10**12,rt=10**9,tt=10**6,jr=10**3,gr=t=>{const n=t.toString(),[,a=""]=n.split(".");return Math.min(a.length,3)},nt=({value:t})=>{const n=Math.abs(t);if(n>=Mr){const a=t/Mr;return{value:a,unit:"T",precision:gr(a)}}if(n>=rt){const a=t/rt;return{value:a,unit:"B",precision:gr(a)}}if(n>=tt){const a=t/tt;return{value:a,unit:"M",precision:gr(a)}}if(n>=jr){const a=t/jr;return{value:a,unit:"K",precision:gr(a)}}return{value:t,unit:"",precision:gr(t)}},ut=t=>(n,a,u)=>{try{return a&&n.type==="volume"?{format:s=>s===void 0?"":t.formatVolume({format:n,symbolInfo:a,precision:u,value:s,volumeDetail:nt({value:s})})}:null}catch{return null}};var lt=function(){var t=(0,Vr.r)(),n=t.format,a=(0,r.useMemo)(function(){return br({format:function(s){var g=s.value,S=s.precision;return n(g,{precision:S})}})},[n]),u=(0,r.useMemo)(function(){return ut({formatVolume:function(s){var g=s.value,S=s.volumeDetail;if(g===void 0||!S)return"";var K=n(S.value,{precision:S.precision});return"".concat(K).concat(S.unit)}})},[n]);return{priceFormatterFactory:a,studyFormatterFactory:u}},dt=e(74069),ir=function(t){var n=(0,dt.K)(t),a=(0,r.useMemo)(function(){return n.map(function(u){var s=u.id,g=u.time,S=u.isBuy,K=u.price,Y=u.tooltip;return{id:s,text:"",price:K,quantity:"",time:Math.round(g/1e3),isBuy:S,tooltip:Y}})},[n]);return{executionOrders:a}},at=function(t){var n=t.namespace,a=t.symbol,u=(0,Br.j)({namespace:n,symbol:a});return(0,r.useMemo)(function(){return u.map(function(s){var g=s.id,S=s.price,K=s.formattedPrice,Y=s.sideText,H=s.showCloseButton,j=s.isPriceChangeable,$=s.cancelTooltipText,f=s.onClick,l=s.onCancel,T=s.onPriceChanged;return{id:g,price:+S,text:"".concat(Y," ").concat(K),editable:j,cancellable:H,cancelTooltip:$,type:c.ND.PriceAlert,onCancel:H?l:void 0,onMove:j?function(W){var _=W.price;T({currentPrice:_})}:void 0,onClick:f}})},[u])},vt=e(90664),ot=function(t){var n=t.symbol,a=(0,vt.w)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){var s=u.id,g=u.text,S=u.price;return{id:s,text:g,price:+S,type:c.ND.BreakEvenPrice}})},[a])},cr=e(82508),ft=e(91025),mt=e(36077),Rr=function(t){var n=t.symbol,a=(0,mt.c)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){return(0,ft._)((0,cr._)({},u),{type:c.ND.LiquidationPrice})})},[a])},pt=function(t){var n=t.namespace,a=t.symbol,u=at({namespace:n,symbol:a}),s=ot({symbol:a}),g=Rr({symbol:a});return(0,r.useMemo)(function(){return(0,d._)(u).concat((0,d._)(s),(0,d._)(g))},[u,s,g])},st=e(42902),Ir=e(90170),yt=e(63345),ht=e(48210),Pr=e(51289),xr=e(24326),$r=e(90922),kr=e(34235),it=e(63762),Ft=function(t){var n=(0,Fe.f0)(t),a=n.pricePrecision,u=n.isCM,s=(0,st.yD)(t)||{},g=s.lastPrice,S=g===void 0?"0":g,K=(0,kr.Bp)(u),Y=(0,it.HY)(t),H=(0,kr.gv)(u),j=(0,kr.WF)(u);return(0,r.useMemo)(function(){return(0,$r.qF)({orders:(0,d._)(K).concat((0,d._)(H),(0,d._)(j)),symbol:t}).map(function($){return Y({order:$,lastPrice:S,pricePrecision:a})})},[S,K,H,a,t,Y])},gt=e(31542),Tt=function(t){var n=t.type;(0,gt.u4)("webClick",{module:"chart",elementId:n})},Nt=function(t){var n=t.symbol,a=t.namespace,u=(0,r.useState)(0),s=u[0],g=u[1],S=(0,r.useCallback)(function(){return g(function(N){return N+1})},[]),K=Ft(n),Y=(0,xr.IT)({namespace:a}),H=(0,Fe.f0)(n),j=H.isDelivery,$=H.contractType,f=(0,st.yD)(n)||{},l=f.lastPrice,T=l===void 0?"0":l,W=(0,yt.s)(n),_=(0,it.YZ)({symbol:n,afterConfirmationModalIsShown:S}),O=(0,it.u8)(n),G=(0,it.jn)(n),te=(0,r.useCallback)(function(N){return(0,ht.D)({order:N,isDelivery:j,symbol:j?N.symbol:"".concat(N.symbol,"_").concat($),module:"tradingview_kline"})},[$,j]),J=(0,r.useMemo)(function(){return K.map(function(N){var w=N.order,re=N.price,ie=N.quantity,le=N.quantityText,de=N.isBuy,Be=N.text,ve=N.priceTooltipText,Ce=N.pnlText,ce=(0,Pr.$L)(w),Ke=(0,Pr.OX)(w),Ze=(0,Pr.xL)(w),Ge=ce==="LIMIT",Xe=(0,Ir.Y)(w);return{id:String(Ke),text:Be,price:+re,quantity:le,isBuy:de,editable:Xe&&G,tooltip:ve,modifyTooltip:w.closePosition||!Ge?"":void 0,onMove:G?function(Ie){var He=Ie.price;te(w),Tt({type:"TV_vertical"}),_({order:w,price:+He,quantity:+Ze,lastPrice:T})}:void 0,onModify:w.closePosition||!Ge||!Xe?void 0:function(){Tt({type:"TV_amountclick"}),O({order:w,price:re.toString(),quantity:ie,lastPrice:T})},onCancel:function(){Tt({type:"TV_cancel"}),W(w)}}})},[G,K,T,te,_,O,W,s]),oe=(0,r.useMemo)(function(){return Y.map(function(N){var w=N.id,re=N.value,ie=N.text,le=N.onChange,de=N.isBuy,Be=N.type;return{id:w,price:+re,quantity:"",text:ie,isBuy:de,editable:!0,limitOrderType:c.aA.OrderPreview,onMove:function(ve){var Ce=ve.price;(0,xr.eP)({namespace:a,orderType:Be}),le({id:w,newPrice:Ce})},onMoving:function(ve){var Ce=ve.price;return le({id:w,newPrice:Ce})}}})},[Y,a]),Te=(0,r.useMemo)(function(){return(0,d._)(J).concat((0,d._)(oe))},[J,oe]);return{orderLines:Te}},Vt=e(17855),Bt=function(t,n){var a=(0,r.useState)(t()),u=a[0],s=a[1];return(0,Vt.A)(function(){return s(t())},n),u},Kt=e(58656),Ht=function(t){var n=t.symbol,a=(0,Kt.Q)({symbol:n})||[],u=Bt(function(){return a.map(function(s){var g=s.position,S=s.pnl,K=s.pnlInNumber,Y=s.pnlText,H=s.price,j=s.quantity,$=s.reverseText,f=s.isBuy,l=s.displayReverseOrderModal,T=s.displayClosePositionModal;return{id:g.id||"".concat(g.symbol,"-").concat(g.positionSide),PNL:K,text:"".concat(Y," ").concat(S),price:H,quantity:j,tooltip:"",reverseTooltip:$,isBuy:f,onReverse:l,onClose:function(){T(),(0,gt.u4)("$WebClick",{item:1,module:"xxx",$element_id:"kline_close_position"})}}})},[a]);return{positionLines:u}},jt=e(47354),bt=e.n(jt),$t=e(87017),Ut=e(99168),Yt=e(3742),Wt=e(43335),Gt=e(15540),zt=e(20163),Qt=e(47255);const Jt=t=>({endTime:n})=>(n??0)===t;var Zt=e(36488),Ct=e(48018),Xt=e(82911),We=e(69578),wt=e(53208),Rt=e(19862),qt=e(8874),en=e(26679),rn=e(66547),It=(function(){var t=(0,o._)(function(n){var a,u,s,g,S,K,Y,H,j;return(0,i._)(this,function($){switch($.label){case 0:a=n.onFetchKlineData,u=n.startTime,s=n.endTime,g=n.interval,S=n.limit,K=S===void 0?1e3:S,Y=[],H=[],j=s,$.label=1;case 1:return[4,a((0,cr._)({interval:g,limit:K},j===0?{}:{endTime:j}))];case 2:if(H=$.sent(),H.length===0)return[3,4];H.forEach(function(f){var l=(0,v._)(f,8),T=l[0],W=l[1],_=l[2],O=l[3],G=l[4],te=l[5],J=l[7];Y.push({time:+T,open:+W,high:+_,low:+O,close:+G,volume:+te,quantity:+J})}),j=H[0][0]-1,$.label=3;case 3:if(j>u)return[3,1];$.label=4;case 4:return Y.sort(function(f,l){return f.time-l.time}),[2,Y]}})});return function(a){return t.apply(this,arguments)}})(),tn=function(t){var n=t.firstDataRequest,a=t.hasRequested,u=t.rangeEndDate;return n&&!a?0:u*1e3},nn=e(78142),an=e(16238),St=e.n(an),on=e(98087),xt=e.n(on),sn=e(55009),cn=e.n(sn),kt=e(56477);const Et=t=>cn()(t.chartsCount(),n=>t.chart(n)),un=async t=>{try{const{schema:n,data:a}=await t.exportData({includedStudies:[]}),u=xt()(a)?.[n.findIndex(s=>s.type==="time")];return u!==void 0&&Number.isFinite(u)?u*1e3:void 0}catch{return}},ln=({getWidget:t,...n})=>{const a=new Map,u=new Map,s=()=>{try{const f=t();return f?St()(Et(f).map(l=>l.symbolExt()?.ticker)):[]}catch{return[]}},g=f=>{const l=s();return!l.length||l.includes(f)},S=f=>{const l=u.get(f);if(l)return l;const T={controller:new AbortController,claimedAsMain:!1};return u.set(f,T),T},K=f=>{const l=u.get(f);l&&(l.controller.abort(),u.delete(f))},Y=()=>{const f=s();f.length&&Array.from(u.keys()).forEach(l=>{const T=u.get(l);if(T){if(f.includes(l)){T.claimedAsMain=!0;return}a.has(l)||T.claimedAsMain&&K(l)}})},H=(f,l)=>{f.closed||l.time{f.held=[];let T=l;try{const _=(await f.options.fetchSince(l)).filter(O=>O.time>=l&&!f.closed);_.forEach(O=>{f.highWater=Math.max(f.highWater,O.time),f.options.onTick(O)}),T=xt()(_)?.time??l}finally{const W=f.held??[];f.held=void 0,W.filter(_=>_.time>=T).forEach(_=>{H(f,_)})}},$=(0,kt.k)({...n,onCheck:Y,repair:async()=>{const f=t();if(f)try{const l=Et(f),T=St()(l.map(_=>{const O=a.get(_.symbolExt()?.ticker??"");return O&&{chart:_,feed:O}}));if(!T.length){f.resetCache(),l.forEach(_=>{_.resetData()});return}const W=St()(await Promise.all(T.map(async({chart:_,feed:O})=>{const G=await un(_);return G===void 0?void 0:{feed:O,fromMs:G}})));await Promise.allSettled(W.map(({feed:_,fromMs:O})=>j(_,O)))}catch{}}});return{openFeed:f=>{const l={options:f,held:void 0,highWater:0,closed:!1,detector:void 0};return l.detector=(0,kt.k)({...n,subscribeSignals:f.subscribeSignals,repair:async()=>{l.closed||!l.highWater||g(f.ticker)||await j(l,l.highWater)}}),a.set(f.ticker,l),g(f.ticker)&&$.resubscribeSignals(f.subscribeSignals),{push:T=>{l.held?l.held.push(T):H(l,T)},close:()=>{l.closed=!0,l.detector?.dispose(),a.get(f.ticker)===l&&a.delete(f.ticker)}}},subscribeWake:$.subscribeWake,getSignal:f=>{const l=S(f);return s().includes(f)&&(l.claimedAsMain=!0),l.controller.signal},dispose:()=>{u.forEach(({controller:f})=>{f.abort()}),u.clear(),a.forEach(f=>{f.detector?.dispose()}),a.clear(),$.dispose()}}};var dn=e(71730),vn=e(36620),fn=function(t){var n=t.namespace,a=k.y$(n),u=(0,vn.l)().getSocketSignals,s=(0,r.useRef)();(0,r.useEffect)(function(){return function(){var S;(S=s.current)===null||S===void 0||S.instance.dispose(),s.current=void 0}},[]);var g=(0,r.useCallback)(function(S){var K,Y,H=S;if(((K=s.current)===null||K===void 0?void 0:K.key)===H)return s.current.instance;(Y=s.current)===null||Y===void 0||Y.instance.dispose();var j=nn.wr(S),$=ln({getWidget:function(){return a.getState().tradingViewReference},checkPeriodMs:dn.D(j)});return s.current={key:H,instance:$},$},[a]);return(0,r.useMemo)(function(){return{ensureRecovery:g,getSocketSignals:u}},[g,u])},mn="tv_web_error",pn=[Ye.bt.TRADING,Ye.bt.DELIVERING],Re;(function(t){t.Futures="futures",t.FuturesMark="futures-mark",t.FuturesIndex="futures-index",t.Delivery="delivery",t.DeliveryMark="delivery-mark",t.DeliveryIndex="delivery-index"})(Re||(Re={}));var Mt=function(t){var n=t.symbol,a=t.description,u=t.productType,s=t.pricePrecision,g=t.params,S=t.supportedResolutions,K=S===void 0?F:S;return{symbol:n,description:a,tickSize:Math.pow(10,s),exchange:"Binance",supportedResolutions:K,type:u,params:g}},yn=function(t){var n=t.namespace,a=t.useMiniTicker,u=a===void 0?!1:a,s=t.isCM,g=c.y$(n),S=k.y$(n),K=(0,$t.useQueryClient)(),Y=fn({namespace:n}),H=Y.ensureRecovery,j=Y.getSocketSignals,$=(0,r.useRef)(0),f=(0,r.useRef)(!1),l=(0,r.useMemo)(function(){return bt()(function(){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsStart",catch:function(Q){var ee=Q.error,ae=Q.track;ae({GET_BAR_START_TYPE:ee})},log:function(Q){var ee=Q.track;ee({GET_BAR_START_TYPE:"SUCCESS"})}})})},[]),T=(0,r.useMemo)(function(){return bt()(function(z){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsEnd",catch:function(ee){var ae=ee.error,Se=ee.track;Se({GET_BAR_END_TYPE:ae,duration_:(0,We.Pu)(z-$.current)})},log:function(ee){var ae=ee.track;ae({GET_BAR_END_TYPE:"SUCCESS",duration_:(0,We.Pu)(z-$.current)})}})})},[]),W=(0,Nr.o)("trd-chart","trade-ui"),_=W.getI18n,O=(0,he.ud)(),G=O.getI18n,te=(0,v._)(g(c.Ir),1),J=te[0],oe=(0,v._)(S(k.A7),2),Te=oe[1],N=(0,se.Ye)(),w=(0,Zt.JD)(s).isContUnit,re=(0,se.y_)(),ie=(0,r.useRef)(N),le=(0,r.useRef)(J),de=(0,r.useRef)(re);(0,r.useEffect)(function(){ie.current=N},[N]),(0,r.useEffect)(function(){le.current=J},[J]),(0,r.useEffect)(function(){de.current=re},[re]);var Be=(0,r.useMemo)(function(){return w?Rt.b:Rt.S},[w]),ve=(0,r.useMemo)(function(){return G("usdtFutures-13",{defaultValue:"Futures"})||""},[G]),Ce=(0,r.useMemo)(function(){return G("inverseFutures-13",{defaultValue:"Delivery"})||""},[G]),ce=(0,r.useMemo)(function(){return _("indexPrice",{defaultValue:"Index Price"})||""},[_]),Ke=(0,r.useMemo)(function(){return _("markPrice",{defaultValue:"Mark Price"})||""},[_]),Ze=(0,r.useMemo)(function(){return _("lastPrice",{defaultValue:"Last Price"})||""},[_]),Ge=(0,r.useMemo)(function(){return{exchanges:[{name:"Binance",value:"Binance",desc:""}],symbols_types:[{name:ve,value:Re.Futures},{name:"".concat(ve," ").concat(Ke),value:Re.FuturesMark},{name:"".concat(ve," ").concat(ce),value:Re.FuturesIndex},{name:Ce,value:Re.Delivery},{name:"".concat(Ce," ").concat(Ke),value:Re.DeliveryMark},{name:"".concat(Ce," ").concat(ce),value:Re.DeliveryIndex}]}},[Ce,ve,ce,Ke]),Xe=(0,r.useCallback)((0,o._)(function(){var z,Q,ee;return(0,i._)(this,function(ae){switch(ae.label){case 0:return ae.trys.push([0,2,,3]),[4,(0,en.c)(K)];case 1:return ae.sent(),[3,3];case 2:return z=ae.sent(),(0,gt.u4)(mn,{type:"onFetchSymbolsInfo",message:z.message,business:"futures"}),[3,3];case 3:return Q=ie.current,ee=Object.keys(Q).reduce(function(Se,xe){var ne=Q[xe]||Ct.o,ke=ne.symbol,Me=ne.pricePrecision,fe=ne.isDelivery,Pe=ne.contractStatus;if(!pn.includes(Pe))return Se;var _e=(0,Yt.S)(ne,G),je=_e.symbol,Ee=_e.contractTypeI18n,me=_e.deliveryDateFormat,ye="".concat(je," ").concat(Ee).concat(me?" ".concat(me):""),$e="".concat(ye," ").concat(Ze),Oe="".concat(ye," ").concat(ce),lr="".concat(ye," ").concat(Ke);return Se.concat([Mt({symbol:ke,description:$e,productType:fe?Re.Delivery:Re.Futures,pricePrecision:Me,params:{priceType:c.SJ.Last}}),Mt({symbol:ke,description:Oe,productType:fe?Re.DeliveryIndex:Re.FuturesIndex,pricePrecision:Me,params:{priceType:c.SJ.Index}}),Mt({symbol:ke,description:lr,productType:fe?Re.DeliveryMark:Re.FuturesMark,pricePrecision:(0,Xt.X)(Me),params:{priceType:c.SJ.Mark}})])},[]),[2,ee]}})}),[G,ce,Ke,Ze,K]),Ie=(0,r.useCallback)(function(z){var Q=z.ticker,ee=z.symbol,ae=z.interval,Se=z.priceType,xe=z.retryUntil,ne=ie.current[ee]||Ct.o,ke=ne.pair,Me=ne.contractType,fe=ne.contractVal,Pe=ne.isDelivery,_e=Pe?Be(fe):function(Oe){return Oe},je=(0,wt.h)({symbol:ee,pair:ke,contractType:Me,isDelivery:Pe,priceType:Se}),Ee,me,ye=function(Oe){return zt.y("tv","".concat(Pe?"cm":"um","|").concat(ee,"|").concat(Se,"|").concat(Oe.interval,"|").concat((Ee=Oe.limit)!==null&&Ee!==void 0?Ee:"","|").concat((me=Oe.endTime)!==null&&me!==void 0?me:""))};if(xe===void 0)return{transform:_e,onFetchKlineData:je};var $e=H(ae);return{transform:_e,onFetchKlineData:Qt.b(je,{enabled:Jt(xe),key:ye,signal:$e.getSignal(Q),subscribeWake:$e.subscribeWake})}},[H,Be]),He=(0,r.useCallback)((function(){var z=(0,o._)(function(Q){var ee,ae,Se,xe,ne,ke,Me,fe,Pe,_e,je,Ee,me,ye,$e,Oe,lr,Or;return(0,i._)(this,function(dr){switch(dr.label){case 0:return ee=Q.binanceInterval,ae=Q.rangeStartDate,Se=Q.rangeEndDate,xe=Q.tickerInfo,ne=Q.symbolInfo,ke=Q.firstDataRequest,$.current=Date.now(),l(),Me=xe.symbol,fe=Me===void 0?"":Me,Pe=xe.params,_e=Pe.priceType,je=_e===void 0?le.current:_e,de.current({symbol:fe,priceType:je})?(T(Date.now()),[2,[]]):(Ee=Math.max(0,ae*1e3),me=tn({firstDataRequest:ke,hasRequested:f.current,rangeEndDate:Se}),f.current=!0,$e=Ie({ticker:(ye=ne.ticker)!==null&&ye!==void 0?ye:"",symbol:fe,interval:ee,priceType:je,retryUntil:ke?me:void 0}),Oe=$e.transform,lr=$e.onFetchKlineData,[4,It({onFetchKlineData:lr,startTime:Ee,endTime:me,interval:ee})]);case 1:return Or=dr.sent(),T(Date.now()),[2,Or.map(Oe)]}})});return function(Q){return z.apply(this,arguments)}})(),[Ie,T,l]),ur=(0,r.useCallback)(function(z){var Q=z.binanceInterval,ee=z.callback,ae=z.tickerInfo,Se=z.symbolInfo,xe=ae.symbol,ne=xe===void 0?"":xe,ke=ae.params,Me=ke.priceType,fe=Me===void 0?le.current:Me;if(de.current({symbol:ne,priceType:fe}))return function(){return null};var Pe=ie.current[ne]||Ct.o,_e=Pe.pair,je=Pe.contractType,Ee=Pe.isDelivery,me,ye=(me=Se.ticker)!==null&&me!==void 0?me:"",$e=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe}).transform,Oe=H(Q),lr=Oe.openFeed({ticker:ye,onTick:ee,subscribeSignals:j(Ee),fetchSince:(function(){var dr=(0,o._)(function(Lr){var Ur,Yr,ct,De;return(0,i._)(this,function(ze){switch(ze.label){case 0:return Ur=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe,retryUntil:0}),Yr=Ur.transform,ct=Ur.onFetchKlineData,[4,It({onFetchKlineData:ct,startTime:Lr,endTime:0,interval:Q})];case 1:return De=ze.sent(),[2,De.map(Yr)]}})});return function(Lr){return dr.apply(this,arguments)}})()}),Or=(0,rn.B)({symbol:ne,pair:_e,contractType:je,interval:Q,priceType:fe,isDelivery:Ee,useMiniTicker:u,callback:function(dr){lr.push($e(dr))}});return function(){lr.close(),Or()}},[Ie,H,j,u]),Tr=(0,r.useCallback)((0,o._)(function(){var z;return(0,i._)(this,function(Q){switch(Q.label){case 0:return[4,K.fetchQuery(Wt.Bz.SERVER_TIME(s),function(){return(0,Gt.l)(s)},{})];case 1:return z=Q.sent(),[2,Math.floor(z/1e3)]}})}),[K,s]),_r=(0,r.useCallback)(function(z){return(0,cr._)({},z.template,(0,qt.sp)(s))},[s]),Cr=(0,r.useMemo)(function(){return ue()},[]),Er=Cr({config:Ge,onFetchSymbolsInfo:Xe,onFetchBars:He,onSubscribeBars:ur,onFetchServerTimeApi:Tr,onTransformSymbolInfo:_r}),we=Er.datafeed,Dr=Er.symbolsInfo;return(0,Ut.A)(function(){return Te()},[w]),{datafeed:we,symbolsInfo:Dr}},Pt="trd_chart_tv_load_timeout",hn=function(){var t=(0,We.PM)({id:"chartReady"}),n=(0,We.PM)({id:"getBarsStart",timeout:3}),a=(0,We.PM)({id:"getBarsEnd",timeout:6}),u=(0,We.PM)({id:"chartResponsive",timeout:3});We.HP.init({step:t._(n)._(a)._(u),eventId:Pt}),(0,We.HP)({eventId:Pt,stepId:"chartReady",catch:function(g){var S=g.error,K=g.track;K({CHART_READY_TYPE:S,connection:JSON.stringify(navigator.connection)})},log:function(g){var S=g.track;S({CHART_READY_TYPE:"SUCCESS",connection:JSON.stringify(navigator.connection)})}})},gn=function(t){(0,We.HP)({eventId:Pt,stepId:"chartResponsive",catch:function(a){var u=a.error,s=a.track;s({CHART_RESPONSIVE_TYPE:u,connection:JSON.stringify(navigator.connection)})},log:function(a){var u=a.track;u({CHART_RESPONSIVE_TYPE:"SUCCESS",duration_:(0,We.Pu)(Date.now()-t)})}})},Tn=[Qe.ZE,Qe.F3],Dt=function(t){var n=t.symbol,a=t.namespace,u=t.useMiniTicker,s=u===void 0?!1:u,g=c.y$(a),S=k.y$(a),K=(0,v._)(g(c.A8),1),Y=K[0],H=(0,v._)(g(c.Ir),2),j=H[0],$=H[1],f=(0,v._)(S(k.yj),1),l=f[0],T=(0,v._)(g(c.e6),1),W=T[0],_=(0,v._)(g(c._b),1),O=_[0],G=(0,he.ot)(),te=(0,pr.K7)(),J=te.pathname.includes(Ae.Q2),oe=te.pathname.includes(Ae.eq),Te=(0,r.useMemo)(function(){return J?(0,d._)(B.oT).concat(["hide_left_toolbar_by_default"]):B.oT},[J]),N=(0,se.Ye)(),w=(0,se.v3)({symbol:n}),re=(0,Fe.es)(n),ie=yn({namespace:a,useMiniTicker:s,isCM:re}),le=ie.datafeed,de=ie.symbolsInfo,Be=(0,Je.DP)(),ve=Be.theme,Ce=ve==="light"||ve==="light_glacier"?"light":"dark",ce=(0,yr.Z)(Ce),Ke=(0,r.useRef)(-1),Ze=Ht({symbol:n}).positionLines,Ge=Nt({symbol:n,namespace:a}).orderLines,Xe=ir({symbol:n}).executionOrders,Ie=pt({namespace:a,symbol:n}),He=(0,se.y0)(),ur=(0,r.useRef)(He),Tr=(0,r.useRef)(N),_r=(0,r.useRef)(l),Cr=(0,r.useRef)(J),Er=(0,r.useRef)(oe),we=(0,r.useRef)(de);(0,r.useEffect)(function(){we.current=de},[de]),(0,r.useEffect)(function(){ur.current=He},[He]),(0,r.useEffect)(function(){Tr.current=N},[N]),(0,r.useEffect)(function(){_r.current=l},[l]),(0,r.useEffect)(function(){Cr.current=J},[J]),(0,r.useEffect)(function(){Er.current=oe},[oe]),(0,Ve.a)(n,s),(0,sr.A)({symbol:n,interval:Y,priceType:j});var Dr=(0,r.useCallback)(function(){Ke.current=Date.now()},[]),z=(0,r.useCallback)((0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=__dispatchChartMirror(Sr,fr,()=>fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})())),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er=="string"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}),[a]),Q=(0,r.useMemo)(function(){return z},[z]),ee=(0,or.R)(),ae=(0,Ne.fQ)(a),Se=(0,Ne.m3)(a),xe=(0,Ne.S_)(a),ne=(0,Ne.Ds)(a),ke=(0,r.useRef)(n),Me=(0,r.useRef)(Y),fe=(0,r.useCallback)(function(){return ee("tradingView"),xe({panel_index:0,symbol:n,interval:Y}),hn()},[Y,ee,n,xe]),Pe=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be,vr,fr,er=(fr=(vr=(be=qe.symbolExt())===null||be===void 0?void 0:be.name)!==null&&vr!==void 0?vr:qe.symbol())!==null&&fr!==void 0?fr:"";ae({panel_index:ze,symbol:er,previous_symbol:ke.current}),ke.current=er},[ae]),_e=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be=qe.resolution();Se({panel_index:ze,interval:be,previous_interval:Me.current}),Me.current=be},[Se]),je=(0,r.useCallback)(function(){gn(Ke.current)},[]),Ee=(0,r.useCallback)(function(){},[]),me=(0,r.useMemo)(function(){return J?Ee:void 0},[Ee,J]),ye=(0,r.useCallback)((function(){var De=(0,o._)(function(ze){var qe,be,vr,fr,er,nr,mr,ar,Sr;return(0,i._)(this,function(Wr){switch(Wr.label){case 0:return qe=ze.chart,be=qe.symbolExt(),be?(vr=b()(we.current,function(Gr){return Gr.ticker===be.ticker||Gr.symbol===be.ticker}),fr=vr||{},er=fr.params,nr=er===void 0?{}:er,mr=nr.priceType,ar=mr===void 0?"":mr,ar&&$(ar),Sr=Tr.current[be.name],Sr?Er.current||Cr.current?[2]:[4,z()]:[2]):[2];case 1:return Wr.sent(),G({stateSymbol:Sr.symbol,isDeliveryColumn:Sr.isDelivery}),[2]}})});return function(ze){return De.apply(this,arguments)}})(),[G,z,$]),$e=qr(),Oe=$e.onCrosshairMoved,lr=$e.crosshairPrice,Or=wr({namespace:a,symbol:n,priceType:j,crosshairPrice:lr}),dr=lt(),Lr=(0,r.useMemo)(function(){return L({symbol:n,params:{priceType:j}})},[n,j]),Ur=(0,r.useMemo)(function(){return{symbol:Lr,theme:ce,limitOrders:Ge,positionOrders:Ze,executionOrders:Xe,labelLines:Ie,initialConfig:{tvConfig:{symbol:Lr,library_path:"/".concat(Ye.u_,"/trading-platform-30/"),theme:ce,datafeed:le,enabled_features:Te,context_menu:Or,custom_formatters:dr},onCrosshairMoved:Oe,onScriptLoaded:Dr,onSave:Q,onChartReadyDone:fe,onFirstTimeDataLoadedDone:je,onInitChart:me,onActiveChartChanged:ye,onSymbolChanged:Pe,onIntervalChanged:_e}}},[Lr,le,Te,Xe,Ie,Or,dr,Oe,ye,fe,je,me,_e,Q,Dr,Pe,Ge,Ze,ce]),Yr=Le({namespace:a,type:"tv"}),ct=(0,r.useCallback)(function(){ne({panel_index:0,symbol:n}),Yr()},[Yr,n,ne]);return(0,p.jsxs)(D.t,{name:"trading_view",onReset:ct,children:[(0,p.jsx)(Ue.A,{namespace:a,tradingViewProps:Ur}),w&&j==="index"&&O===c.Ev.Single&&W===c.tU.TradingView&&(0,p.jsx)(rr.B,{})]})};const Cn=Dt}}[70940]; +} + +/** Registration rejects source drift before any replacement can reach the runtime cache. */ +export function replaceChartMirrorFactory(originalFactory, dispatch) { + if (typeof originalFactory !== 'function' || Function.prototype.toString.call(originalFactory) !== originalFactorySource) { + throw new Error('Mirror factory source does not match the pinned public module'); + } + return createMirrorFactory(dispatch); +} diff --git a/experiments/binance-chart-storage/mirror-writer.js b/experiments/binance-chart-storage/mirror-writer.js new file mode 100644 index 0000000..e4aa8cc --- /dev/null +++ b/experiments/binance-chart-storage/mirror-writer.js @@ -0,0 +1,184 @@ +import { snapshotJson, byteLength, equalJson } from './adapter.js'; + +const DEFAULT_LIMITS = Object.freeze({ + maxEntries: 512, + maxBatchBytes: 4 * 1024 * 1024, + maxReadBytes: 4 * 1024 * 1024, + maxPendingBatches: 16, + maxPendingBytes: 64 * 1024 * 1024, +}); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +/** + * One explicit mirror destination batch, never a replacement localForage API. + * The caller owns native instance creation, source reads and the original catch. + * Each accepted batch commits or aborts as a whole; it is never replayed. + * The explicit page probe scope relies on its harness to own an anonymous browser; + * origin/path validation alone cannot establish profile isolation or authorization. + */ +export function createChartMirrorWriter({ limits: suppliedLimits = {}, scope = 'lab' } = {}) { + assert(scope === 'lab' || scope === 'isolated-ususdt-probe', 'Unknown chart mirror scope'); + if (scope === 'lab') { + assert(location.origin === 'https://chart-storage.test', 'Chart mirror writer requires the isolated lab origin'); + } else { + assert(location.origin === 'https://www.binance.com' && location.pathname === '/zh-CN/futures/USUSDT' && self === top, + 'Chart mirror writer requires the top-level USUSDT probe page'); + } + const limits = { ...DEFAULT_LIMITS, ...suppliedLimits }; + for (const [key, value] of Object.entries(limits)) { + assert(Object.hasOwn(DEFAULT_LIMITS, key), `Unknown mirror limit: ${key}`); + assert(Number.isSafeInteger(value) && value > 0, `${key} must be a positive safe integer`); + } + let phase = 'active'; + let stopping; + let resolveDrain; + const stats = { + acceptedBatches: 0, rejectedBatches: 0, failedBatches: 0, + pendingBatches: 0, pendingBytes: 0, peakPendingBatches: 0, peakPendingBytes: 0, + transactions: 0, committedTransactions: 0, abortedTransactions: 0, + committedWrites: 0, skippedWrites: 0, + }; + + function transact(database, entries) { + return new Promise((resolve, reject) => { + const transaction = database.transaction('keyvaluepairs', 'readwrite'); + stats.transactions += 1; + const store = transaction.objectStore('keyvaluepairs'); + let index = 0; + let readBytes = 0; + let writes = 0; + let skipped = 0; + let failure; + transaction.oncomplete = () => { + stats.committedTransactions += 1; + stats.committedWrites += writes; + stats.skippedWrites += skipped; + resolve(); + }; + transaction.onabort = () => { + stats.abortedTransactions += 1; + reject(failure || transaction.error || new DOMException('Chart mirror batch aborted', 'AbortError')); + }; + transaction.onerror = event => { failure = event.target.error; }; + + function guard(action) { + try { + action(); + } catch (error) { + failure = error; + try { + transaction.abort(); + } catch (abortError) { + // A competing native abort listener may already own completion. + if (abortError instanceof DOMException && abortError.name === 'InvalidStateError') return; + throw abortError; + } + } + } + + function advance() { + if (index === entries.length) return; + const [key, value] = entries[index]; + const read = store.get(key); + read.onsuccess = () => guard(() => { + const current = read.result === undefined ? null : snapshotJson(read.result); + readBytes += byteLength(current); + assert(readBytes <= limits.maxReadBytes, 'Mirror stored-value byte limit exceeded'); + function writeIfChanged(present) { + if (present && equalJson(current, value)) { + skipped += 1; + index += 1; + advance(); + } else { + const put = store.put(value === null ? undefined : value, key); + writes += 1; + put.onsuccess = () => guard(() => { index += 1; advance(); }); + } + } + if (read.result === undefined) { + const exists = store.getKey(key); + exists.onsuccess = () => guard(() => writeIfChanged(exists.result !== undefined)); + } else { + writeIfChanged(true); + } + }); + } + guard(advance); + }); + } + + async function write(target, entries) { + await target.ready(); + assert(target.driver() === 'asyncStorage', 'Chart mirror batching requires the native IndexedDB driver'); + const info = target._dbInfo; + assert((info.name === 'chart_futures' || info.name === 'chart_delivery') && info.storeName === 'keyvaluepairs', + 'Unexpected chart mirror database configuration'); + await transact(info.db, entries); + } + + function release(bytes) { + stats.pendingBatches -= 1; + stats.pendingBytes -= bytes; + if (phase === 'draining' && stats.pendingBatches === 0) resolveDrain(); + } + + function dispatch(target, entries, nativeThunk) { + assert(typeof nativeThunk === 'function', 'Chart mirror dispatch requires its original native expression'); + if (phase === 'stopped') return nativeThunk(); + if (phase === 'draining') return [stopping.then(() => Promise.all(nativeThunk()))]; + + let copied; + let bytes; + try { + assert(Array.isArray(entries), 'Chart mirror entries must be an array'); + if (entries.length === 0) return []; + assert(entries.length <= limits.maxEntries, 'Chart mirror entry limit exceeded'); + const keys = new Set(); + copied = entries.map(entry => { + assert(Array.isArray(entry) && entry.length === 2, 'Chart mirror entry must be a key/value pair'); + const [key, value] = entry; + assert(typeof key === 'string' && (key.startsWith('#TV_SYMBOL-') || key.startsWith('myTradingView')), + 'Chart mirror received a non-chart key'); + assert(!keys.has(key), 'Chart mirror keys must be unique'); + keys.add(key); + return [key, snapshotJson(value)]; + }); + bytes = byteLength(copied); + assert(bytes <= limits.maxBatchBytes, 'Chart mirror batch byte limit exceeded'); + assert(stats.pendingBatches < limits.maxPendingBatches, 'Chart mirror pending batch limit exceeded'); + assert(stats.pendingBytes + bytes <= limits.maxPendingBytes, 'Chart mirror pending byte limit exceeded'); + assert(phase === 'active', 'Chart mirror admission stopped during validation'); + } catch (error) { + stats.rejectedBatches += 1; + return [Promise.reject(error)]; + } + stats.acceptedBatches += 1; + stats.pendingBatches += 1; + stats.pendingBytes += bytes; + stats.peakPendingBatches = Math.max(stats.peakPendingBatches, stats.pendingBatches); + stats.peakPendingBytes = Math.max(stats.peakPendingBytes, stats.pendingBytes); + const completion = write(target, copied).then( + () => { release(bytes); }, + error => { stats.failedBatches += 1; release(bytes); throw error; }, + ); + return [completion]; + } + + return Object.freeze({ + dispatch, + getStats: () => ({ ...stats }), + stop() { + if (phase === 'active') { + phase = 'draining'; + const drained = stats.pendingBatches === 0 + ? Promise.resolve() + : new Promise(resolve => { resolveDrain = resolve; }); + stopping = drained.then(() => { phase = 'stopped'; }); + } + return stopping; + }, + }); +} diff --git a/experiments/binance-chart-storage/native-preflight-core.js b/experiments/binance-chart-storage/native-preflight-core.js new file mode 100644 index 0000000..ccc12f4 --- /dev/null +++ b/experiments/binance-chart-storage/native-preflight-core.js @@ -0,0 +1,124 @@ +import { observeChartStorageBootstrap } from './bootstrap.js'; +import { replaceChartMirrorFactory } from './mirror-module.js'; + +const INCIDENT_URL = 'https://www.binance.com/zh-CN/futures/USUSDT'; +const OBSERVATION_MS = 30_000; + +function isIncidentPage() { + return location.href === INCIDENT_URL && self === top; +} + +/** + * Observe the pinned mirror boundary without inspecting values or adding storage + * work. Native thunk evaluation stays synchronous, including its array and error + * identities. The returned dispatcher is for module composition; the userscript + * publishes only snapshot and stop. + */ +export function startNativeMirrorPreflight() { + const state = { + active: true, outcome: 'waiting', stopReason: null, failure: null, + attempts: 0, matches: 0, executions: 0, completed: false, + dispatches: 0, entryCount: 0, + }; + let observer; + let timer; + + function snapshot() { + return { ...state, failure: state.failure === null ? null : { ...state.failure } }; + } + + function retire(reason) { + if (!state.active) return snapshot(); + state.active = false; + state.stopReason = reason; + clearTimeout(timer); + self.removeEventListener('pagehide', onPageHide); + if (observer) observer.stop(); + return snapshot(); + } + + function fail(code) { + state.failure = { name: 'PreflightError', code }; + state.outcome = 'failed'; + retire('failure'); + } + + function onPageHide() { + retire('pagehide'); + } + + function dispatch(_target, entries, nativeThunk) { + if (state.active) { + if (!isIncidentPage()) retire('scope_changed'); + else { + try { + const count = entries.length; + if (state.active) { + state.dispatches += 1; + state.entryCount += count; + } + } catch { + // Diagnostic metadata failure retires observation, never the native operation. + fail('entry_count_unavailable'); + } + } + } + return nativeThunk(); + } + + const session = Object.freeze({ snapshot, stop: () => retire('manual'), dispatch }); + if (!isIncidentPage()) { + fail('unsupported_page'); + return session; + } + + try { + observer = observeChartStorageBootstrap({ + replaceMirrorFactory(original) { + if (!isIncidentPage()) { + retire('scope_changed'); + return original; + } + state.attempts += 1; + let factory; + try { + factory = replaceChartMirrorFactory(original, dispatch); + } catch { + fail('source_mismatch'); + return original; + } + state.matches += 1; + return function (...args) { + if (state.active) { + if (!isIncidentPage()) retire('scope_changed'); + else state.executions += 1; + } + try { + return Reflect.apply(factory, this, args); + } catch (error) { + if (state.active) fail('module_execution_failed'); + throw error; + } + }; + }, + onCapture() { + if (!state.active) return; + if (!isIncidentPage()) retire('scope_changed'); + else { + state.completed = true; + state.outcome = 'captured'; + } + }, + }); + } catch { + fail('bootstrap_rejected'); + return session; + } + + observer.captured.catch(() => { + if (state.active) fail('observation_failed'); + }); + self.addEventListener('pagehide', onPageHide, { once: true }); + timer = setTimeout(() => retire('deadline'), OBSERVATION_MS); + return session; +} diff --git a/experiments/binance-chart-storage/native-preflight-entry.user.js b/experiments/binance-chart-storage/native-preflight-entry.user.js new file mode 100644 index 0000000..b2c11a1 --- /dev/null +++ b/experiments/binance-chart-storage/native-preflight-entry.user.js @@ -0,0 +1,21 @@ +// ==UserScript== +// @name Binance Chart Mirror Native Preflight (Experiment) +// @namespace binance.chart.mirror.native-preflight +// @version 0.0.1 +// @description Observe pinned mirror startup and native dispatch counts without custom storage writes. +// @match https://www.binance.com/zh-CN/futures/USUSDT +// @run-at document-start +// @sandbox raw +// @grant none +// @noframes +// ==/UserScript== + +import { startNativeMirrorPreflight } from './native-preflight-core.js'; + +(function installNativeMirrorPreflight() { + if (Object.hasOwn(self, '__BINANCE_MIRROR_PREFLIGHT__')) return; + const { snapshot, stop } = startNativeMirrorPreflight(); + Object.defineProperty(self, '__BINANCE_MIRROR_PREFLIGHT__', { + value: Object.freeze({ snapshot, stop }), configurable: true, + }); +})(); diff --git a/experiments/binance-chart-storage/native-preflight.user.js b/experiments/binance-chart-storage/native-preflight.user.js new file mode 100644 index 0000000..1cd8537 --- /dev/null +++ b/experiments/binance-chart-storage/native-preflight.user.js @@ -0,0 +1,1025 @@ +// ==UserScript== +// @name Binance Chart Mirror Native Preflight (Experiment) +// @namespace binance.chart.mirror.native-preflight +// @version 0.0.1 +// @description Observe pinned mirror startup and native dispatch counts without custom storage writes. +// @match https://www.binance.com/zh-CN/futures/USUSDT +// @run-at document-start +// @sandbox raw +// @grant none +// @noframes +// ==/UserScript== +(() => { + // experiments/binance-chart-storage/bootstrap.js + var QUEUE_NAME = "webpackChunkfutures_trade_ui"; + function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } = {}) { + const lab = location.origin === "https://chart-storage.test"; + const incidentPage = location.origin === "https://www.binance.com" && location.pathname === "/zh-CN/futures/USUSDT" && self === top; + if (!lab && !incidentPage) throw new Error("Storage bootstrap requires the lab or the top-level USUSDT incident page"); + if (onCapture !== void 0 && typeof onCapture !== "function") throw new Error("onCapture must be a synchronous function"); + if (replaceMirrorFactory !== void 0 && typeof replaceMirrorFactory !== "function") throw new Error("replaceMirrorFactory must be a synchronous function"); + const moduleId = replaceMirrorFactory === void 0 ? "43917" : "70940"; + const globalDescriptor = Object.getOwnPropertyDescriptor(self, QUEUE_NAME); + if (globalDescriptor && !Object.hasOwn(globalDescriptor, "value")) throw new Error("Storage bootstrap requires a native queue property"); + const queue = globalDescriptor && globalDescriptor.value !== void 0 ? globalDescriptor.value : []; + const nativeAppend = Array.prototype.push; + if (!Array.isArray(queue) || queue.length !== 0 || queue.push !== nativeAppend || Object.hasOwn(queue, "push")) { + throw new Error("Storage bootstrap must start before runtime on a native empty queue"); + } + if (globalDescriptor && globalDescriptor.value === void 0 && !globalDescriptor.configurable) { + throw new Error("Storage bootstrap cannot own the existing global queue property"); + } + const createdQueue = !globalDescriptor || globalDescriptor.value === void 0; + if (createdQueue) Object.defineProperty(self, QUEUE_NAME, { value: queue, writable: true, configurable: true, enumerable: true }); + let runtimePush; + let inFlightAppends = 0; + let active = true; + let originalFactory; + let observedFactory; + let observedChunk; + let runtimeRequire; + let resolveCapture; + let rejectCapture; + const captured = new Promise((resolve, reject) => { + resolveCapture = resolve; + rejectCapture = reject; + }); + function restore() { + if (runtimeRequire && runtimeRequire.m[moduleId] === observedFactory) runtimeRequire.m[moduleId] = originalFactory; + if (observedChunk && observedChunk[1][moduleId] === observedFactory) observedChunk[1][moduleId] = originalFactory; + const descriptor = Object.getOwnPropertyDescriptor(queue, "push"); + if (!descriptor || descriptor.get !== getPush || descriptor.set !== setPush || !descriptor.configurable) { + throw new Error("Storage bootstrap lost queue accessor ownership"); + } + if (runtimePush) { + Object.defineProperty(queue, "push", { value: runtimePush, writable: true, configurable: true, enumerable: true }); + } else { + delete queue.push; + if (createdQueue && inFlightAppends === 0 && queue.length === 0 && Object.getOwnPropertyDescriptor(self, QUEUE_NAME)?.value === queue) { + if (globalDescriptor) Object.defineProperty(self, QUEUE_NAME, globalDescriptor); + else delete self[QUEUE_NAME]; + } + } + } + function finish(succeeded, result) { + if (!active) return; + active = false; + try { + restore(); + } catch (cleanupError) { + if (succeeded) { + succeeded = false; + result = cleanupError; + } + } + if (succeeded) resolveCapture(result); + else rejectCapture(result); + } + function prepareChunk(chunk) { + if (!active) return chunk; + try { + if (!Array.isArray(chunk) || !Array.isArray(chunk[0]) || !chunk[1] || typeof chunk[1] !== "object") { + throw new Error("Storage bootstrap observed an unsupported chunk shape"); + } + if (!Object.hasOwn(chunk[1], moduleId)) return chunk; + if (originalFactory) throw new Error("Storage module registered twice before capture"); + originalFactory = chunk[1][moduleId]; + if (typeof originalFactory !== "function") throw new Error("Storage module factory must be a function"); + const hostRuntime = chunk[2]; + if (hostRuntime !== void 0 && typeof hostRuntime !== "function") throw new Error("Storage chunk runtime callback must be a function"); + const executionFactory = replaceMirrorFactory === void 0 ? originalFactory : replaceMirrorFactory(originalFactory); + if (!active) return chunk; + if (typeof executionFactory !== "function") throw new Error("Mirror replacement must return a factory synchronously"); + observedFactory = function(module, exports, require2) { + runtimeRequire = require2; + let returned; + try { + returned = executionFactory.call(this, module, exports, require2); + } catch (hostError) { + finish(false, hostError); + throw hostError; + } + if (active) { + try { + const result = onCapture === void 0 ? void 0 : onCapture(module.exports); + if (result && typeof result.then === "function") throw new Error("Storage observation callback must complete synchronously"); + finish(true, module.exports); + } catch (observationError) { + finish(false, observationError); + } + } + return returned; + }; + observedChunk = [chunk[0], { ...chunk[1], [moduleId]: observedFactory }, (require2) => { + runtimeRequire = require2; + if (hostRuntime !== void 0) return hostRuntime(require2); + }]; + return observedChunk; + } catch (observationError) { + finish(false, observationError); + return chunk; + } + } + function preRuntimePush(...chunks) { + if (!active || runtimePush) return nativeAppend.apply(this, chunks); + inFlightAppends += 1; + try { + return nativeAppend.apply(this, chunks.map(prepareChunk)); + } finally { + inFlightAppends -= 1; + } + } + function runtimeObservingPush(chunk) { + return runtimePush.call(this, prepareChunk(chunk)); + } + function getPush() { + return runtimePush ? runtimeObservingPush : preRuntimePush; + } + function setPush(dispatcher) { + if (!active) { + Object.defineProperty(queue, "push", { value: dispatcher, writable: true, configurable: true, enumerable: true }); + return; + } + if (runtimePush || typeof dispatcher !== "function") { + finish(false, new Error("Storage bootstrap observed an unexpected push replacement")); + Object.defineProperty(queue, "push", { value: dispatcher, writable: true, configurable: true, enumerable: true }); + return; + } + runtimePush = dispatcher; + } + Object.defineProperty(queue, "push", { get: getPush, set: setPush, enumerable: true, configurable: true }); + return Object.freeze({ + captured, + stop() { + finish(false, new Error("Storage bootstrap stopped before module execution")); + } + }); + } + + // experiments/binance-chart-storage/mirror-module.js + var originalFactorySource = '70940(Z,x,e){"use strict";e.r(x),e.d(x,{TradingView:()=>Dt,default:()=>Cn});var o=e(49253),v=e(77135),d=e(75510),i=e(42288),p=e(31085),r=e(41594),M=e(43917),h=e.n(M),P=e(81936),b=e.n(P),V=e(7980),U=e.n(V),D=e(76469),c=e(4260),k=e(53837),B=e(27571),C=e(45250),y=e(87646),A=e.n(y),q=e(80817);const E=(0,C.range)(1,1380),m=(0,C.range)(1,365).map(t=>`${t}D`),R=(0,C.range)(1,52).map(t=>`${t}W`),I=(0,C.range)(1,12).map(t=>`${t}M`),F=["1S",...E,...m,...R,...I],L=({symbol:t,params:n})=>{const a=new URLSearchParams(n);return`${t}@${a.toString()}`},X=t=>{const[n,a]=t.split("@"),s=new URLSearchParams(a).entries(),g=A()(Array.from(s));return{symbol:n,params:g}},ue=()=>{const t=new Map;let n={},a=[],u={onReady:()=>null,resolveSymbol:()=>null,searchSymbols:()=>null,getBars:()=>null,subscribeBars:()=>null,unsubscribeBars:()=>null};return({config:s,onSymbolResolving:g,onSymbolResolved:S,onFetchSymbolsInfo:K,onFetchBars:Y,onSubscribeBars:H,onUnsubscribeBars:j,onFetchServerTimeApi:$,onTransformSymbolInfo:f=({template:T})=>T,onSearchSymbols:l=()=>!0})=>(u={onReady:T=>{setTimeout(async()=>{const _=(await K()).map(O=>({...O,ticker:L({symbol:O.symbol,params:O.params||{}})}));a=_.map(({ticker:O,symbol:G,description:te="",exchange:J="",type:oe=""})=>({symbol:G,ticker:O,description:te,exchange:J,type:oe})),n=(0,C.keyBy)(_,"ticker"),T({exchanges:[],symbols_types:[],supported_resolutions:F,supports_marks:!1,supports_timescale_marks:!1,supports_time:!0,...s})})},resolveSymbol:async(T,W,_)=>{const O=n[T];if(!O){_(`cannot resolve symbol - ${T}`);return}const G=f({info:O,template:{description:O.description||"",fractional:!1,has_seconds:!0,seconds_multipliers:["1"],has_intraday:!0,intraday_multipliers:["1","3","5","15","30","60","120","240","360","480","720"],has_daily:!0,daily_multipliers:["1"],has_weekly_and_monthly:!0,weekly_multipliers:["1"],monthly_multipliers:["1"],minmov:1,minmove2:0,name:O.symbol,ticker:O.ticker,timezone:"Etc/UTC",pricescale:O.tickSize,session:"24x7",type:O.type||"",exchange:O.exchange||"",listed_exchange:O.exchange||"",format:"price",supported_resolutions:F,visible_plots_set:"ohlcv",volume_precision:3}});if(g){const te=X(G.ticker||"");if(!await g({symbol:T,info:G,tickerInfo:te})){_(`symbol resolving error - ${T}`);return}}setTimeout(()=>{W(G),S?.(G)})},searchSymbols:(T,W,_,O)=>{const G=new RegExp(T,"i"),te=new RegExp(W,"i"),J=new RegExp(_,"i"),oe=a.filter(Te=>{const{symbol:N,ticker:w,description:re="",exchange:ie="",type:le=""}=Te;return[N,w,re].some(de=>G.test(de||""))&&te.test(ie)&&J.test(le)&&l?.({item:Te})});O(oe)},getBars:async(T,W,_,O,G)=>{const{from:te,to:J,firstDataRequest:oe}=_,Te=(0,q.Cz)(W);try{if(J>=0){const N=X(T.ticker||""),w=await Y({symbolInfo:T,resolution:W,rangeStartDate:te,rangeEndDate:J,tickerInfo:N,binanceInterval:Te,firstDataRequest:oe});O(w,{noData:w.length===0})}else O([],{noData:!0})}catch(N){G(N.toString())}},subscribeBars:(T,W,_,O,G)=>{const te=X(T.ticker||""),J=(0,q.Cz)(W),oe=H({symbolInfo:T,resolution:W,callback:_,onResetCacheNeededCallback:G,tickerInfo:te,binanceInterval:J});t.set(O,oe)},unsubscribeBars:T=>{t?.get(T)?.(),t?.delete(T),j?.(T)},getServerTime:async T=>{try{const W=await $();T(W)}catch(W){console.warn(W),T(Date.now()/1e3)}}},{datafeed:u,observers:t,symbolsInfo:n})},Le=({namespace:t,type:n})=>{const a=(0,r.useMemo)(()=>h().createInstance({name:t}),[t]);return(0,r.useCallback)(()=>{n==="tv"&&a.removeItem("myTradingView")},[a,n])};var Ue=e(76535),pr=e(26531),Ye=e(17409),Qe=e(94652),Ae=e(76830),he=e(48651),Fe=e(79344),se=e(51029),Je=e(36307),or=e(25379),yr=e(44745),Ne=e(15727),rr=e(45358),sr=e(82071),Ve=e(85411),ge=e(94889),hr=e(94547),tr=e(80686),Ar=e(61875);function Fr(t){return(0,ge._)(t)||(0,hr._)(t)||(0,Ar._)(t)||(0,tr._)()}var Nr=e(92873),zr=e(35630),Vr=e(12055),Qr=e(79515),Br=e(80737),Jr=e(40545),Zr=e(98107),Kr=e(51846),Xr="Chart.Crosshair.PlusButton.DrawHorizontalLine",wr=function(t){var n=t.namespace,a=t.symbol,u=t.priceType,s=t.crosshairPrice,g=(0,Nr.o)("","kline-ui").getI18n,S=(0,Vr.r)().format,K=k.y$(n),Y=(0,Br.S)({namespace:n}),H=(0,Jr.hw)(),j=(0,Zr.c)(),$=(0,Qr.nH)(),f=(0,se.Ye)(),l=(0,Kr.E)({symbol:a}),T=(0,Fe.f0)(a),W=T.isCM,_=(0,r.useRef)(s);(0,r.useEffect)(function(){_.current=s},[s]);var O=(0,r.useRef)(H);(0,r.useEffect)(function(){O.current=H},[H]);var G=(0,r.useRef)(j);(0,r.useEffect)(function(){G.current=j},[j]);var te=(0,r.useRef)(u);(0,r.useEffect)(function(){te.current=u},[u]);var J=(0,r.useRef)($);(0,r.useEffect)(function(){J.current=$},[$]);var oe=(0,r.useRef)(f);(0,r.useEffect)(function(){oe.current=f},[f]);var Te=(0,r.useRef)(l);(0,r.useEffect)(function(){Te.current=l},[l]);var N=(0,r.useRef)(W);return(0,r.useEffect)(function(){N.current=W},[W]),{items_processor:(function(){var w=(0,o._)(function(re,ie,le){var de,Be,ve,Ce,ce,Ke,Ze,Ge,Xe,Ie,He,ur,Tr,_r,Cr;return(0,i._)(this,function(Er){if(Be=K.getState().tradingViewReference,!Be)return console.warn("[context-menu] trading view reference is not found"),[2,re];if(Be.chartsCount()>1)return[2,re];if(ve=Be.activeChart().symbolExt(),!ve)return[2,re];switch(Ce=ve.name,ce=_.current,Ke=le.menuName==="CrosshairMenuView"?"chart-trading":"context-menu",Ze={price:"".concat(ce.value),formattedPrice:ce.formatted,source:Ke,chartType:"tradingview"},Ge=(de=oe.current[Ce])===null||de===void 0?void 0:de.baseAsset,Xe=g("trade",{defaultValue:"Trade"}),Ie=S(ce.formatted),He=G.current&&te.current===c.SJ.Last&&J.current&&Ge?[ie.createAction({actionId:"Chart.PlaceOrder.BuyLimit",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("limit",{defaultValue:"Limit"})),onExecute:function(){return Te.current.onClickBuyLimit(Ze)}}),ie.createAction({actionId:"Chart.PlaceOrder.BuyStop",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("stop",{defaultValue:"Stop"})),onExecute:function(){return Te.current.onClickBuyStop(Ze)}})]:[],ur=O.current?ie.createAction({actionId:"Chart.PriceAlert.CreateAtCrosshair",label:g("create-alert-at",{defaultValue:"Create Alert at {{price}}",price:ce.formatted}),onExecute:function(){Y({symbol:Ce,price:ce.formatted,mode:zr.n.CREATE})}}):null,re.forEach(function(we){if(!(!("execute"in we)||!("getState"in we))){var Dr=we.getState();if(Dr.actionId===Xr){var z=we.execute.bind(we);we.execute=function(){(0,Kr.A)({elementId:"draw_horizontal_line",isDelivery:N.current,chartType:"tradingview"}),z()}}}}),le.menuName){case"CrosshairMenuView":return ur?[2,[ur].concat((0,d._)(He),(0,d._)(re.slice()))]:[2,(0,d._)(He).concat((0,d._)(re.slice()))];case"ChartContextMenu":return ur?(Tr=Fr(re),_r=Tr[0],Cr=Tr.slice(1),[2,[_r,ur].concat((0,d._)(He),(0,d._)(Cr))]):[2,(0,d._)(He).concat((0,d._)(re.slice()))];default:return[2,re]}return[2]})});return function(re,ie,le){return w.apply(this,arguments)}})()}},qr=function(){var t=(0,r.useState)({value:NaN,formatted:""}),n=t[0],a=t[1],u=(0,r.useCallback)(function(s){var g=s.crosshairData,S=s.activeChart;S&&a({value:g.price,formatted:S.priceFormatter().format(g.price)})},[]);return{onCrosshairMoved:u,crosshairPrice:n}};const Hr=t=>Math.floor(Math.log10(t)),et=t=>{const[n="1"]=t.split(",");return Hr(+n)},pe=({symbolInfo:t,minTick:n})=>{const{pricescale:a=2}=t||{};return n==="default"||n===void 0?Hr(a):et(n)},br=t=>(n,a)=>{try{return n?{format:u=>{const s=pe({symbolInfo:n,minTick:a});return t.format({symbolInfo:n,minTick:a,value:u,precision:s})}}:null}catch{return null}},Mr=10**12,rt=10**9,tt=10**6,jr=10**3,gr=t=>{const n=t.toString(),[,a=""]=n.split(".");return Math.min(a.length,3)},nt=({value:t})=>{const n=Math.abs(t);if(n>=Mr){const a=t/Mr;return{value:a,unit:"T",precision:gr(a)}}if(n>=rt){const a=t/rt;return{value:a,unit:"B",precision:gr(a)}}if(n>=tt){const a=t/tt;return{value:a,unit:"M",precision:gr(a)}}if(n>=jr){const a=t/jr;return{value:a,unit:"K",precision:gr(a)}}return{value:t,unit:"",precision:gr(t)}},ut=t=>(n,a,u)=>{try{return a&&n.type==="volume"?{format:s=>s===void 0?"":t.formatVolume({format:n,symbolInfo:a,precision:u,value:s,volumeDetail:nt({value:s})})}:null}catch{return null}};var lt=function(){var t=(0,Vr.r)(),n=t.format,a=(0,r.useMemo)(function(){return br({format:function(s){var g=s.value,S=s.precision;return n(g,{precision:S})}})},[n]),u=(0,r.useMemo)(function(){return ut({formatVolume:function(s){var g=s.value,S=s.volumeDetail;if(g===void 0||!S)return"";var K=n(S.value,{precision:S.precision});return"".concat(K).concat(S.unit)}})},[n]);return{priceFormatterFactory:a,studyFormatterFactory:u}},dt=e(74069),ir=function(t){var n=(0,dt.K)(t),a=(0,r.useMemo)(function(){return n.map(function(u){var s=u.id,g=u.time,S=u.isBuy,K=u.price,Y=u.tooltip;return{id:s,text:"",price:K,quantity:"",time:Math.round(g/1e3),isBuy:S,tooltip:Y}})},[n]);return{executionOrders:a}},at=function(t){var n=t.namespace,a=t.symbol,u=(0,Br.j)({namespace:n,symbol:a});return(0,r.useMemo)(function(){return u.map(function(s){var g=s.id,S=s.price,K=s.formattedPrice,Y=s.sideText,H=s.showCloseButton,j=s.isPriceChangeable,$=s.cancelTooltipText,f=s.onClick,l=s.onCancel,T=s.onPriceChanged;return{id:g,price:+S,text:"".concat(Y," ").concat(K),editable:j,cancellable:H,cancelTooltip:$,type:c.ND.PriceAlert,onCancel:H?l:void 0,onMove:j?function(W){var _=W.price;T({currentPrice:_})}:void 0,onClick:f}})},[u])},vt=e(90664),ot=function(t){var n=t.symbol,a=(0,vt.w)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){var s=u.id,g=u.text,S=u.price;return{id:s,text:g,price:+S,type:c.ND.BreakEvenPrice}})},[a])},cr=e(82508),ft=e(91025),mt=e(36077),Rr=function(t){var n=t.symbol,a=(0,mt.c)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){return(0,ft._)((0,cr._)({},u),{type:c.ND.LiquidationPrice})})},[a])},pt=function(t){var n=t.namespace,a=t.symbol,u=at({namespace:n,symbol:a}),s=ot({symbol:a}),g=Rr({symbol:a});return(0,r.useMemo)(function(){return(0,d._)(u).concat((0,d._)(s),(0,d._)(g))},[u,s,g])},st=e(42902),Ir=e(90170),yt=e(63345),ht=e(48210),Pr=e(51289),xr=e(24326),$r=e(90922),kr=e(34235),it=e(63762),Ft=function(t){var n=(0,Fe.f0)(t),a=n.pricePrecision,u=n.isCM,s=(0,st.yD)(t)||{},g=s.lastPrice,S=g===void 0?"0":g,K=(0,kr.Bp)(u),Y=(0,it.HY)(t),H=(0,kr.gv)(u),j=(0,kr.WF)(u);return(0,r.useMemo)(function(){return(0,$r.qF)({orders:(0,d._)(K).concat((0,d._)(H),(0,d._)(j)),symbol:t}).map(function($){return Y({order:$,lastPrice:S,pricePrecision:a})})},[S,K,H,a,t,Y])},gt=e(31542),Tt=function(t){var n=t.type;(0,gt.u4)("webClick",{module:"chart",elementId:n})},Nt=function(t){var n=t.symbol,a=t.namespace,u=(0,r.useState)(0),s=u[0],g=u[1],S=(0,r.useCallback)(function(){return g(function(N){return N+1})},[]),K=Ft(n),Y=(0,xr.IT)({namespace:a}),H=(0,Fe.f0)(n),j=H.isDelivery,$=H.contractType,f=(0,st.yD)(n)||{},l=f.lastPrice,T=l===void 0?"0":l,W=(0,yt.s)(n),_=(0,it.YZ)({symbol:n,afterConfirmationModalIsShown:S}),O=(0,it.u8)(n),G=(0,it.jn)(n),te=(0,r.useCallback)(function(N){return(0,ht.D)({order:N,isDelivery:j,symbol:j?N.symbol:"".concat(N.symbol,"_").concat($),module:"tradingview_kline"})},[$,j]),J=(0,r.useMemo)(function(){return K.map(function(N){var w=N.order,re=N.price,ie=N.quantity,le=N.quantityText,de=N.isBuy,Be=N.text,ve=N.priceTooltipText,Ce=N.pnlText,ce=(0,Pr.$L)(w),Ke=(0,Pr.OX)(w),Ze=(0,Pr.xL)(w),Ge=ce==="LIMIT",Xe=(0,Ir.Y)(w);return{id:String(Ke),text:Be,price:+re,quantity:le,isBuy:de,editable:Xe&&G,tooltip:ve,modifyTooltip:w.closePosition||!Ge?"":void 0,onMove:G?function(Ie){var He=Ie.price;te(w),Tt({type:"TV_vertical"}),_({order:w,price:+He,quantity:+Ze,lastPrice:T})}:void 0,onModify:w.closePosition||!Ge||!Xe?void 0:function(){Tt({type:"TV_amountclick"}),O({order:w,price:re.toString(),quantity:ie,lastPrice:T})},onCancel:function(){Tt({type:"TV_cancel"}),W(w)}}})},[G,K,T,te,_,O,W,s]),oe=(0,r.useMemo)(function(){return Y.map(function(N){var w=N.id,re=N.value,ie=N.text,le=N.onChange,de=N.isBuy,Be=N.type;return{id:w,price:+re,quantity:"",text:ie,isBuy:de,editable:!0,limitOrderType:c.aA.OrderPreview,onMove:function(ve){var Ce=ve.price;(0,xr.eP)({namespace:a,orderType:Be}),le({id:w,newPrice:Ce})},onMoving:function(ve){var Ce=ve.price;return le({id:w,newPrice:Ce})}}})},[Y,a]),Te=(0,r.useMemo)(function(){return(0,d._)(J).concat((0,d._)(oe))},[J,oe]);return{orderLines:Te}},Vt=e(17855),Bt=function(t,n){var a=(0,r.useState)(t()),u=a[0],s=a[1];return(0,Vt.A)(function(){return s(t())},n),u},Kt=e(58656),Ht=function(t){var n=t.symbol,a=(0,Kt.Q)({symbol:n})||[],u=Bt(function(){return a.map(function(s){var g=s.position,S=s.pnl,K=s.pnlInNumber,Y=s.pnlText,H=s.price,j=s.quantity,$=s.reverseText,f=s.isBuy,l=s.displayReverseOrderModal,T=s.displayClosePositionModal;return{id:g.id||"".concat(g.symbol,"-").concat(g.positionSide),PNL:K,text:"".concat(Y," ").concat(S),price:H,quantity:j,tooltip:"",reverseTooltip:$,isBuy:f,onReverse:l,onClose:function(){T(),(0,gt.u4)("$WebClick",{item:1,module:"xxx",$element_id:"kline_close_position"})}}})},[a]);return{positionLines:u}},jt=e(47354),bt=e.n(jt),$t=e(87017),Ut=e(99168),Yt=e(3742),Wt=e(43335),Gt=e(15540),zt=e(20163),Qt=e(47255);const Jt=t=>({endTime:n})=>(n??0)===t;var Zt=e(36488),Ct=e(48018),Xt=e(82911),We=e(69578),wt=e(53208),Rt=e(19862),qt=e(8874),en=e(26679),rn=e(66547),It=(function(){var t=(0,o._)(function(n){var a,u,s,g,S,K,Y,H,j;return(0,i._)(this,function($){switch($.label){case 0:a=n.onFetchKlineData,u=n.startTime,s=n.endTime,g=n.interval,S=n.limit,K=S===void 0?1e3:S,Y=[],H=[],j=s,$.label=1;case 1:return[4,a((0,cr._)({interval:g,limit:K},j===0?{}:{endTime:j}))];case 2:if(H=$.sent(),H.length===0)return[3,4];H.forEach(function(f){var l=(0,v._)(f,8),T=l[0],W=l[1],_=l[2],O=l[3],G=l[4],te=l[5],J=l[7];Y.push({time:+T,open:+W,high:+_,low:+O,close:+G,volume:+te,quantity:+J})}),j=H[0][0]-1,$.label=3;case 3:if(j>u)return[3,1];$.label=4;case 4:return Y.sort(function(f,l){return f.time-l.time}),[2,Y]}})});return function(a){return t.apply(this,arguments)}})(),tn=function(t){var n=t.firstDataRequest,a=t.hasRequested,u=t.rangeEndDate;return n&&!a?0:u*1e3},nn=e(78142),an=e(16238),St=e.n(an),on=e(98087),xt=e.n(on),sn=e(55009),cn=e.n(sn),kt=e(56477);const Et=t=>cn()(t.chartsCount(),n=>t.chart(n)),un=async t=>{try{const{schema:n,data:a}=await t.exportData({includedStudies:[]}),u=xt()(a)?.[n.findIndex(s=>s.type==="time")];return u!==void 0&&Number.isFinite(u)?u*1e3:void 0}catch{return}},ln=({getWidget:t,...n})=>{const a=new Map,u=new Map,s=()=>{try{const f=t();return f?St()(Et(f).map(l=>l.symbolExt()?.ticker)):[]}catch{return[]}},g=f=>{const l=s();return!l.length||l.includes(f)},S=f=>{const l=u.get(f);if(l)return l;const T={controller:new AbortController,claimedAsMain:!1};return u.set(f,T),T},K=f=>{const l=u.get(f);l&&(l.controller.abort(),u.delete(f))},Y=()=>{const f=s();f.length&&Array.from(u.keys()).forEach(l=>{const T=u.get(l);if(T){if(f.includes(l)){T.claimedAsMain=!0;return}a.has(l)||T.claimedAsMain&&K(l)}})},H=(f,l)=>{f.closed||l.time{f.held=[];let T=l;try{const _=(await f.options.fetchSince(l)).filter(O=>O.time>=l&&!f.closed);_.forEach(O=>{f.highWater=Math.max(f.highWater,O.time),f.options.onTick(O)}),T=xt()(_)?.time??l}finally{const W=f.held??[];f.held=void 0,W.filter(_=>_.time>=T).forEach(_=>{H(f,_)})}},$=(0,kt.k)({...n,onCheck:Y,repair:async()=>{const f=t();if(f)try{const l=Et(f),T=St()(l.map(_=>{const O=a.get(_.symbolExt()?.ticker??"");return O&&{chart:_,feed:O}}));if(!T.length){f.resetCache(),l.forEach(_=>{_.resetData()});return}const W=St()(await Promise.all(T.map(async({chart:_,feed:O})=>{const G=await un(_);return G===void 0?void 0:{feed:O,fromMs:G}})));await Promise.allSettled(W.map(({feed:_,fromMs:O})=>j(_,O)))}catch{}}});return{openFeed:f=>{const l={options:f,held:void 0,highWater:0,closed:!1,detector:void 0};return l.detector=(0,kt.k)({...n,subscribeSignals:f.subscribeSignals,repair:async()=>{l.closed||!l.highWater||g(f.ticker)||await j(l,l.highWater)}}),a.set(f.ticker,l),g(f.ticker)&&$.resubscribeSignals(f.subscribeSignals),{push:T=>{l.held?l.held.push(T):H(l,T)},close:()=>{l.closed=!0,l.detector?.dispose(),a.get(f.ticker)===l&&a.delete(f.ticker)}}},subscribeWake:$.subscribeWake,getSignal:f=>{const l=S(f);return s().includes(f)&&(l.claimedAsMain=!0),l.controller.signal},dispose:()=>{u.forEach(({controller:f})=>{f.abort()}),u.clear(),a.forEach(f=>{f.detector?.dispose()}),a.clear(),$.dispose()}}};var dn=e(71730),vn=e(36620),fn=function(t){var n=t.namespace,a=k.y$(n),u=(0,vn.l)().getSocketSignals,s=(0,r.useRef)();(0,r.useEffect)(function(){return function(){var S;(S=s.current)===null||S===void 0||S.instance.dispose(),s.current=void 0}},[]);var g=(0,r.useCallback)(function(S){var K,Y,H=S;if(((K=s.current)===null||K===void 0?void 0:K.key)===H)return s.current.instance;(Y=s.current)===null||Y===void 0||Y.instance.dispose();var j=nn.wr(S),$=ln({getWidget:function(){return a.getState().tradingViewReference},checkPeriodMs:dn.D(j)});return s.current={key:H,instance:$},$},[a]);return(0,r.useMemo)(function(){return{ensureRecovery:g,getSocketSignals:u}},[g,u])},mn="tv_web_error",pn=[Ye.bt.TRADING,Ye.bt.DELIVERING],Re;(function(t){t.Futures="futures",t.FuturesMark="futures-mark",t.FuturesIndex="futures-index",t.Delivery="delivery",t.DeliveryMark="delivery-mark",t.DeliveryIndex="delivery-index"})(Re||(Re={}));var Mt=function(t){var n=t.symbol,a=t.description,u=t.productType,s=t.pricePrecision,g=t.params,S=t.supportedResolutions,K=S===void 0?F:S;return{symbol:n,description:a,tickSize:Math.pow(10,s),exchange:"Binance",supportedResolutions:K,type:u,params:g}},yn=function(t){var n=t.namespace,a=t.useMiniTicker,u=a===void 0?!1:a,s=t.isCM,g=c.y$(n),S=k.y$(n),K=(0,$t.useQueryClient)(),Y=fn({namespace:n}),H=Y.ensureRecovery,j=Y.getSocketSignals,$=(0,r.useRef)(0),f=(0,r.useRef)(!1),l=(0,r.useMemo)(function(){return bt()(function(){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsStart",catch:function(Q){var ee=Q.error,ae=Q.track;ae({GET_BAR_START_TYPE:ee})},log:function(Q){var ee=Q.track;ee({GET_BAR_START_TYPE:"SUCCESS"})}})})},[]),T=(0,r.useMemo)(function(){return bt()(function(z){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsEnd",catch:function(ee){var ae=ee.error,Se=ee.track;Se({GET_BAR_END_TYPE:ae,duration_:(0,We.Pu)(z-$.current)})},log:function(ee){var ae=ee.track;ae({GET_BAR_END_TYPE:"SUCCESS",duration_:(0,We.Pu)(z-$.current)})}})})},[]),W=(0,Nr.o)("trd-chart","trade-ui"),_=W.getI18n,O=(0,he.ud)(),G=O.getI18n,te=(0,v._)(g(c.Ir),1),J=te[0],oe=(0,v._)(S(k.A7),2),Te=oe[1],N=(0,se.Ye)(),w=(0,Zt.JD)(s).isContUnit,re=(0,se.y_)(),ie=(0,r.useRef)(N),le=(0,r.useRef)(J),de=(0,r.useRef)(re);(0,r.useEffect)(function(){ie.current=N},[N]),(0,r.useEffect)(function(){le.current=J},[J]),(0,r.useEffect)(function(){de.current=re},[re]);var Be=(0,r.useMemo)(function(){return w?Rt.b:Rt.S},[w]),ve=(0,r.useMemo)(function(){return G("usdtFutures-13",{defaultValue:"Futures"})||""},[G]),Ce=(0,r.useMemo)(function(){return G("inverseFutures-13",{defaultValue:"Delivery"})||""},[G]),ce=(0,r.useMemo)(function(){return _("indexPrice",{defaultValue:"Index Price"})||""},[_]),Ke=(0,r.useMemo)(function(){return _("markPrice",{defaultValue:"Mark Price"})||""},[_]),Ze=(0,r.useMemo)(function(){return _("lastPrice",{defaultValue:"Last Price"})||""},[_]),Ge=(0,r.useMemo)(function(){return{exchanges:[{name:"Binance",value:"Binance",desc:""}],symbols_types:[{name:ve,value:Re.Futures},{name:"".concat(ve," ").concat(Ke),value:Re.FuturesMark},{name:"".concat(ve," ").concat(ce),value:Re.FuturesIndex},{name:Ce,value:Re.Delivery},{name:"".concat(Ce," ").concat(Ke),value:Re.DeliveryMark},{name:"".concat(Ce," ").concat(ce),value:Re.DeliveryIndex}]}},[Ce,ve,ce,Ke]),Xe=(0,r.useCallback)((0,o._)(function(){var z,Q,ee;return(0,i._)(this,function(ae){switch(ae.label){case 0:return ae.trys.push([0,2,,3]),[4,(0,en.c)(K)];case 1:return ae.sent(),[3,3];case 2:return z=ae.sent(),(0,gt.u4)(mn,{type:"onFetchSymbolsInfo",message:z.message,business:"futures"}),[3,3];case 3:return Q=ie.current,ee=Object.keys(Q).reduce(function(Se,xe){var ne=Q[xe]||Ct.o,ke=ne.symbol,Me=ne.pricePrecision,fe=ne.isDelivery,Pe=ne.contractStatus;if(!pn.includes(Pe))return Se;var _e=(0,Yt.S)(ne,G),je=_e.symbol,Ee=_e.contractTypeI18n,me=_e.deliveryDateFormat,ye="".concat(je," ").concat(Ee).concat(me?" ".concat(me):""),$e="".concat(ye," ").concat(Ze),Oe="".concat(ye," ").concat(ce),lr="".concat(ye," ").concat(Ke);return Se.concat([Mt({symbol:ke,description:$e,productType:fe?Re.Delivery:Re.Futures,pricePrecision:Me,params:{priceType:c.SJ.Last}}),Mt({symbol:ke,description:Oe,productType:fe?Re.DeliveryIndex:Re.FuturesIndex,pricePrecision:Me,params:{priceType:c.SJ.Index}}),Mt({symbol:ke,description:lr,productType:fe?Re.DeliveryMark:Re.FuturesMark,pricePrecision:(0,Xt.X)(Me),params:{priceType:c.SJ.Mark}})])},[]),[2,ee]}})}),[G,ce,Ke,Ze,K]),Ie=(0,r.useCallback)(function(z){var Q=z.ticker,ee=z.symbol,ae=z.interval,Se=z.priceType,xe=z.retryUntil,ne=ie.current[ee]||Ct.o,ke=ne.pair,Me=ne.contractType,fe=ne.contractVal,Pe=ne.isDelivery,_e=Pe?Be(fe):function(Oe){return Oe},je=(0,wt.h)({symbol:ee,pair:ke,contractType:Me,isDelivery:Pe,priceType:Se}),Ee,me,ye=function(Oe){return zt.y("tv","".concat(Pe?"cm":"um","|").concat(ee,"|").concat(Se,"|").concat(Oe.interval,"|").concat((Ee=Oe.limit)!==null&&Ee!==void 0?Ee:"","|").concat((me=Oe.endTime)!==null&&me!==void 0?me:""))};if(xe===void 0)return{transform:_e,onFetchKlineData:je};var $e=H(ae);return{transform:_e,onFetchKlineData:Qt.b(je,{enabled:Jt(xe),key:ye,signal:$e.getSignal(Q),subscribeWake:$e.subscribeWake})}},[H,Be]),He=(0,r.useCallback)((function(){var z=(0,o._)(function(Q){var ee,ae,Se,xe,ne,ke,Me,fe,Pe,_e,je,Ee,me,ye,$e,Oe,lr,Or;return(0,i._)(this,function(dr){switch(dr.label){case 0:return ee=Q.binanceInterval,ae=Q.rangeStartDate,Se=Q.rangeEndDate,xe=Q.tickerInfo,ne=Q.symbolInfo,ke=Q.firstDataRequest,$.current=Date.now(),l(),Me=xe.symbol,fe=Me===void 0?"":Me,Pe=xe.params,_e=Pe.priceType,je=_e===void 0?le.current:_e,de.current({symbol:fe,priceType:je})?(T(Date.now()),[2,[]]):(Ee=Math.max(0,ae*1e3),me=tn({firstDataRequest:ke,hasRequested:f.current,rangeEndDate:Se}),f.current=!0,$e=Ie({ticker:(ye=ne.ticker)!==null&&ye!==void 0?ye:"",symbol:fe,interval:ee,priceType:je,retryUntil:ke?me:void 0}),Oe=$e.transform,lr=$e.onFetchKlineData,[4,It({onFetchKlineData:lr,startTime:Ee,endTime:me,interval:ee})]);case 1:return Or=dr.sent(),T(Date.now()),[2,Or.map(Oe)]}})});return function(Q){return z.apply(this,arguments)}})(),[Ie,T,l]),ur=(0,r.useCallback)(function(z){var Q=z.binanceInterval,ee=z.callback,ae=z.tickerInfo,Se=z.symbolInfo,xe=ae.symbol,ne=xe===void 0?"":xe,ke=ae.params,Me=ke.priceType,fe=Me===void 0?le.current:Me;if(de.current({symbol:ne,priceType:fe}))return function(){return null};var Pe=ie.current[ne]||Ct.o,_e=Pe.pair,je=Pe.contractType,Ee=Pe.isDelivery,me,ye=(me=Se.ticker)!==null&&me!==void 0?me:"",$e=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe}).transform,Oe=H(Q),lr=Oe.openFeed({ticker:ye,onTick:ee,subscribeSignals:j(Ee),fetchSince:(function(){var dr=(0,o._)(function(Lr){var Ur,Yr,ct,De;return(0,i._)(this,function(ze){switch(ze.label){case 0:return Ur=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe,retryUntil:0}),Yr=Ur.transform,ct=Ur.onFetchKlineData,[4,It({onFetchKlineData:ct,startTime:Lr,endTime:0,interval:Q})];case 1:return De=ze.sent(),[2,De.map(Yr)]}})});return function(Lr){return dr.apply(this,arguments)}})()}),Or=(0,rn.B)({symbol:ne,pair:_e,contractType:je,interval:Q,priceType:fe,isDelivery:Ee,useMiniTicker:u,callback:function(dr){lr.push($e(dr))}});return function(){lr.close(),Or()}},[Ie,H,j,u]),Tr=(0,r.useCallback)((0,o._)(function(){var z;return(0,i._)(this,function(Q){switch(Q.label){case 0:return[4,K.fetchQuery(Wt.Bz.SERVER_TIME(s),function(){return(0,Gt.l)(s)},{})];case 1:return z=Q.sent(),[2,Math.floor(z/1e3)]}})}),[K,s]),_r=(0,r.useCallback)(function(z){return(0,cr._)({},z.template,(0,qt.sp)(s))},[s]),Cr=(0,r.useMemo)(function(){return ue()},[]),Er=Cr({config:Ge,onFetchSymbolsInfo:Xe,onFetchBars:He,onSubscribeBars:ur,onFetchServerTimeApi:Tr,onTransformSymbolInfo:_r}),we=Er.datafeed,Dr=Er.symbolsInfo;return(0,Ut.A)(function(){return Te()},[w]),{datafeed:we,symbolsInfo:Dr}},Pt="trd_chart_tv_load_timeout",hn=function(){var t=(0,We.PM)({id:"chartReady"}),n=(0,We.PM)({id:"getBarsStart",timeout:3}),a=(0,We.PM)({id:"getBarsEnd",timeout:6}),u=(0,We.PM)({id:"chartResponsive",timeout:3});We.HP.init({step:t._(n)._(a)._(u),eventId:Pt}),(0,We.HP)({eventId:Pt,stepId:"chartReady",catch:function(g){var S=g.error,K=g.track;K({CHART_READY_TYPE:S,connection:JSON.stringify(navigator.connection)})},log:function(g){var S=g.track;S({CHART_READY_TYPE:"SUCCESS",connection:JSON.stringify(navigator.connection)})}})},gn=function(t){(0,We.HP)({eventId:Pt,stepId:"chartResponsive",catch:function(a){var u=a.error,s=a.track;s({CHART_RESPONSIVE_TYPE:u,connection:JSON.stringify(navigator.connection)})},log:function(a){var u=a.track;u({CHART_RESPONSIVE_TYPE:"SUCCESS",duration_:(0,We.Pu)(Date.now()-t)})}})},Tn=[Qe.ZE,Qe.F3],Dt=function(t){var n=t.symbol,a=t.namespace,u=t.useMiniTicker,s=u===void 0?!1:u,g=c.y$(a),S=k.y$(a),K=(0,v._)(g(c.A8),1),Y=K[0],H=(0,v._)(g(c.Ir),2),j=H[0],$=H[1],f=(0,v._)(S(k.yj),1),l=f[0],T=(0,v._)(g(c.e6),1),W=T[0],_=(0,v._)(g(c._b),1),O=_[0],G=(0,he.ot)(),te=(0,pr.K7)(),J=te.pathname.includes(Ae.Q2),oe=te.pathname.includes(Ae.eq),Te=(0,r.useMemo)(function(){return J?(0,d._)(B.oT).concat(["hide_left_toolbar_by_default"]):B.oT},[J]),N=(0,se.Ye)(),w=(0,se.v3)({symbol:n}),re=(0,Fe.es)(n),ie=yn({namespace:a,useMiniTicker:s,isCM:re}),le=ie.datafeed,de=ie.symbolsInfo,Be=(0,Je.DP)(),ve=Be.theme,Ce=ve==="light"||ve==="light_glacier"?"light":"dark",ce=(0,yr.Z)(Ce),Ke=(0,r.useRef)(-1),Ze=Ht({symbol:n}).positionLines,Ge=Nt({symbol:n,namespace:a}).orderLines,Xe=ir({symbol:n}).executionOrders,Ie=pt({namespace:a,symbol:n}),He=(0,se.y0)(),ur=(0,r.useRef)(He),Tr=(0,r.useRef)(N),_r=(0,r.useRef)(l),Cr=(0,r.useRef)(J),Er=(0,r.useRef)(oe),we=(0,r.useRef)(de);(0,r.useEffect)(function(){we.current=de},[de]),(0,r.useEffect)(function(){ur.current=He},[He]),(0,r.useEffect)(function(){Tr.current=N},[N]),(0,r.useEffect)(function(){_r.current=l},[l]),(0,r.useEffect)(function(){Cr.current=J},[J]),(0,r.useEffect)(function(){Er.current=oe},[oe]),(0,Ve.a)(n,s),(0,sr.A)({symbol:n,interval:Y,priceType:j});var Dr=(0,r.useCallback)(function(){Ke.current=Date.now()},[]),z=(0,r.useCallback)((0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})()),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er=="string"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}),[a]),Q=(0,r.useMemo)(function(){return z},[z]),ee=(0,or.R)(),ae=(0,Ne.fQ)(a),Se=(0,Ne.m3)(a),xe=(0,Ne.S_)(a),ne=(0,Ne.Ds)(a),ke=(0,r.useRef)(n),Me=(0,r.useRef)(Y),fe=(0,r.useCallback)(function(){return ee("tradingView"),xe({panel_index:0,symbol:n,interval:Y}),hn()},[Y,ee,n,xe]),Pe=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be,vr,fr,er=(fr=(vr=(be=qe.symbolExt())===null||be===void 0?void 0:be.name)!==null&&vr!==void 0?vr:qe.symbol())!==null&&fr!==void 0?fr:"";ae({panel_index:ze,symbol:er,previous_symbol:ke.current}),ke.current=er},[ae]),_e=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be=qe.resolution();Se({panel_index:ze,interval:be,previous_interval:Me.current}),Me.current=be},[Se]),je=(0,r.useCallback)(function(){gn(Ke.current)},[]),Ee=(0,r.useCallback)(function(){},[]),me=(0,r.useMemo)(function(){return J?Ee:void 0},[Ee,J]),ye=(0,r.useCallback)((function(){var De=(0,o._)(function(ze){var qe,be,vr,fr,er,nr,mr,ar,Sr;return(0,i._)(this,function(Wr){switch(Wr.label){case 0:return qe=ze.chart,be=qe.symbolExt(),be?(vr=b()(we.current,function(Gr){return Gr.ticker===be.ticker||Gr.symbol===be.ticker}),fr=vr||{},er=fr.params,nr=er===void 0?{}:er,mr=nr.priceType,ar=mr===void 0?"":mr,ar&&$(ar),Sr=Tr.current[be.name],Sr?Er.current||Cr.current?[2]:[4,z()]:[2]):[2];case 1:return Wr.sent(),G({stateSymbol:Sr.symbol,isDeliveryColumn:Sr.isDelivery}),[2]}})});return function(ze){return De.apply(this,arguments)}})(),[G,z,$]),$e=qr(),Oe=$e.onCrosshairMoved,lr=$e.crosshairPrice,Or=wr({namespace:a,symbol:n,priceType:j,crosshairPrice:lr}),dr=lt(),Lr=(0,r.useMemo)(function(){return L({symbol:n,params:{priceType:j}})},[n,j]),Ur=(0,r.useMemo)(function(){return{symbol:Lr,theme:ce,limitOrders:Ge,positionOrders:Ze,executionOrders:Xe,labelLines:Ie,initialConfig:{tvConfig:{symbol:Lr,library_path:"/".concat(Ye.u_,"/trading-platform-30/"),theme:ce,datafeed:le,enabled_features:Te,context_menu:Or,custom_formatters:dr},onCrosshairMoved:Oe,onScriptLoaded:Dr,onSave:Q,onChartReadyDone:fe,onFirstTimeDataLoadedDone:je,onInitChart:me,onActiveChartChanged:ye,onSymbolChanged:Pe,onIntervalChanged:_e}}},[Lr,le,Te,Xe,Ie,Or,dr,Oe,ye,fe,je,me,_e,Q,Dr,Pe,Ge,Ze,ce]),Yr=Le({namespace:a,type:"tv"}),ct=(0,r.useCallback)(function(){ne({panel_index:0,symbol:n}),Yr()},[Yr,n,ne]);return(0,p.jsxs)(D.t,{name:"trading_view",onReset:ct,children:[(0,p.jsx)(Ue.A,{namespace:a,tradingViewProps:Ur}),w&&j==="index"&&O===c.Ev.Single&&W===c.tU.TradingView&&(0,p.jsx)(rr.B,{})]})};const Cn=Dt}'; + function createMirrorFactory(__dispatchChartMirror) { + if (typeof __dispatchChartMirror !== "function") throw new Error("Mirror dispatch must be a function"); + return { 70940(Z, x, e) { + "use strict"; + e.r(x), e.d(x, { TradingView: () => Dt, default: () => Cn }); + var o = e(49253), v = e(77135), d = e(75510), i = e(42288), p = e(31085), r = e(41594), M = e(43917), h = e.n(M), P = e(81936), b = e.n(P), V = e(7980), U = e.n(V), D = e(76469), c = e(4260), k = e(53837), B = e(27571), C = e(45250), y = e(87646), A = e.n(y), q = e(80817); + const E = (0, C.range)(1, 1380), m = (0, C.range)(1, 365).map((t) => `${t}D`), R = (0, C.range)(1, 52).map((t) => `${t}W`), I = (0, C.range)(1, 12).map((t) => `${t}M`), F = ["1S", ...E, ...m, ...R, ...I], L = ({ symbol: t, params: n }) => { + const a = new URLSearchParams(n); + return `${t}@${a.toString()}`; + }, X = (t) => { + const [n, a] = t.split("@"), s = new URLSearchParams(a).entries(), g = A()(Array.from(s)); + return { symbol: n, params: g }; + }, ue = () => { + const t = /* @__PURE__ */ new Map(); + let n = {}, a = [], u = { onReady: () => null, resolveSymbol: () => null, searchSymbols: () => null, getBars: () => null, subscribeBars: () => null, unsubscribeBars: () => null }; + return ({ config: s, onSymbolResolving: g, onSymbolResolved: S, onFetchSymbolsInfo: K, onFetchBars: Y, onSubscribeBars: H, onUnsubscribeBars: j, onFetchServerTimeApi: $, onTransformSymbolInfo: f = ({ template: T }) => T, onSearchSymbols: l = () => true }) => (u = { onReady: (T) => { + setTimeout(async () => { + const _ = (await K()).map((O) => ({ ...O, ticker: L({ symbol: O.symbol, params: O.params || {} }) })); + a = _.map(({ ticker: O, symbol: G, description: te = "", exchange: J = "", type: oe = "" }) => ({ symbol: G, ticker: O, description: te, exchange: J, type: oe })), n = (0, C.keyBy)(_, "ticker"), T({ exchanges: [], symbols_types: [], supported_resolutions: F, supports_marks: false, supports_timescale_marks: false, supports_time: true, ...s }); + }); + }, resolveSymbol: async (T, W, _) => { + const O = n[T]; + if (!O) { + _(`cannot resolve symbol - ${T}`); + return; + } + const G = f({ info: O, template: { description: O.description || "", fractional: false, has_seconds: true, seconds_multipliers: ["1"], has_intraday: true, intraday_multipliers: ["1", "3", "5", "15", "30", "60", "120", "240", "360", "480", "720"], has_daily: true, daily_multipliers: ["1"], has_weekly_and_monthly: true, weekly_multipliers: ["1"], monthly_multipliers: ["1"], minmov: 1, minmove2: 0, name: O.symbol, ticker: O.ticker, timezone: "Etc/UTC", pricescale: O.tickSize, session: "24x7", type: O.type || "", exchange: O.exchange || "", listed_exchange: O.exchange || "", format: "price", supported_resolutions: F, visible_plots_set: "ohlcv", volume_precision: 3 } }); + if (g) { + const te = X(G.ticker || ""); + if (!await g({ symbol: T, info: G, tickerInfo: te })) { + _(`symbol resolving error - ${T}`); + return; + } + } + setTimeout(() => { + W(G), S?.(G); + }); + }, searchSymbols: (T, W, _, O) => { + const G = new RegExp(T, "i"), te = new RegExp(W, "i"), J = new RegExp(_, "i"), oe = a.filter((Te) => { + const { symbol: N, ticker: w, description: re = "", exchange: ie = "", type: le = "" } = Te; + return [N, w, re].some((de) => G.test(de || "")) && te.test(ie) && J.test(le) && l?.({ item: Te }); + }); + O(oe); + }, getBars: async (T, W, _, O, G) => { + const { from: te, to: J, firstDataRequest: oe } = _, Te = (0, q.Cz)(W); + try { + if (J >= 0) { + const N = X(T.ticker || ""), w = await Y({ symbolInfo: T, resolution: W, rangeStartDate: te, rangeEndDate: J, tickerInfo: N, binanceInterval: Te, firstDataRequest: oe }); + O(w, { noData: w.length === 0 }); + } else O([], { noData: true }); + } catch (N) { + G(N.toString()); + } + }, subscribeBars: (T, W, _, O, G) => { + const te = X(T.ticker || ""), J = (0, q.Cz)(W), oe = H({ symbolInfo: T, resolution: W, callback: _, onResetCacheNeededCallback: G, tickerInfo: te, binanceInterval: J }); + t.set(O, oe); + }, unsubscribeBars: (T) => { + t?.get(T)?.(), t?.delete(T), j?.(T); + }, getServerTime: async (T) => { + try { + const W = await $(); + T(W); + } catch (W) { + console.warn(W), T(Date.now() / 1e3); + } + } }, { datafeed: u, observers: t, symbolsInfo: n }); + }, Le = ({ namespace: t, type: n }) => { + const a = (0, r.useMemo)(() => h().createInstance({ name: t }), [t]); + return (0, r.useCallback)(() => { + n === "tv" && a.removeItem("myTradingView"); + }, [a, n]); + }; + var Ue = e(76535), pr = e(26531), Ye = e(17409), Qe = e(94652), Ae = e(76830), he = e(48651), Fe = e(79344), se = e(51029), Je = e(36307), or = e(25379), yr = e(44745), Ne = e(15727), rr = e(45358), sr = e(82071), Ve = e(85411), ge = e(94889), hr = e(94547), tr = e(80686), Ar = e(61875); + function Fr(t) { + return (0, ge._)(t) || (0, hr._)(t) || (0, Ar._)(t) || (0, tr._)(); + } + var Nr = e(92873), zr = e(35630), Vr = e(12055), Qr = e(79515), Br = e(80737), Jr = e(40545), Zr = e(98107), Kr = e(51846), Xr = "Chart.Crosshair.PlusButton.DrawHorizontalLine", wr = function(t) { + var n = t.namespace, a = t.symbol, u = t.priceType, s = t.crosshairPrice, g = (0, Nr.o)("", "kline-ui").getI18n, S = (0, Vr.r)().format, K = k.y$(n), Y = (0, Br.S)({ namespace: n }), H = (0, Jr.hw)(), j = (0, Zr.c)(), $ = (0, Qr.nH)(), f = (0, se.Ye)(), l = (0, Kr.E)({ symbol: a }), T = (0, Fe.f0)(a), W = T.isCM, _ = (0, r.useRef)(s); + (0, r.useEffect)(function() { + _.current = s; + }, [s]); + var O = (0, r.useRef)(H); + (0, r.useEffect)(function() { + O.current = H; + }, [H]); + var G = (0, r.useRef)(j); + (0, r.useEffect)(function() { + G.current = j; + }, [j]); + var te = (0, r.useRef)(u); + (0, r.useEffect)(function() { + te.current = u; + }, [u]); + var J = (0, r.useRef)($); + (0, r.useEffect)(function() { + J.current = $; + }, [$]); + var oe = (0, r.useRef)(f); + (0, r.useEffect)(function() { + oe.current = f; + }, [f]); + var Te = (0, r.useRef)(l); + (0, r.useEffect)(function() { + Te.current = l; + }, [l]); + var N = (0, r.useRef)(W); + return (0, r.useEffect)(function() { + N.current = W; + }, [W]), { items_processor: (function() { + var w = (0, o._)(function(re, ie, le) { + var de, Be, ve, Ce, ce, Ke, Ze, Ge, Xe, Ie, He, ur, Tr, _r, Cr; + return (0, i._)(this, function(Er) { + if (Be = K.getState().tradingViewReference, !Be) return console.warn("[context-menu] trading view reference is not found"), [2, re]; + if (Be.chartsCount() > 1) return [2, re]; + if (ve = Be.activeChart().symbolExt(), !ve) return [2, re]; + switch (Ce = ve.name, ce = _.current, Ke = le.menuName === "CrosshairMenuView" ? "chart-trading" : "context-menu", Ze = { price: "".concat(ce.value), formattedPrice: ce.formatted, source: Ke, chartType: "tradingview" }, Ge = (de = oe.current[Ce]) === null || de === void 0 ? void 0 : de.baseAsset, Xe = g("trade", { defaultValue: "Trade" }), Ie = S(ce.formatted), He = G.current && te.current === c.SJ.Last && J.current && Ge ? [ie.createAction({ actionId: "Chart.PlaceOrder.BuyLimit", label: "".concat(Xe, " ").concat(Ge, " @ ").concat(Ie, " ").concat(g("limit", { defaultValue: "Limit" })), onExecute: function() { + return Te.current.onClickBuyLimit(Ze); + } }), ie.createAction({ actionId: "Chart.PlaceOrder.BuyStop", label: "".concat(Xe, " ").concat(Ge, " @ ").concat(Ie, " ").concat(g("stop", { defaultValue: "Stop" })), onExecute: function() { + return Te.current.onClickBuyStop(Ze); + } })] : [], ur = O.current ? ie.createAction({ actionId: "Chart.PriceAlert.CreateAtCrosshair", label: g("create-alert-at", { defaultValue: "Create Alert at {{price}}", price: ce.formatted }), onExecute: function() { + Y({ symbol: Ce, price: ce.formatted, mode: zr.n.CREATE }); + } }) : null, re.forEach(function(we) { + if (!(!("execute" in we) || !("getState" in we))) { + var Dr = we.getState(); + if (Dr.actionId === Xr) { + var z = we.execute.bind(we); + we.execute = function() { + (0, Kr.A)({ elementId: "draw_horizontal_line", isDelivery: N.current, chartType: "tradingview" }), z(); + }; + } + } + }), le.menuName) { + case "CrosshairMenuView": + return ur ? [2, [ur].concat((0, d._)(He), (0, d._)(re.slice()))] : [2, (0, d._)(He).concat((0, d._)(re.slice()))]; + case "ChartContextMenu": + return ur ? (Tr = Fr(re), _r = Tr[0], Cr = Tr.slice(1), [2, [_r, ur].concat((0, d._)(He), (0, d._)(Cr))]) : [2, (0, d._)(He).concat((0, d._)(re.slice()))]; + default: + return [2, re]; + } + return [2]; + }); + }); + return function(re, ie, le) { + return w.apply(this, arguments); + }; + })() }; + }, qr = function() { + var t = (0, r.useState)({ value: NaN, formatted: "" }), n = t[0], a = t[1], u = (0, r.useCallback)(function(s) { + var g = s.crosshairData, S = s.activeChart; + S && a({ value: g.price, formatted: S.priceFormatter().format(g.price) }); + }, []); + return { onCrosshairMoved: u, crosshairPrice: n }; + }; + const Hr = (t) => Math.floor(Math.log10(t)), et = (t) => { + const [n = "1"] = t.split(","); + return Hr(+n); + }, pe = ({ symbolInfo: t, minTick: n }) => { + const { pricescale: a = 2 } = t || {}; + return n === "default" || n === void 0 ? Hr(a) : et(n); + }, br = (t) => (n, a) => { + try { + return n ? { format: (u) => { + const s = pe({ symbolInfo: n, minTick: a }); + return t.format({ symbolInfo: n, minTick: a, value: u, precision: s }); + } } : null; + } catch { + return null; + } + }, Mr = 10 ** 12, rt = 10 ** 9, tt = 10 ** 6, jr = 10 ** 3, gr = (t) => { + const n = t.toString(), [, a = ""] = n.split("."); + return Math.min(a.length, 3); + }, nt = ({ value: t }) => { + const n = Math.abs(t); + if (n >= Mr) { + const a = t / Mr; + return { value: a, unit: "T", precision: gr(a) }; + } + if (n >= rt) { + const a = t / rt; + return { value: a, unit: "B", precision: gr(a) }; + } + if (n >= tt) { + const a = t / tt; + return { value: a, unit: "M", precision: gr(a) }; + } + if (n >= jr) { + const a = t / jr; + return { value: a, unit: "K", precision: gr(a) }; + } + return { value: t, unit: "", precision: gr(t) }; + }, ut = (t) => (n, a, u) => { + try { + return a && n.type === "volume" ? { format: (s) => s === void 0 ? "" : t.formatVolume({ format: n, symbolInfo: a, precision: u, value: s, volumeDetail: nt({ value: s }) }) } : null; + } catch { + return null; + } + }; + var lt = function() { + var t = (0, Vr.r)(), n = t.format, a = (0, r.useMemo)(function() { + return br({ format: function(s) { + var g = s.value, S = s.precision; + return n(g, { precision: S }); + } }); + }, [n]), u = (0, r.useMemo)(function() { + return ut({ formatVolume: function(s) { + var g = s.value, S = s.volumeDetail; + if (g === void 0 || !S) return ""; + var K = n(S.value, { precision: S.precision }); + return "".concat(K).concat(S.unit); + } }); + }, [n]); + return { priceFormatterFactory: a, studyFormatterFactory: u }; + }, dt = e(74069), ir = function(t) { + var n = (0, dt.K)(t), a = (0, r.useMemo)(function() { + return n.map(function(u) { + var s = u.id, g = u.time, S = u.isBuy, K = u.price, Y = u.tooltip; + return { id: s, text: "", price: K, quantity: "", time: Math.round(g / 1e3), isBuy: S, tooltip: Y }; + }); + }, [n]); + return { executionOrders: a }; + }, at = function(t) { + var n = t.namespace, a = t.symbol, u = (0, Br.j)({ namespace: n, symbol: a }); + return (0, r.useMemo)(function() { + return u.map(function(s) { + var g = s.id, S = s.price, K = s.formattedPrice, Y = s.sideText, H = s.showCloseButton, j = s.isPriceChangeable, $ = s.cancelTooltipText, f = s.onClick, l = s.onCancel, T = s.onPriceChanged; + return { id: g, price: +S, text: "".concat(Y, " ").concat(K), editable: j, cancellable: H, cancelTooltip: $, type: c.ND.PriceAlert, onCancel: H ? l : void 0, onMove: j ? function(W) { + var _ = W.price; + T({ currentPrice: _ }); + } : void 0, onClick: f }; + }); + }, [u]); + }, vt = e(90664), ot = function(t) { + var n = t.symbol, a = (0, vt.w)({ symbol: n }); + return (0, r.useMemo)(function() { + return a.map(function(u) { + var s = u.id, g = u.text, S = u.price; + return { id: s, text: g, price: +S, type: c.ND.BreakEvenPrice }; + }); + }, [a]); + }, cr = e(82508), ft = e(91025), mt = e(36077), Rr = function(t) { + var n = t.symbol, a = (0, mt.c)({ symbol: n }); + return (0, r.useMemo)(function() { + return a.map(function(u) { + return (0, ft._)((0, cr._)({}, u), { type: c.ND.LiquidationPrice }); + }); + }, [a]); + }, pt = function(t) { + var n = t.namespace, a = t.symbol, u = at({ namespace: n, symbol: a }), s = ot({ symbol: a }), g = Rr({ symbol: a }); + return (0, r.useMemo)(function() { + return (0, d._)(u).concat((0, d._)(s), (0, d._)(g)); + }, [u, s, g]); + }, st = e(42902), Ir = e(90170), yt = e(63345), ht = e(48210), Pr = e(51289), xr = e(24326), $r = e(90922), kr = e(34235), it = e(63762), Ft = function(t) { + var n = (0, Fe.f0)(t), a = n.pricePrecision, u = n.isCM, s = (0, st.yD)(t) || {}, g = s.lastPrice, S = g === void 0 ? "0" : g, K = (0, kr.Bp)(u), Y = (0, it.HY)(t), H = (0, kr.gv)(u), j = (0, kr.WF)(u); + return (0, r.useMemo)(function() { + return (0, $r.qF)({ orders: (0, d._)(K).concat((0, d._)(H), (0, d._)(j)), symbol: t }).map(function($) { + return Y({ order: $, lastPrice: S, pricePrecision: a }); + }); + }, [S, K, H, a, t, Y]); + }, gt = e(31542), Tt = function(t) { + var n = t.type; + (0, gt.u4)("webClick", { module: "chart", elementId: n }); + }, Nt = function(t) { + var n = t.symbol, a = t.namespace, u = (0, r.useState)(0), s = u[0], g = u[1], S = (0, r.useCallback)(function() { + return g(function(N) { + return N + 1; + }); + }, []), K = Ft(n), Y = (0, xr.IT)({ namespace: a }), H = (0, Fe.f0)(n), j = H.isDelivery, $ = H.contractType, f = (0, st.yD)(n) || {}, l = f.lastPrice, T = l === void 0 ? "0" : l, W = (0, yt.s)(n), _ = (0, it.YZ)({ symbol: n, afterConfirmationModalIsShown: S }), O = (0, it.u8)(n), G = (0, it.jn)(n), te = (0, r.useCallback)(function(N) { + return (0, ht.D)({ order: N, isDelivery: j, symbol: j ? N.symbol : "".concat(N.symbol, "_").concat($), module: "tradingview_kline" }); + }, [$, j]), J = (0, r.useMemo)(function() { + return K.map(function(N) { + var w = N.order, re = N.price, ie = N.quantity, le = N.quantityText, de = N.isBuy, Be = N.text, ve = N.priceTooltipText, Ce = N.pnlText, ce = (0, Pr.$L)(w), Ke = (0, Pr.OX)(w), Ze = (0, Pr.xL)(w), Ge = ce === "LIMIT", Xe = (0, Ir.Y)(w); + return { id: String(Ke), text: Be, price: +re, quantity: le, isBuy: de, editable: Xe && G, tooltip: ve, modifyTooltip: w.closePosition || !Ge ? "" : void 0, onMove: G ? function(Ie) { + var He = Ie.price; + te(w), Tt({ type: "TV_vertical" }), _({ order: w, price: +He, quantity: +Ze, lastPrice: T }); + } : void 0, onModify: w.closePosition || !Ge || !Xe ? void 0 : function() { + Tt({ type: "TV_amountclick" }), O({ order: w, price: re.toString(), quantity: ie, lastPrice: T }); + }, onCancel: function() { + Tt({ type: "TV_cancel" }), W(w); + } }; + }); + }, [G, K, T, te, _, O, W, s]), oe = (0, r.useMemo)(function() { + return Y.map(function(N) { + var w = N.id, re = N.value, ie = N.text, le = N.onChange, de = N.isBuy, Be = N.type; + return { id: w, price: +re, quantity: "", text: ie, isBuy: de, editable: true, limitOrderType: c.aA.OrderPreview, onMove: function(ve) { + var Ce = ve.price; + (0, xr.eP)({ namespace: a, orderType: Be }), le({ id: w, newPrice: Ce }); + }, onMoving: function(ve) { + var Ce = ve.price; + return le({ id: w, newPrice: Ce }); + } }; + }); + }, [Y, a]), Te = (0, r.useMemo)(function() { + return (0, d._)(J).concat((0, d._)(oe)); + }, [J, oe]); + return { orderLines: Te }; + }, Vt = e(17855), Bt = function(t, n) { + var a = (0, r.useState)(t()), u = a[0], s = a[1]; + return (0, Vt.A)(function() { + return s(t()); + }, n), u; + }, Kt = e(58656), Ht = function(t) { + var n = t.symbol, a = (0, Kt.Q)({ symbol: n }) || [], u = Bt(function() { + return a.map(function(s) { + var g = s.position, S = s.pnl, K = s.pnlInNumber, Y = s.pnlText, H = s.price, j = s.quantity, $ = s.reverseText, f = s.isBuy, l = s.displayReverseOrderModal, T = s.displayClosePositionModal; + return { id: g.id || "".concat(g.symbol, "-").concat(g.positionSide), PNL: K, text: "".concat(Y, " ").concat(S), price: H, quantity: j, tooltip: "", reverseTooltip: $, isBuy: f, onReverse: l, onClose: function() { + T(), (0, gt.u4)("$WebClick", { item: 1, module: "xxx", $element_id: "kline_close_position" }); + } }; + }); + }, [a]); + return { positionLines: u }; + }, jt = e(47354), bt = e.n(jt), $t = e(87017), Ut = e(99168), Yt = e(3742), Wt = e(43335), Gt = e(15540), zt = e(20163), Qt = e(47255); + const Jt = (t) => ({ endTime: n }) => (n ?? 0) === t; + var Zt = e(36488), Ct = e(48018), Xt = e(82911), We = e(69578), wt = e(53208), Rt = e(19862), qt = e(8874), en = e(26679), rn = e(66547), It = (function() { + var t = (0, o._)(function(n) { + var a, u, s, g, S, K, Y, H, j; + return (0, i._)(this, function($) { + switch ($.label) { + case 0: + a = n.onFetchKlineData, u = n.startTime, s = n.endTime, g = n.interval, S = n.limit, K = S === void 0 ? 1e3 : S, Y = [], H = [], j = s, $.label = 1; + case 1: + return [4, a((0, cr._)({ interval: g, limit: K }, j === 0 ? {} : { endTime: j }))]; + case 2: + if (H = $.sent(), H.length === 0) return [3, 4]; + H.forEach(function(f) { + var l = (0, v._)(f, 8), T = l[0], W = l[1], _ = l[2], O = l[3], G = l[4], te = l[5], J = l[7]; + Y.push({ time: +T, open: +W, high: +_, low: +O, close: +G, volume: +te, quantity: +J }); + }), j = H[0][0] - 1, $.label = 3; + case 3: + if (j > u) return [3, 1]; + $.label = 4; + case 4: + return Y.sort(function(f, l) { + return f.time - l.time; + }), [2, Y]; + } + }); + }); + return function(a) { + return t.apply(this, arguments); + }; + })(), tn = function(t) { + var n = t.firstDataRequest, a = t.hasRequested, u = t.rangeEndDate; + return n && !a ? 0 : u * 1e3; + }, nn = e(78142), an = e(16238), St = e.n(an), on = e(98087), xt = e.n(on), sn = e(55009), cn = e.n(sn), kt = e(56477); + const Et = (t) => cn()(t.chartsCount(), (n) => t.chart(n)), un = async (t) => { + try { + const { schema: n, data: a } = await t.exportData({ includedStudies: [] }), u = xt()(a)?.[n.findIndex((s) => s.type === "time")]; + return u !== void 0 && Number.isFinite(u) ? u * 1e3 : void 0; + } catch { + return; + } + }, ln = ({ getWidget: t, ...n }) => { + const a = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Map(), s = () => { + try { + const f = t(); + return f ? St()(Et(f).map((l) => l.symbolExt()?.ticker)) : []; + } catch { + return []; + } + }, g = (f) => { + const l = s(); + return !l.length || l.includes(f); + }, S = (f) => { + const l = u.get(f); + if (l) return l; + const T = { controller: new AbortController(), claimedAsMain: false }; + return u.set(f, T), T; + }, K = (f) => { + const l = u.get(f); + l && (l.controller.abort(), u.delete(f)); + }, Y = () => { + const f = s(); + f.length && Array.from(u.keys()).forEach((l) => { + const T = u.get(l); + if (T) { + if (f.includes(l)) { + T.claimedAsMain = true; + return; + } + a.has(l) || T.claimedAsMain && K(l); + } + }); + }, H = (f, l) => { + f.closed || l.time < f.highWater || (f.highWater = l.time, f.options.onTick(l), f.detector?.onBar(l), g(f.options.ticker) && $.onBar(l)); + }, j = async (f, l) => { + f.held = []; + let T = l; + try { + const _ = (await f.options.fetchSince(l)).filter((O) => O.time >= l && !f.closed); + _.forEach((O) => { + f.highWater = Math.max(f.highWater, O.time), f.options.onTick(O); + }), T = xt()(_)?.time ?? l; + } finally { + const W = f.held ?? []; + f.held = void 0, W.filter((_) => _.time >= T).forEach((_) => { + H(f, _); + }); + } + }, $ = (0, kt.k)({ ...n, onCheck: Y, repair: async () => { + const f = t(); + if (f) try { + const l = Et(f), T = St()(l.map((_) => { + const O = a.get(_.symbolExt()?.ticker ?? ""); + return O && { chart: _, feed: O }; + })); + if (!T.length) { + f.resetCache(), l.forEach((_) => { + _.resetData(); + }); + return; + } + const W = St()(await Promise.all(T.map(async ({ chart: _, feed: O }) => { + const G = await un(_); + return G === void 0 ? void 0 : { feed: O, fromMs: G }; + }))); + await Promise.allSettled(W.map(({ feed: _, fromMs: O }) => j(_, O))); + } catch { + } + } }); + return { openFeed: (f) => { + const l = { options: f, held: void 0, highWater: 0, closed: false, detector: void 0 }; + return l.detector = (0, kt.k)({ ...n, subscribeSignals: f.subscribeSignals, repair: async () => { + l.closed || !l.highWater || g(f.ticker) || await j(l, l.highWater); + } }), a.set(f.ticker, l), g(f.ticker) && $.resubscribeSignals(f.subscribeSignals), { push: (T) => { + l.held ? l.held.push(T) : H(l, T); + }, close: () => { + l.closed = true, l.detector?.dispose(), a.get(f.ticker) === l && a.delete(f.ticker); + } }; + }, subscribeWake: $.subscribeWake, getSignal: (f) => { + const l = S(f); + return s().includes(f) && (l.claimedAsMain = true), l.controller.signal; + }, dispose: () => { + u.forEach(({ controller: f }) => { + f.abort(); + }), u.clear(), a.forEach((f) => { + f.detector?.dispose(); + }), a.clear(), $.dispose(); + } }; + }; + var dn = e(71730), vn = e(36620), fn = function(t) { + var n = t.namespace, a = k.y$(n), u = (0, vn.l)().getSocketSignals, s = (0, r.useRef)(); + (0, r.useEffect)(function() { + return function() { + var S; + (S = s.current) === null || S === void 0 || S.instance.dispose(), s.current = void 0; + }; + }, []); + var g = (0, r.useCallback)(function(S) { + var K, Y, H = S; + if (((K = s.current) === null || K === void 0 ? void 0 : K.key) === H) return s.current.instance; + (Y = s.current) === null || Y === void 0 || Y.instance.dispose(); + var j = nn.wr(S), $ = ln({ getWidget: function() { + return a.getState().tradingViewReference; + }, checkPeriodMs: dn.D(j) }); + return s.current = { key: H, instance: $ }, $; + }, [a]); + return (0, r.useMemo)(function() { + return { ensureRecovery: g, getSocketSignals: u }; + }, [g, u]); + }, mn = "tv_web_error", pn = [Ye.bt.TRADING, Ye.bt.DELIVERING], Re; + (function(t) { + t.Futures = "futures", t.FuturesMark = "futures-mark", t.FuturesIndex = "futures-index", t.Delivery = "delivery", t.DeliveryMark = "delivery-mark", t.DeliveryIndex = "delivery-index"; + })(Re || (Re = {})); + var Mt = function(t) { + var n = t.symbol, a = t.description, u = t.productType, s = t.pricePrecision, g = t.params, S = t.supportedResolutions, K = S === void 0 ? F : S; + return { symbol: n, description: a, tickSize: Math.pow(10, s), exchange: "Binance", supportedResolutions: K, type: u, params: g }; + }, yn = function(t) { + var n = t.namespace, a = t.useMiniTicker, u = a === void 0 ? false : a, s = t.isCM, g = c.y$(n), S = k.y$(n), K = (0, $t.useQueryClient)(), Y = fn({ namespace: n }), H = Y.ensureRecovery, j = Y.getSocketSignals, $ = (0, r.useRef)(0), f = (0, r.useRef)(false), l = (0, r.useMemo)(function() { + return bt()(function() { + return (0, We.HP)({ eventId: "trd_chart_tv_load_timeout", stepId: "getBarsStart", catch: function(Q) { + var ee = Q.error, ae = Q.track; + ae({ GET_BAR_START_TYPE: ee }); + }, log: function(Q) { + var ee = Q.track; + ee({ GET_BAR_START_TYPE: "SUCCESS" }); + } }); + }); + }, []), T = (0, r.useMemo)(function() { + return bt()(function(z) { + return (0, We.HP)({ eventId: "trd_chart_tv_load_timeout", stepId: "getBarsEnd", catch: function(ee) { + var ae = ee.error, Se = ee.track; + Se({ GET_BAR_END_TYPE: ae, duration_: (0, We.Pu)(z - $.current) }); + }, log: function(ee) { + var ae = ee.track; + ae({ GET_BAR_END_TYPE: "SUCCESS", duration_: (0, We.Pu)(z - $.current) }); + } }); + }); + }, []), W = (0, Nr.o)("trd-chart", "trade-ui"), _ = W.getI18n, O = (0, he.ud)(), G = O.getI18n, te = (0, v._)(g(c.Ir), 1), J = te[0], oe = (0, v._)(S(k.A7), 2), Te = oe[1], N = (0, se.Ye)(), w = (0, Zt.JD)(s).isContUnit, re = (0, se.y_)(), ie = (0, r.useRef)(N), le = (0, r.useRef)(J), de = (0, r.useRef)(re); + (0, r.useEffect)(function() { + ie.current = N; + }, [N]), (0, r.useEffect)(function() { + le.current = J; + }, [J]), (0, r.useEffect)(function() { + de.current = re; + }, [re]); + var Be = (0, r.useMemo)(function() { + return w ? Rt.b : Rt.S; + }, [w]), ve = (0, r.useMemo)(function() { + return G("usdtFutures-13", { defaultValue: "Futures" }) || ""; + }, [G]), Ce = (0, r.useMemo)(function() { + return G("inverseFutures-13", { defaultValue: "Delivery" }) || ""; + }, [G]), ce = (0, r.useMemo)(function() { + return _("indexPrice", { defaultValue: "Index Price" }) || ""; + }, [_]), Ke = (0, r.useMemo)(function() { + return _("markPrice", { defaultValue: "Mark Price" }) || ""; + }, [_]), Ze = (0, r.useMemo)(function() { + return _("lastPrice", { defaultValue: "Last Price" }) || ""; + }, [_]), Ge = (0, r.useMemo)(function() { + return { exchanges: [{ name: "Binance", value: "Binance", desc: "" }], symbols_types: [{ name: ve, value: Re.Futures }, { name: "".concat(ve, " ").concat(Ke), value: Re.FuturesMark }, { name: "".concat(ve, " ").concat(ce), value: Re.FuturesIndex }, { name: Ce, value: Re.Delivery }, { name: "".concat(Ce, " ").concat(Ke), value: Re.DeliveryMark }, { name: "".concat(Ce, " ").concat(ce), value: Re.DeliveryIndex }] }; + }, [Ce, ve, ce, Ke]), Xe = (0, r.useCallback)((0, o._)(function() { + var z, Q, ee; + return (0, i._)(this, function(ae) { + switch (ae.label) { + case 0: + return ae.trys.push([0, 2, , 3]), [4, (0, en.c)(K)]; + case 1: + return ae.sent(), [3, 3]; + case 2: + return z = ae.sent(), (0, gt.u4)(mn, { type: "onFetchSymbolsInfo", message: z.message, business: "futures" }), [3, 3]; + case 3: + return Q = ie.current, ee = Object.keys(Q).reduce(function(Se, xe) { + var ne = Q[xe] || Ct.o, ke = ne.symbol, Me = ne.pricePrecision, fe = ne.isDelivery, Pe = ne.contractStatus; + if (!pn.includes(Pe)) return Se; + var _e = (0, Yt.S)(ne, G), je = _e.symbol, Ee = _e.contractTypeI18n, me = _e.deliveryDateFormat, ye = "".concat(je, " ").concat(Ee).concat(me ? " ".concat(me) : ""), $e = "".concat(ye, " ").concat(Ze), Oe = "".concat(ye, " ").concat(ce), lr = "".concat(ye, " ").concat(Ke); + return Se.concat([Mt({ symbol: ke, description: $e, productType: fe ? Re.Delivery : Re.Futures, pricePrecision: Me, params: { priceType: c.SJ.Last } }), Mt({ symbol: ke, description: Oe, productType: fe ? Re.DeliveryIndex : Re.FuturesIndex, pricePrecision: Me, params: { priceType: c.SJ.Index } }), Mt({ symbol: ke, description: lr, productType: fe ? Re.DeliveryMark : Re.FuturesMark, pricePrecision: (0, Xt.X)(Me), params: { priceType: c.SJ.Mark } })]); + }, []), [2, ee]; + } + }); + }), [G, ce, Ke, Ze, K]), Ie = (0, r.useCallback)(function(z) { + var Q = z.ticker, ee = z.symbol, ae = z.interval, Se = z.priceType, xe = z.retryUntil, ne = ie.current[ee] || Ct.o, ke = ne.pair, Me = ne.contractType, fe = ne.contractVal, Pe = ne.isDelivery, _e = Pe ? Be(fe) : function(Oe) { + return Oe; + }, je = (0, wt.h)({ symbol: ee, pair: ke, contractType: Me, isDelivery: Pe, priceType: Se }), Ee, me, ye = function(Oe) { + return zt.y("tv", "".concat(Pe ? "cm" : "um", "|").concat(ee, "|").concat(Se, "|").concat(Oe.interval, "|").concat((Ee = Oe.limit) !== null && Ee !== void 0 ? Ee : "", "|").concat((me = Oe.endTime) !== null && me !== void 0 ? me : "")); + }; + if (xe === void 0) return { transform: _e, onFetchKlineData: je }; + var $e = H(ae); + return { transform: _e, onFetchKlineData: Qt.b(je, { enabled: Jt(xe), key: ye, signal: $e.getSignal(Q), subscribeWake: $e.subscribeWake }) }; + }, [H, Be]), He = (0, r.useCallback)((function() { + var z = (0, o._)(function(Q) { + var ee, ae, Se, xe, ne, ke, Me, fe, Pe, _e, je, Ee, me, ye, $e, Oe, lr, Or; + return (0, i._)(this, function(dr) { + switch (dr.label) { + case 0: + return ee = Q.binanceInterval, ae = Q.rangeStartDate, Se = Q.rangeEndDate, xe = Q.tickerInfo, ne = Q.symbolInfo, ke = Q.firstDataRequest, $.current = Date.now(), l(), Me = xe.symbol, fe = Me === void 0 ? "" : Me, Pe = xe.params, _e = Pe.priceType, je = _e === void 0 ? le.current : _e, de.current({ symbol: fe, priceType: je }) ? (T(Date.now()), [2, []]) : (Ee = Math.max(0, ae * 1e3), me = tn({ firstDataRequest: ke, hasRequested: f.current, rangeEndDate: Se }), f.current = true, $e = Ie({ ticker: (ye = ne.ticker) !== null && ye !== void 0 ? ye : "", symbol: fe, interval: ee, priceType: je, retryUntil: ke ? me : void 0 }), Oe = $e.transform, lr = $e.onFetchKlineData, [4, It({ onFetchKlineData: lr, startTime: Ee, endTime: me, interval: ee })]); + case 1: + return Or = dr.sent(), T(Date.now()), [2, Or.map(Oe)]; + } + }); + }); + return function(Q) { + return z.apply(this, arguments); + }; + })(), [Ie, T, l]), ur = (0, r.useCallback)(function(z) { + var Q = z.binanceInterval, ee = z.callback, ae = z.tickerInfo, Se = z.symbolInfo, xe = ae.symbol, ne = xe === void 0 ? "" : xe, ke = ae.params, Me = ke.priceType, fe = Me === void 0 ? le.current : Me; + if (de.current({ symbol: ne, priceType: fe })) return function() { + return null; + }; + var Pe = ie.current[ne] || Ct.o, _e = Pe.pair, je = Pe.contractType, Ee = Pe.isDelivery, me, ye = (me = Se.ticker) !== null && me !== void 0 ? me : "", $e = Ie({ ticker: ye, symbol: ne, interval: Q, priceType: fe }).transform, Oe = H(Q), lr = Oe.openFeed({ ticker: ye, onTick: ee, subscribeSignals: j(Ee), fetchSince: (function() { + var dr = (0, o._)(function(Lr) { + var Ur, Yr, ct, De; + return (0, i._)(this, function(ze) { + switch (ze.label) { + case 0: + return Ur = Ie({ ticker: ye, symbol: ne, interval: Q, priceType: fe, retryUntil: 0 }), Yr = Ur.transform, ct = Ur.onFetchKlineData, [4, It({ onFetchKlineData: ct, startTime: Lr, endTime: 0, interval: Q })]; + case 1: + return De = ze.sent(), [2, De.map(Yr)]; + } + }); + }); + return function(Lr) { + return dr.apply(this, arguments); + }; + })() }), Or = (0, rn.B)({ symbol: ne, pair: _e, contractType: je, interval: Q, priceType: fe, isDelivery: Ee, useMiniTicker: u, callback: function(dr) { + lr.push($e(dr)); + } }); + return function() { + lr.close(), Or(); + }; + }, [Ie, H, j, u]), Tr = (0, r.useCallback)((0, o._)(function() { + var z; + return (0, i._)(this, function(Q) { + switch (Q.label) { + case 0: + return [4, K.fetchQuery(Wt.Bz.SERVER_TIME(s), function() { + return (0, Gt.l)(s); + }, {})]; + case 1: + return z = Q.sent(), [2, Math.floor(z / 1e3)]; + } + }); + }), [K, s]), _r = (0, r.useCallback)(function(z) { + return (0, cr._)({}, z.template, (0, qt.sp)(s)); + }, [s]), Cr = (0, r.useMemo)(function() { + return ue(); + }, []), Er = Cr({ config: Ge, onFetchSymbolsInfo: Xe, onFetchBars: He, onSubscribeBars: ur, onFetchServerTimeApi: Tr, onTransformSymbolInfo: _r }), we = Er.datafeed, Dr = Er.symbolsInfo; + return (0, Ut.A)(function() { + return Te(); + }, [w]), { datafeed: we, symbolsInfo: Dr }; + }, Pt = "trd_chart_tv_load_timeout", hn = function() { + var t = (0, We.PM)({ id: "chartReady" }), n = (0, We.PM)({ id: "getBarsStart", timeout: 3 }), a = (0, We.PM)({ id: "getBarsEnd", timeout: 6 }), u = (0, We.PM)({ id: "chartResponsive", timeout: 3 }); + We.HP.init({ step: t._(n)._(a)._(u), eventId: Pt }), (0, We.HP)({ eventId: Pt, stepId: "chartReady", catch: function(g) { + var S = g.error, K = g.track; + K({ CHART_READY_TYPE: S, connection: JSON.stringify(navigator.connection) }); + }, log: function(g) { + var S = g.track; + S({ CHART_READY_TYPE: "SUCCESS", connection: JSON.stringify(navigator.connection) }); + } }); + }, gn = function(t) { + (0, We.HP)({ eventId: Pt, stepId: "chartResponsive", catch: function(a) { + var u = a.error, s = a.track; + s({ CHART_RESPONSIVE_TYPE: u, connection: JSON.stringify(navigator.connection) }); + }, log: function(a) { + var u = a.track; + u({ CHART_RESPONSIVE_TYPE: "SUCCESS", duration_: (0, We.Pu)(Date.now() - t) }); + } }); + }, Tn = [Qe.ZE, Qe.F3], Dt = function(t) { + var n = t.symbol, a = t.namespace, u = t.useMiniTicker, s = u === void 0 ? false : u, g = c.y$(a), S = k.y$(a), K = (0, v._)(g(c.A8), 1), Y = K[0], H = (0, v._)(g(c.Ir), 2), j = H[0], $ = H[1], f = (0, v._)(S(k.yj), 1), l = f[0], T = (0, v._)(g(c.e6), 1), W = T[0], _ = (0, v._)(g(c._b), 1), O = _[0], G = (0, he.ot)(), te = (0, pr.K7)(), J = te.pathname.includes(Ae.Q2), oe = te.pathname.includes(Ae.eq), Te = (0, r.useMemo)(function() { + return J ? (0, d._)(B.oT).concat(["hide_left_toolbar_by_default"]) : B.oT; + }, [J]), N = (0, se.Ye)(), w = (0, se.v3)({ symbol: n }), re = (0, Fe.es)(n), ie = yn({ namespace: a, useMiniTicker: s, isCM: re }), le = ie.datafeed, de = ie.symbolsInfo, Be = (0, Je.DP)(), ve = Be.theme, Ce = ve === "light" || ve === "light_glacier" ? "light" : "dark", ce = (0, yr.Z)(Ce), Ke = (0, r.useRef)(-1), Ze = Ht({ symbol: n }).positionLines, Ge = Nt({ symbol: n, namespace: a }).orderLines, Xe = ir({ symbol: n }).executionOrders, Ie = pt({ namespace: a, symbol: n }), He = (0, se.y0)(), ur = (0, r.useRef)(He), Tr = (0, r.useRef)(N), _r = (0, r.useRef)(l), Cr = (0, r.useRef)(J), Er = (0, r.useRef)(oe), we = (0, r.useRef)(de); + (0, r.useEffect)(function() { + we.current = de; + }, [de]), (0, r.useEffect)(function() { + ur.current = He; + }, [He]), (0, r.useEffect)(function() { + Tr.current = N; + }, [N]), (0, r.useEffect)(function() { + _r.current = l; + }, [l]), (0, r.useEffect)(function() { + Cr.current = J; + }, [J]), (0, r.useEffect)(function() { + Er.current = oe; + }, [oe]), (0, Ve.a)(n, s), (0, sr.A)({ symbol: n, interval: Y, priceType: j }); + var Dr = (0, r.useCallback)(function() { + Ke.current = Date.now(); + }, []), z = (0, r.useCallback)((0, o._)(function() { + var De, ze, qe, be, vr, fr, er; + return (0, i._)(this, function(nr) { + switch (nr.label) { + case 0: + if (Cr.current || !_r.current) return [2, Promise.resolve()]; + nr.label = 1; + case 1: + return nr.trys.push([1, 5, , 6]), De = h().createInstance({ name: a }), [4, De.keys()]; + case 2: + return ze = nr.sent(), qe = ze.filter(function(mr) { + return /^((#TV_SYMBOL-)|(myTradingView))/g.test(mr); + }), be = qe.map((function() { + var mr = (0, o._)(function(ar) { + return (0, i._)(this, function(Sr) { + return [2, De.getItem(ar)]; + }); + }); + return function(ar) { + return mr.apply(this, arguments); + }; + })()), [4, Promise.all(be)]; + case 3: + return vr = nr.sent(), fr = U()(qe, vr), [4, Promise.all(Tn.map((function() { + var mr = (0, o._)(function(ar) { + var Sr, Wr; + return (0, i._)(this, function(Gr) { + switch (Gr.label) { + case 0: + return a === ar ? [2, Promise.resolve()] : (Sr = h().createInstance({ name: ar }), Wr = __dispatchChartMirror(Sr, fr, () => fr.map((function() { + var Sn = (0, o._)(function(Lt) { + var _t, Ot, At; + return (0, i._)(this, function(Mn) { + return _t = (0, v._)(Lt, 2), Ot = _t[0], At = _t[1], Ot ? [2, Sr.setItem(Ot, At)] : [2, Promise.resolve()]; + }); + }); + return function(Lt) { + return Sn.apply(this, arguments); + }; + })())), [4, Promise.all(Wr)]); + case 1: + return Gr.sent(), [2, Promise.resolve()]; + } + }); + }); + return function(ar) { + return mr.apply(this, arguments); + }; + })()))]; + case 4: + return nr.sent(), [2, Promise.resolve()]; + case 5: + return er = nr.sent(), console.warn(typeof er == "string" ? er : er.message), [2, Promise.resolve()]; + case 6: + return [2]; + } + }); + }), [a]), Q = (0, r.useMemo)(function() { + return z; + }, [z]), ee = (0, or.R)(), ae = (0, Ne.fQ)(a), Se = (0, Ne.m3)(a), xe = (0, Ne.S_)(a), ne = (0, Ne.Ds)(a), ke = (0, r.useRef)(n), Me = (0, r.useRef)(Y), fe = (0, r.useCallback)(function() { + return ee("tradingView"), xe({ panel_index: 0, symbol: n, interval: Y }), hn(); + }, [Y, ee, n, xe]), Pe = (0, r.useCallback)(function(De) { + var ze = De.index, qe = De.chart, be, vr, fr, er = (fr = (vr = (be = qe.symbolExt()) === null || be === void 0 ? void 0 : be.name) !== null && vr !== void 0 ? vr : qe.symbol()) !== null && fr !== void 0 ? fr : ""; + ae({ panel_index: ze, symbol: er, previous_symbol: ke.current }), ke.current = er; + }, [ae]), _e = (0, r.useCallback)(function(De) { + var ze = De.index, qe = De.chart, be = qe.resolution(); + Se({ panel_index: ze, interval: be, previous_interval: Me.current }), Me.current = be; + }, [Se]), je = (0, r.useCallback)(function() { + gn(Ke.current); + }, []), Ee = (0, r.useCallback)(function() { + }, []), me = (0, r.useMemo)(function() { + return J ? Ee : void 0; + }, [Ee, J]), ye = (0, r.useCallback)((function() { + var De = (0, o._)(function(ze) { + var qe, be, vr, fr, er, nr, mr, ar, Sr; + return (0, i._)(this, function(Wr) { + switch (Wr.label) { + case 0: + return qe = ze.chart, be = qe.symbolExt(), be ? (vr = b()(we.current, function(Gr) { + return Gr.ticker === be.ticker || Gr.symbol === be.ticker; + }), fr = vr || {}, er = fr.params, nr = er === void 0 ? {} : er, mr = nr.priceType, ar = mr === void 0 ? "" : mr, ar && $(ar), Sr = Tr.current[be.name], Sr ? Er.current || Cr.current ? [2] : [4, z()] : [2]) : [2]; + case 1: + return Wr.sent(), G({ stateSymbol: Sr.symbol, isDeliveryColumn: Sr.isDelivery }), [2]; + } + }); + }); + return function(ze) { + return De.apply(this, arguments); + }; + })(), [G, z, $]), $e = qr(), Oe = $e.onCrosshairMoved, lr = $e.crosshairPrice, Or = wr({ namespace: a, symbol: n, priceType: j, crosshairPrice: lr }), dr = lt(), Lr = (0, r.useMemo)(function() { + return L({ symbol: n, params: { priceType: j } }); + }, [n, j]), Ur = (0, r.useMemo)(function() { + return { symbol: Lr, theme: ce, limitOrders: Ge, positionOrders: Ze, executionOrders: Xe, labelLines: Ie, initialConfig: { tvConfig: { symbol: Lr, library_path: "/".concat(Ye.u_, "/trading-platform-30/"), theme: ce, datafeed: le, enabled_features: Te, context_menu: Or, custom_formatters: dr }, onCrosshairMoved: Oe, onScriptLoaded: Dr, onSave: Q, onChartReadyDone: fe, onFirstTimeDataLoadedDone: je, onInitChart: me, onActiveChartChanged: ye, onSymbolChanged: Pe, onIntervalChanged: _e } }; + }, [Lr, le, Te, Xe, Ie, Or, dr, Oe, ye, fe, je, me, _e, Q, Dr, Pe, Ge, Ze, ce]), Yr = Le({ namespace: a, type: "tv" }), ct = (0, r.useCallback)(function() { + ne({ panel_index: 0, symbol: n }), Yr(); + }, [Yr, n, ne]); + return (0, p.jsxs)(D.t, { name: "trading_view", onReset: ct, children: [(0, p.jsx)(Ue.A, { namespace: a, tradingViewProps: Ur }), w && j === "index" && O === c.Ev.Single && W === c.tU.TradingView && (0, p.jsx)(rr.B, {})] }); + }; + const Cn = Dt; + } }[70940]; + } + function replaceChartMirrorFactory(originalFactory, dispatch) { + if (typeof originalFactory !== "function" || Function.prototype.toString.call(originalFactory) !== originalFactorySource) { + throw new Error("Mirror factory source does not match the pinned public module"); + } + return createMirrorFactory(dispatch); + } + + // experiments/binance-chart-storage/native-preflight-core.js + var INCIDENT_URL = "https://www.binance.com/zh-CN/futures/USUSDT"; + var OBSERVATION_MS = 3e4; + function isIncidentPage() { + return location.href === INCIDENT_URL && self === top; + } + function startNativeMirrorPreflight() { + const state = { + active: true, + outcome: "waiting", + stopReason: null, + failure: null, + attempts: 0, + matches: 0, + executions: 0, + completed: false, + dispatches: 0, + entryCount: 0 + }; + let observer; + let timer; + function snapshot() { + return { ...state, failure: state.failure === null ? null : { ...state.failure } }; + } + function retire(reason) { + if (!state.active) return snapshot(); + state.active = false; + state.stopReason = reason; + clearTimeout(timer); + self.removeEventListener("pagehide", onPageHide); + if (observer) observer.stop(); + return snapshot(); + } + function fail(code) { + state.failure = { name: "PreflightError", code }; + state.outcome = "failed"; + retire("failure"); + } + function onPageHide() { + retire("pagehide"); + } + function dispatch(_target, entries, nativeThunk) { + if (state.active) { + if (!isIncidentPage()) retire("scope_changed"); + else { + try { + const count = entries.length; + if (state.active) { + state.dispatches += 1; + state.entryCount += count; + } + } catch { + fail("entry_count_unavailable"); + } + } + } + return nativeThunk(); + } + const session = Object.freeze({ snapshot, stop: () => retire("manual"), dispatch }); + if (!isIncidentPage()) { + fail("unsupported_page"); + return session; + } + try { + observer = observeChartStorageBootstrap({ + replaceMirrorFactory(original) { + if (!isIncidentPage()) { + retire("scope_changed"); + return original; + } + state.attempts += 1; + let factory; + try { + factory = replaceChartMirrorFactory(original, dispatch); + } catch { + fail("source_mismatch"); + return original; + } + state.matches += 1; + return function(...args) { + if (state.active) { + if (!isIncidentPage()) retire("scope_changed"); + else state.executions += 1; + } + try { + return Reflect.apply(factory, this, args); + } catch (error) { + if (state.active) fail("module_execution_failed"); + throw error; + } + }; + }, + onCapture() { + if (!state.active) return; + if (!isIncidentPage()) retire("scope_changed"); + else { + state.completed = true; + state.outcome = "captured"; + } + } + }); + } catch { + fail("bootstrap_rejected"); + return session; + } + observer.captured.catch(() => { + if (state.active) fail("observation_failed"); + }); + self.addEventListener("pagehide", onPageHide, { once: true }); + timer = setTimeout(() => retire("deadline"), OBSERVATION_MS); + return session; + } + + // experiments/binance-chart-storage/native-preflight-entry.user.js + (function installNativeMirrorPreflight() { + if (Object.hasOwn(self, "__BINANCE_MIRROR_PREFLIGHT__")) return; + const { snapshot, stop } = startNativeMirrorPreflight(); + Object.defineProperty(self, "__BINANCE_MIRROR_PREFLIGHT__", { + value: Object.freeze({ snapshot, stop }), + configurable: true + }); + })(); +})(); diff --git a/experiments/binance-chart-storage/preflight.js b/experiments/binance-chart-storage/preflight.js new file mode 100644 index 0000000..66bd6d0 --- /dev/null +++ b/experiments/binance-chart-storage/preflight.js @@ -0,0 +1,90 @@ +import { observeChartStorageBootstrap } from './bootstrap.js'; + +/** + * Bounded startup observation. Native instances, storage methods and results are + * untouched. Only two fixed chart database names and the default store are reported. + */ +export function startChartStoragePreflight() { + const state = { + outcome: 'waiting', active: true, captureAtMs: null, failure: null, + instances: { chart_futures: 0, chart_delivery: 0 }, firstInstances: [], + }; + let localforage; + let originalFactory; + let originalDescriptor; + let observedFactory; + let timer; + let observer; + + function snapshot() { + return { + ...state, instances: { ...state.instances }, + firstInstances: state.firstInstances.map(instance => ({ ...instance })), + }; + } + + function stop() { + if (!state.active) return snapshot(); + state.active = false; + clearTimeout(timer); + self.removeEventListener('pagehide', stop); + if (localforage && observedFactory) { + const descriptor = Object.getOwnPropertyDescriptor(localforage, 'createInstance'); + if (descriptor?.value !== observedFactory || !descriptor.configurable) { + state.outcome = 'failed'; + state.failure = 'factory_ownership_changed'; + } else if (originalDescriptor) { + Object.defineProperty(localforage, 'createInstance', originalDescriptor); + } else { + delete localforage.createInstance; + } + } + observer.stop(); + return snapshot(); + } + + observer = observeChartStorageBootstrap({ + onCapture(actualExport) { + localforage = actualExport; + originalDescriptor = Object.getOwnPropertyDescriptor(actualExport, 'createInstance'); + originalFactory = actualExport.createInstance; + state.captureAtMs = performance.now(); + observedFactory = function (...args) { + const instance = Reflect.apply(originalFactory, this, args); + if (state.active) { + try { + const name = instance._config.name; + if (Object.hasOwn(state.instances, name)) { + state.instances[name] += 1; + if (state.firstInstances.length < 16) { + state.firstInstances.push({ + name, storeName: instance._config.storeName === 'keyvaluepairs' ? 'keyvaluepairs' : null, + atMs: performance.now(), + }); + } + } + } catch { + // Metadata failure retires the observer while returning the untouched native instance. + state.outcome = 'failed'; + state.failure = 'instance_metadata_unavailable'; + stop(); + } + } + return instance; + }; + Object.defineProperty(actualExport, 'createInstance', { + value: observedFactory, writable: true, configurable: true, enumerable: true, + }); + state.outcome = 'captured'; + }, + }); + observer.captured.catch(() => { + if (!state.active) return; + state.outcome = 'failed'; + state.failure = 'bootstrap_rejected'; + stop(); + }); + self.addEventListener('pagehide', stop, { once: true }); + timer = setTimeout(stop, 30_000); + return Object.freeze({ snapshot, stop }); +} diff --git a/experiments/binance-chart-storage/run-anonymous-probe.js b/experiments/binance-chart-storage/run-anonymous-probe.js new file mode 100644 index 0000000..16969ea --- /dev/null +++ b/experiments/binance-chart-storage/run-anonymous-probe.js @@ -0,0 +1,139 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { chromium } from 'playwright'; +import { build } from 'esbuild'; + +const target = 'https://www.binance.com/zh-CN/futures/USUSDT'; +const directory = await mkdtemp(join(tmpdir(), 'binance-mirror-bulk-probe-')); +const report = { startedAt: new Date().toISOString(), environment: 'new anonymous Chrome context, no storageState or extensions', stages: [], browserClosed: false, pageErrors: 0 }; +const bundle = await build({ entryPoints: [new URL('anonymous-probe-entry.js', import.meta.url).pathname], bundle: true, + write: false, format: 'iife', target: 'es2022', minify: false }); +await writeFile(join(directory, 'injection.js'), bundle.outputFiles[0].text); +let browser; +let page; +let stage = 'launch'; + +/** Cleanup has a deadline because accepted IndexedDB work can remain blocked. */ +async function bounded(operation) { + let timer; + try { + return await Promise.race([operation, new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error('Probe operation deadline exceeded')), 10000); + })]); + } finally { + clearTimeout(timer); + } +} + +function validateStats(snapshot) { + assert.equal(snapshot.capture, 'captured'); + assert.equal(snapshot.matched, 1); + assert.equal(snapshot.executed, 1); + assert.equal(snapshot.failed, 0); + for (const field of ['rejectedBatches', 'failedBatches', 'abortedTransactions', 'pendingBatches', 'pendingBytes']) { + assert.equal(snapshot.writer[field], 0, field); + } + assert(snapshot.writer.committedTransactions > 0); + assert(snapshot.writer.committedWrites > 0); +} + +/** Check only the fixed anonymous page contract; never collect account or storage contents. */ +async function assertAnonymousPage() { + assert.equal(page.url(), target); + report.anonymousControls = await page.getByText('立即注册', { exact: true }).evaluateAll(nodes => nodes.map(node => ({ + tag: node.tagName, parentTag: node.parentElement.tagName, visible: node.getClientRects().length > 0, + }))); + const register = page.getByRole('button', { name: '立即注册', exact: true }); + await register.waitFor({ state: 'visible', timeout: 10000 }); + assert.equal(await register.count(), 1); +} + +async function changeInterval(label, resolution) { + await assertAnonymousPage(); + const before = await page.evaluate(() => self.__ANONYMOUS_MIRROR_PROBE__.snapshot()); + await page.getByText(label, { exact: true }).click({ timeout: 10000 }); + await page.waitForFunction(({ completed, resolution }) => { + const chart = document.querySelector('#chart_futures-tradingview iframe')?.contentWindow?.tradingViewApi?.activeChart(); + return self.__ANONYMOUS_MIRROR_PROBE__.snapshot().completed > completed && chart?.resolution() === resolution && chart.dataReady(); + }, { completed: before.completed, resolution }, { timeout: 25000 }); + await assertAnonymousPage(); + const after = await page.evaluate(() => self.__ANONYMOUS_MIRROR_PROBE__.snapshot()); + const verification = await page.evaluate(() => self.__ANONYMOUS_MIRROR_PROBE__.verifyLast()); + assert(verification.entries > 0); + assert.equal(verification.valuesMatch, true); + report.stages.push({ stage, resolution, before, after, verification }); + console.log(JSON.stringify({ stage, completed: after.completed, writer: after.writer, verification })); + return after; +} + +console.log(JSON.stringify({ directory, stage })); +try { + browser = await chromium.launch({ channel: 'chrome', headless: true }); + report.browserVersion = browser.version(); + const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, locale: 'zh-CN' }); + await context.addInitScript({ content: bundle.outputFiles[0].text }); + page = await context.newPage(); + page.on('pageerror', () => { report.pageErrors += 1; }); + page.on('framenavigated', frame => { + if (frame === page.mainFrame() && frame.url() !== target) report.navigationRefused = true; + }); + // The runner never connects to an existing browser or imports a profile. + await context.route('**/*', async route => { + const request = route.request(); + if (request.isNavigationRequest() && request.frame() === page.mainFrame() && request.url() !== target) { + report.navigationRefused = true; + await route.abort('blockedbyclient'); + return; + } + await route.continue(); + }); + stage = 'document'; + const response = await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 45000 }); + report.documentStatus = response.status(); + await page.getByText('Trading View', { exact: true }).waitFor({ state: 'visible', timeout: 45000 }); + await assertAnonymousPage(); + stage = 'chart_ready'; + await page.getByText('Trading View', { exact: true }).click({ timeout: 10000 }); + await page.waitForFunction(() => document.querySelector('#chart_futures-tradingview iframe')?.contentWindow?.tradingViewApi?.activeChart().dataReady() === true, + null, { timeout: 45000 }); + stage = 'bulk_one_hour'; + validateStats(await changeInterval('1小时', '60')); + stage = 'bulk_fifteen_minutes'; + validateStats(await changeInterval('15分钟', '15')); + await bounded(page.evaluate(() => self.__ANONYMOUS_MIRROR_PROBE__.stop())); + const stopped = await page.evaluate(() => self.__ANONYMOUS_MIRROR_PROBE__.snapshot()); + stage = 'native_after_stop'; + const native = await changeInterval('4小时', '240'); + assert(native.nativeCalls > stopped.nativeCalls); + assert.deepEqual(native.writer, stopped.writer); + assert.equal(native.failed, 0); + assert.equal(report.navigationRefused, undefined); + assert.equal(report.pageErrors, 0); + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + await page.screenshot({ path: join(directory, 'page.png') }); + report.outcome = 'bulk_and_native_continuation_verified'; +} catch (error) { + report.outcome = 'incomplete'; + report.failure = { stage, name: error.name }; + if (error.message.includes('strict mode violation')) report.failure.reason = 'ambiguous_locator'; + process.exitCode = 1; +} finally { + try { + if (page && !page.isClosed()) { + report.final = await bounded(page.evaluate(async () => { + const probe = self.__ANONYMOUS_MIRROR_PROBE__; + if (!probe) return { installed: false }; + await probe.stop(); + return probe.snapshot(); + })); + await page.screenshot({ path: join(directory, 'final.png') }); + } + } finally { + if (browser) { await browser.close(); report.browserClosed = true; } + report.finishedAt = new Date().toISOString(); + await writeFile(join(directory, 'result.json'), JSON.stringify(report, null, 2) + '\n'); + console.log(JSON.stringify({ directory, outcome: report.outcome, failure: report.failure, browserClosed: report.browserClosed })); + } +} diff --git a/scripts/binance-chart-storage-smoke.mjs b/scripts/binance-chart-storage-smoke.mjs new file mode 100644 index 0000000..aa5c272 --- /dev/null +++ b/scripts/binance-chart-storage-smoke.mjs @@ -0,0 +1,240 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { chromium } from 'playwright'; + +const target = 'https://www.binance.com/zh-CN/futures/USUSDT'; +const artifactUrl = new URL('binance-orderbook-trade.user.js', import.meta.url); +const artifact = await readFile(artifactUrl, 'utf8'); +const directory = await mkdtemp(join(tmpdir(), 'binance-chart-storage-smoke-')); +const report = { + startedAt: new Date().toISOString(), + environment: 'new anonymous Chrome context, no storageState, existing profile, account, or extensions', + target, + artifact: { path: artifactUrl.pathname, sha256: createHash('sha256').update(artifact).digest('hex'), bytes: Buffer.byteLength(artifact) }, + storageReadback: 'not_performed', + stages: [], + documentStatuses: [], + pageErrors: 0, + pageErrorKinds: { styleParentUnavailable: 0, other: 0 }, + browserClosed: false, +}; +let browser; +let page; +let stage = 'launch'; + +/** Deadlines bound live checks and cleanup; an incomplete operation is never retried. */ +async function bounded(operation, timeout = 10000) { + let timer; + try { + return await Promise.race([operation, new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error('Smoke operation deadline exceeded')), timeout); + })]); + } finally { + clearTimeout(timer); + } +} + +/** Observe only aggregate destination transactions; never inspect keys, values, or account state. */ +function observeDestinationTransactions() { + if (self !== top || location.href !== 'https://www.binance.com/zh-CN/futures/USUSDT') return; + const originalTransaction = IDBDatabase.prototype.transaction; + const originalPut = IDBObjectStore.prototype.put; + const stats = { transactions: 0, completed: 0, aborted: 0, puts: 0, pending: 0 }; + IDBDatabase.prototype.transaction = function (...args) { + const transaction = Reflect.apply(originalTransaction, this, args); + if (this.name === 'chart_delivery' && transaction.mode === 'readwrite') { + stats.transactions += 1; + stats.pending += 1; + transaction.addEventListener('complete', () => { stats.completed += 1; stats.pending -= 1; }, { once: true }); + transaction.addEventListener('abort', () => { stats.aborted += 1; stats.pending -= 1; }, { once: true }); + } + return transaction; + }; + IDBObjectStore.prototype.put = function (...args) { + const request = Reflect.apply(originalPut, this, args); + if (this.transaction.db.name === 'chart_delivery') stats.puts += 1; + return request; + }; + Object.defineProperty(self, '__CHART_STORAGE_SMOKE_OBSERVATION__', { + value: Object.freeze({ snapshot: () => ({ ...stats }) }), + }); +} + +async function assertAnonymousPage() { + assert.equal(page.url(), target); + const register = page.getByRole('button', { name: '立即注册', exact: true }); + await register.waitFor({ state: 'visible', timeout: 10000 }); + assert.equal(await register.count(), 1); + report.anonymousRegisterVisible = true; +} + +function assertOptimized(snapshot) { + assert.equal(snapshot.status, 'active'); + assert.equal(snapshot.phase, 'active'); + assert.equal(snapshot.matches, 1); + assert.equal(snapshot.executions, 1); + for (const field of ['rejectedBatches', 'failedBatches', 'abortedTransactions', 'pendingBatches', 'pendingBytes', 'nativePassthroughBatches']) { + assert.equal(snapshot.writer[field], 0, field); + } + assert(snapshot.writer.committedTransactions > 0); + assert(snapshot.writer.committedWrites > 0); +} + +async function snapshot() { + return page.evaluate(() => { + const panel = document.getElementById('jh-binance-close-qty-multiplier-panel'); + const style = panel && getComputedStyle(panel); + return { optimizer: self.__BINANCE_CHART_STORAGE__.snapshot(), destination: self.__CHART_STORAGE_SMOKE_OBSERVATION__.snapshot(), + startupDOM: self.__CHART_STORAGE_SMOKE_START__, + orderbook: { + panelCount: document.querySelectorAll('#jh-binance-close-qty-multiplier-panel').length, + panelVisible: Boolean(panel && panel.getClientRects().length && style.visibility === 'visible' && style.display !== 'none'), + modeAnchorPresent: Boolean(document.getElementById('position-direction')), + styleCount: document.querySelectorAll('#jh-disabled-control-style').length, + initialized: typeof self.__TM_CLOSE_LONG_DEBUG__ === 'object', + }, + }; + }); +} + +async function changeInterval(label, resolution, optimized) { + await assertAnonymousPage(); + const before = await snapshot(); + const interval = page.getByText(label, { exact: true }); + assert.equal(await interval.count(), 1); + assert.notEqual(await page.evaluate(() => document.querySelector('#chart_futures-tradingview iframe').contentWindow.tradingViewApi.activeChart().resolution()), resolution); + await interval.click({ timeout: 10000 }); + await page.waitForFunction(({ committed, destinationCompleted, resolution, optimized }) => { + const chart = document.querySelector('#chart_futures-tradingview iframe')?.contentWindow?.tradingViewApi?.activeChart(); + if (chart?.resolution() !== resolution || !chart.dataReady()) return false; + const optimizer = self.__BINANCE_CHART_STORAGE__.snapshot(); + const destination = self.__CHART_STORAGE_SMOKE_OBSERVATION__.snapshot(); + return optimizer.writer.pendingBatches === 0 && destination.pending === 0 + && (optimized ? optimizer.writer.committedTransactions > committed : destination.completed > destinationCompleted); + }, { committed: before.optimizer.writer.committedTransactions, destinationCompleted: before.destination.completed, resolution, optimized }, { timeout: 25000 }); + await assertAnonymousPage(); + const after = await snapshot(); + const delta = { + optimizedCommits: after.optimizer.writer.committedTransactions - before.optimizer.writer.committedTransactions, + destinationCommits: after.destination.completed - before.destination.completed, + destinationPuts: after.destination.puts - before.destination.puts, + }; + if (optimized) { + assertOptimized(after.optimizer); + assert(delta.optimizedCommits > 0); + } else { + assert.equal(after.optimizer.phase, 'stopped'); + assert.equal(after.optimizer.status, 'native'); + assert.deepEqual(after.optimizer.writer, before.optimizer.writer); + assert(delta.destinationCommits > 0); + assert(delta.destinationPuts > 0); + } + assert.equal(after.destination.aborted, 0); + report.stages.push({ stage, resolution, chartDataReady: true, before, after, delta }); + console.log(JSON.stringify({ stage, resolution, delta })); +} + +console.log(JSON.stringify({ directory, stage, artifact: report.artifact })); +try { + browser = await chromium.launch({ channel: 'chrome', headless: true, timeout: 30000 }); + report.browserVersion = browser.version(); + const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, locale: 'zh-CN' }); + await context.addInitScript(observeDestinationTransactions); + await context.addInitScript({ content: `self.__CHART_STORAGE_SMOKE_START__ = { + hasDocumentElement: Boolean(document.documentElement), hasHead: Boolean(document.head) + };\n${artifact}` }); + page = await context.newPage(); + page.on('pageerror', error => { + report.pageErrors += 1; + const styleParentUnavailable = error.name === 'TypeError' + && error.message.includes('null') && error.message.includes('appendChild') + && error.stack.includes('injectDisabledControlStyle'); + report.pageErrorKinds[styleParentUnavailable ? 'styleParentUnavailable' : 'other'] += 1; + }); + page.on('response', response => { + const request = response.request(); + if (request.isNavigationRequest() && request.frame() === page.mainFrame() && request.url() === target) { + report.documentStatuses.push(response.status()); + } + }); + page.on('framenavigated', frame => { + if (frame === page.mainFrame() && frame.url() !== target) report.navigationRefused = true; + }); + await context.route('**/*', async route => { + const request = route.request(); + if (request.isNavigationRequest() && request.frame() === page.mainFrame() && request.url() !== target) { + report.navigationRefused = true; + await route.abort('blockedbyclient'); + return; + } + await route.continue(); + }); + stage = 'document'; + const response = await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 45000 }); + report.documentStatus = response.status(); + await page.getByText('Trading View', { exact: true }).waitFor({ state: 'visible', timeout: 45000 }); + await assertAnonymousPage(); + stage = 'chart_ready'; + await page.getByText('Trading View', { exact: true }).click({ timeout: 10000 }); + await page.waitForFunction(() => document.querySelector('#chart_futures-tradingview iframe')?.contentWindow?.tradingViewApi?.activeChart().dataReady() === true, + null, { timeout: 45000 }); + const initial = await snapshot(); + assert.equal(initial.optimizer.matches, 1); + assert.equal(initial.optimizer.executions, 1); + assert.equal(initial.orderbook.panelCount, 1); + assert.equal(initial.orderbook.styleCount, 1); + assert.equal(initial.orderbook.initialized, true); + report.initial = initial; + stage = 'optimized_one_hour'; + await changeInterval('1小时', '60', true); + stage = 'optimized_fifteen_minutes'; + await changeInterval('15分钟', '15', true); + stage = 'stop'; + report.stopped = await bounded(page.evaluate(() => self.__BINANCE_CHART_STORAGE__.stop())); + assert.equal(report.stopped.phase, 'stopped'); + stage = 'native_after_stop'; + await changeInterval('4小时', '240', false); + assert.equal(report.navigationRefused, undefined); + assert.equal(report.pageErrors, 0); + await bounded(page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))))); + await page.screenshot({ path: join(directory, 'page.png'), timeout: 10000 }); + report.outcome = 'production_bulk_and_native_continuation_verified'; +} catch (error) { + report.outcome = 'incomplete'; + report.failure = { stage, name: error.name }; + if (error.message.includes('strict mode violation')) report.failure.reason = 'ambiguous_locator'; + process.exitCode = 1; +} finally { + try { + if (page && !page.isClosed()) { + report.final = await bounded(page.evaluate(async () => { + const optimizer = self.__BINANCE_CHART_STORAGE__; + if (!optimizer) return { installed: false }; + await optimizer.stop(); + return { optimizer: optimizer.snapshot(), destination: self.__CHART_STORAGE_SMOKE_OBSERVATION__.snapshot(), + startupDOM: self.__CHART_STORAGE_SMOKE_START__ }; + })); + await page.screenshot({ path: join(directory, 'final.png'), timeout: 10000 }); + } + } catch (error) { + report.cleanupFailure = { stage: 'page_cleanup', name: error.name }; + report.outcome = 'incomplete'; + process.exitCode = 1; + } + try { + if (browser) { + await bounded(browser.close(), 15000); + report.browserClosed = true; + } + } catch (error) { + report.cleanupFailure = { stage: 'browser_close', name: error.name }; + report.outcome = 'incomplete'; + process.exitCode = 1; + } + report.finishedAt = new Date().toISOString(); + await writeFile(join(directory, 'result.json'), JSON.stringify(report, null, 2) + '\n'); + console.log(JSON.stringify({ directory, outcome: report.outcome, failure: report.failure, browserClosed: report.browserClosed })); +} diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index b7d585d..962cd7c 100644 --- a/scripts/binance-orderbook-trade.user.js +++ b/scripts/binance-orderbook-trade.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.217 +// @version 2.7.218 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -13,6 +13,7 @@ // @updateURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-orderbook-trade.user.js // @downloadURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-orderbook-trade.user.js // @run-at document-start +// @sandbox raw // @grant none // ==/UserScript== (() => { @@ -2663,14 +2664,14 @@ margin, Math.min(anchorRect.left, viewportWidth - width - margin) ); - const top = Math.max( + const top2 = Math.max( margin, Math.min(anchorRect.top, viewportHeight - estimatedHeight - margin) ); return { width: Math.round(width), left: Math.round(left), - top: Math.round(top) + top: Math.round(top2) }; } function waitForTradeFormMutationState(observationRoot, readState, timeoutMs) { @@ -4457,10 +4458,10 @@ if (statusElement.title !== statusTitle) statusElement.title = statusTitle; } function setDepthProfileGeometry(root, geometry) { - const top = `${geometry.top}px`; + const top2 = `${geometry.top}px`; const height = `${geometry.height}px`; const right = `${geometry.rightInset}px`; - if (root.style.top !== top) root.style.top = top; + if (root.style.top !== top2) root.style.top = top2; if (root.style.height !== height) root.style.height = height; if (root.style.right !== right) root.style.right = right; } @@ -5204,9 +5205,1276 @@ }); } + // src/binance-orderbook-trade/chart-storage/scope.js + function isChartStoragePage() { + return self === top && location.origin === "https://www.binance.com" && isFuturesTradingPathname(location.pathname); + } + + // src/binance-orderbook-trade/chart-storage/bootstrap.js + var QUEUE_NAME = "webpackChunkfutures_trade_ui"; + function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } = {}) { + if (!isChartStoragePage()) throw new Error("Storage bootstrap requires a top-level Binance trading page"); + if (onCapture !== void 0 && typeof onCapture !== "function") throw new Error("onCapture must be a synchronous function"); + if (replaceMirrorFactory !== void 0 && typeof replaceMirrorFactory !== "function") throw new Error("replaceMirrorFactory must be a synchronous function"); + const moduleId = replaceMirrorFactory === void 0 ? "43917" : "70940"; + const globalDescriptor = Object.getOwnPropertyDescriptor(self, QUEUE_NAME); + if (globalDescriptor && !Object.hasOwn(globalDescriptor, "value")) throw new Error("Storage bootstrap requires a native queue property"); + const queue = globalDescriptor && globalDescriptor.value !== void 0 ? globalDescriptor.value : []; + const nativeAppend = Array.prototype.push; + if (!Array.isArray(queue) || queue.length !== 0 || queue.push !== nativeAppend || Object.hasOwn(queue, "push")) { + throw new Error("Storage bootstrap must start before runtime on a native empty queue"); + } + if (globalDescriptor && globalDescriptor.value === void 0 && !globalDescriptor.configurable) { + throw new Error("Storage bootstrap cannot own the existing global queue property"); + } + const createdQueue = !globalDescriptor || globalDescriptor.value === void 0; + if (createdQueue) Object.defineProperty(self, QUEUE_NAME, { value: queue, writable: true, configurable: true, enumerable: true }); + let runtimePush; + let inFlightAppends = 0; + let active = true; + let originalFactory; + let observedFactory; + let observedChunk; + let runtimeRequire; + let resolveCapture; + let rejectCapture; + const captured = new Promise((resolve, reject) => { + resolveCapture = resolve; + rejectCapture = reject; + }); + function restore() { + if (runtimeRequire && runtimeRequire.m[moduleId] === observedFactory) runtimeRequire.m[moduleId] = originalFactory; + if (observedChunk && observedChunk[1][moduleId] === observedFactory) observedChunk[1][moduleId] = originalFactory; + const descriptor = Object.getOwnPropertyDescriptor(queue, "push"); + if (!descriptor || descriptor.get !== getPush || descriptor.set !== setPush || !descriptor.configurable) { + throw new Error("Storage bootstrap lost queue accessor ownership"); + } + if (runtimePush) { + Object.defineProperty(queue, "push", { value: runtimePush, writable: true, configurable: true, enumerable: true }); + } else { + delete queue.push; + if (createdQueue && inFlightAppends === 0 && queue.length === 0 && Object.getOwnPropertyDescriptor(self, QUEUE_NAME)?.value === queue) { + if (globalDescriptor) Object.defineProperty(self, QUEUE_NAME, globalDescriptor); + else delete self[QUEUE_NAME]; + } + } + } + function finish(succeeded, result) { + if (!active) return; + active = false; + try { + restore(); + } catch (cleanupError) { + if (succeeded) { + succeeded = false; + result = cleanupError; + } + } + if (succeeded) resolveCapture(result); + else rejectCapture(result); + } + function prepareChunk(chunk) { + if (!active) return chunk; + try { + if (!Array.isArray(chunk) || !Array.isArray(chunk[0]) || !chunk[1] || typeof chunk[1] !== "object") { + throw new Error("Storage bootstrap observed an unsupported chunk shape"); + } + if (!Object.hasOwn(chunk[1], moduleId)) return chunk; + if (originalFactory) throw new Error("Storage module registered twice before capture"); + originalFactory = chunk[1][moduleId]; + if (typeof originalFactory !== "function") throw new Error("Storage module factory must be a function"); + const hostRuntime = chunk[2]; + if (hostRuntime !== void 0 && typeof hostRuntime !== "function") throw new Error("Storage chunk runtime callback must be a function"); + const executionFactory = replaceMirrorFactory === void 0 ? originalFactory : replaceMirrorFactory(originalFactory); + if (!active) return chunk; + if (typeof executionFactory !== "function") throw new Error("Mirror replacement must return a factory synchronously"); + observedFactory = function(module, exports, require2) { + runtimeRequire = require2; + let returned; + try { + returned = executionFactory.call(this, module, exports, require2); + } catch (hostError) { + finish(false, hostError); + throw hostError; + } + if (active) { + try { + const result = onCapture === void 0 ? void 0 : onCapture(module.exports); + if (result && typeof result.then === "function") throw new Error("Storage observation callback must complete synchronously"); + finish(true, module.exports); + } catch (observationError) { + finish(false, observationError); + } + } + return returned; + }; + observedChunk = [chunk[0], { ...chunk[1], [moduleId]: observedFactory }, (require2) => { + runtimeRequire = require2; + if (hostRuntime !== void 0) return hostRuntime(require2); + }]; + return observedChunk; + } catch (observationError) { + finish(false, observationError); + return chunk; + } + } + function preRuntimePush(...chunks) { + if (!active || runtimePush) return nativeAppend.apply(this, chunks); + inFlightAppends += 1; + try { + return nativeAppend.apply(this, chunks.map(prepareChunk)); + } finally { + inFlightAppends -= 1; + } + } + function runtimeObservingPush(chunk) { + return runtimePush.call(this, prepareChunk(chunk)); + } + function getPush() { + return runtimePush ? runtimeObservingPush : preRuntimePush; + } + function setPush(dispatcher) { + if (!active) { + Object.defineProperty(queue, "push", { value: dispatcher, writable: true, configurable: true, enumerable: true }); + return; + } + if (runtimePush || typeof dispatcher !== "function") { + finish(false, new Error("Storage bootstrap observed an unexpected push replacement")); + Object.defineProperty(queue, "push", { value: dispatcher, writable: true, configurable: true, enumerable: true }); + return; + } + runtimePush = dispatcher; + } + Object.defineProperty(queue, "push", { get: getPush, set: setPush, enumerable: true, configurable: true }); + return Object.freeze({ + captured, + stop() { + finish(false, new Error("Storage bootstrap stopped before module execution")); + } + }); + } + + // src/binance-orderbook-trade/chart-storage/mirror-module.js + var originalFactorySource = '70940(Z,x,e){"use strict";e.r(x),e.d(x,{TradingView:()=>Dt,default:()=>Cn});var o=e(49253),v=e(77135),d=e(75510),i=e(42288),p=e(31085),r=e(41594),M=e(43917),h=e.n(M),P=e(81936),b=e.n(P),V=e(7980),U=e.n(V),D=e(76469),c=e(4260),k=e(53837),B=e(27571),C=e(45250),y=e(87646),A=e.n(y),q=e(80817);const E=(0,C.range)(1,1380),m=(0,C.range)(1,365).map(t=>`${t}D`),R=(0,C.range)(1,52).map(t=>`${t}W`),I=(0,C.range)(1,12).map(t=>`${t}M`),F=["1S",...E,...m,...R,...I],L=({symbol:t,params:n})=>{const a=new URLSearchParams(n);return`${t}@${a.toString()}`},X=t=>{const[n,a]=t.split("@"),s=new URLSearchParams(a).entries(),g=A()(Array.from(s));return{symbol:n,params:g}},ue=()=>{const t=new Map;let n={},a=[],u={onReady:()=>null,resolveSymbol:()=>null,searchSymbols:()=>null,getBars:()=>null,subscribeBars:()=>null,unsubscribeBars:()=>null};return({config:s,onSymbolResolving:g,onSymbolResolved:S,onFetchSymbolsInfo:K,onFetchBars:Y,onSubscribeBars:H,onUnsubscribeBars:j,onFetchServerTimeApi:$,onTransformSymbolInfo:f=({template:T})=>T,onSearchSymbols:l=()=>!0})=>(u={onReady:T=>{setTimeout(async()=>{const _=(await K()).map(O=>({...O,ticker:L({symbol:O.symbol,params:O.params||{}})}));a=_.map(({ticker:O,symbol:G,description:te="",exchange:J="",type:oe=""})=>({symbol:G,ticker:O,description:te,exchange:J,type:oe})),n=(0,C.keyBy)(_,"ticker"),T({exchanges:[],symbols_types:[],supported_resolutions:F,supports_marks:!1,supports_timescale_marks:!1,supports_time:!0,...s})})},resolveSymbol:async(T,W,_)=>{const O=n[T];if(!O){_(`cannot resolve symbol - ${T}`);return}const G=f({info:O,template:{description:O.description||"",fractional:!1,has_seconds:!0,seconds_multipliers:["1"],has_intraday:!0,intraday_multipliers:["1","3","5","15","30","60","120","240","360","480","720"],has_daily:!0,daily_multipliers:["1"],has_weekly_and_monthly:!0,weekly_multipliers:["1"],monthly_multipliers:["1"],minmov:1,minmove2:0,name:O.symbol,ticker:O.ticker,timezone:"Etc/UTC",pricescale:O.tickSize,session:"24x7",type:O.type||"",exchange:O.exchange||"",listed_exchange:O.exchange||"",format:"price",supported_resolutions:F,visible_plots_set:"ohlcv",volume_precision:3}});if(g){const te=X(G.ticker||"");if(!await g({symbol:T,info:G,tickerInfo:te})){_(`symbol resolving error - ${T}`);return}}setTimeout(()=>{W(G),S?.(G)})},searchSymbols:(T,W,_,O)=>{const G=new RegExp(T,"i"),te=new RegExp(W,"i"),J=new RegExp(_,"i"),oe=a.filter(Te=>{const{symbol:N,ticker:w,description:re="",exchange:ie="",type:le=""}=Te;return[N,w,re].some(de=>G.test(de||""))&&te.test(ie)&&J.test(le)&&l?.({item:Te})});O(oe)},getBars:async(T,W,_,O,G)=>{const{from:te,to:J,firstDataRequest:oe}=_,Te=(0,q.Cz)(W);try{if(J>=0){const N=X(T.ticker||""),w=await Y({symbolInfo:T,resolution:W,rangeStartDate:te,rangeEndDate:J,tickerInfo:N,binanceInterval:Te,firstDataRequest:oe});O(w,{noData:w.length===0})}else O([],{noData:!0})}catch(N){G(N.toString())}},subscribeBars:(T,W,_,O,G)=>{const te=X(T.ticker||""),J=(0,q.Cz)(W),oe=H({symbolInfo:T,resolution:W,callback:_,onResetCacheNeededCallback:G,tickerInfo:te,binanceInterval:J});t.set(O,oe)},unsubscribeBars:T=>{t?.get(T)?.(),t?.delete(T),j?.(T)},getServerTime:async T=>{try{const W=await $();T(W)}catch(W){console.warn(W),T(Date.now()/1e3)}}},{datafeed:u,observers:t,symbolsInfo:n})},Le=({namespace:t,type:n})=>{const a=(0,r.useMemo)(()=>h().createInstance({name:t}),[t]);return(0,r.useCallback)(()=>{n==="tv"&&a.removeItem("myTradingView")},[a,n])};var Ue=e(76535),pr=e(26531),Ye=e(17409),Qe=e(94652),Ae=e(76830),he=e(48651),Fe=e(79344),se=e(51029),Je=e(36307),or=e(25379),yr=e(44745),Ne=e(15727),rr=e(45358),sr=e(82071),Ve=e(85411),ge=e(94889),hr=e(94547),tr=e(80686),Ar=e(61875);function Fr(t){return(0,ge._)(t)||(0,hr._)(t)||(0,Ar._)(t)||(0,tr._)()}var Nr=e(92873),zr=e(35630),Vr=e(12055),Qr=e(79515),Br=e(80737),Jr=e(40545),Zr=e(98107),Kr=e(51846),Xr="Chart.Crosshair.PlusButton.DrawHorizontalLine",wr=function(t){var n=t.namespace,a=t.symbol,u=t.priceType,s=t.crosshairPrice,g=(0,Nr.o)("","kline-ui").getI18n,S=(0,Vr.r)().format,K=k.y$(n),Y=(0,Br.S)({namespace:n}),H=(0,Jr.hw)(),j=(0,Zr.c)(),$=(0,Qr.nH)(),f=(0,se.Ye)(),l=(0,Kr.E)({symbol:a}),T=(0,Fe.f0)(a),W=T.isCM,_=(0,r.useRef)(s);(0,r.useEffect)(function(){_.current=s},[s]);var O=(0,r.useRef)(H);(0,r.useEffect)(function(){O.current=H},[H]);var G=(0,r.useRef)(j);(0,r.useEffect)(function(){G.current=j},[j]);var te=(0,r.useRef)(u);(0,r.useEffect)(function(){te.current=u},[u]);var J=(0,r.useRef)($);(0,r.useEffect)(function(){J.current=$},[$]);var oe=(0,r.useRef)(f);(0,r.useEffect)(function(){oe.current=f},[f]);var Te=(0,r.useRef)(l);(0,r.useEffect)(function(){Te.current=l},[l]);var N=(0,r.useRef)(W);return(0,r.useEffect)(function(){N.current=W},[W]),{items_processor:(function(){var w=(0,o._)(function(re,ie,le){var de,Be,ve,Ce,ce,Ke,Ze,Ge,Xe,Ie,He,ur,Tr,_r,Cr;return(0,i._)(this,function(Er){if(Be=K.getState().tradingViewReference,!Be)return console.warn("[context-menu] trading view reference is not found"),[2,re];if(Be.chartsCount()>1)return[2,re];if(ve=Be.activeChart().symbolExt(),!ve)return[2,re];switch(Ce=ve.name,ce=_.current,Ke=le.menuName==="CrosshairMenuView"?"chart-trading":"context-menu",Ze={price:"".concat(ce.value),formattedPrice:ce.formatted,source:Ke,chartType:"tradingview"},Ge=(de=oe.current[Ce])===null||de===void 0?void 0:de.baseAsset,Xe=g("trade",{defaultValue:"Trade"}),Ie=S(ce.formatted),He=G.current&&te.current===c.SJ.Last&&J.current&&Ge?[ie.createAction({actionId:"Chart.PlaceOrder.BuyLimit",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("limit",{defaultValue:"Limit"})),onExecute:function(){return Te.current.onClickBuyLimit(Ze)}}),ie.createAction({actionId:"Chart.PlaceOrder.BuyStop",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("stop",{defaultValue:"Stop"})),onExecute:function(){return Te.current.onClickBuyStop(Ze)}})]:[],ur=O.current?ie.createAction({actionId:"Chart.PriceAlert.CreateAtCrosshair",label:g("create-alert-at",{defaultValue:"Create Alert at {{price}}",price:ce.formatted}),onExecute:function(){Y({symbol:Ce,price:ce.formatted,mode:zr.n.CREATE})}}):null,re.forEach(function(we){if(!(!("execute"in we)||!("getState"in we))){var Dr=we.getState();if(Dr.actionId===Xr){var z=we.execute.bind(we);we.execute=function(){(0,Kr.A)({elementId:"draw_horizontal_line",isDelivery:N.current,chartType:"tradingview"}),z()}}}}),le.menuName){case"CrosshairMenuView":return ur?[2,[ur].concat((0,d._)(He),(0,d._)(re.slice()))]:[2,(0,d._)(He).concat((0,d._)(re.slice()))];case"ChartContextMenu":return ur?(Tr=Fr(re),_r=Tr[0],Cr=Tr.slice(1),[2,[_r,ur].concat((0,d._)(He),(0,d._)(Cr))]):[2,(0,d._)(He).concat((0,d._)(re.slice()))];default:return[2,re]}return[2]})});return function(re,ie,le){return w.apply(this,arguments)}})()}},qr=function(){var t=(0,r.useState)({value:NaN,formatted:""}),n=t[0],a=t[1],u=(0,r.useCallback)(function(s){var g=s.crosshairData,S=s.activeChart;S&&a({value:g.price,formatted:S.priceFormatter().format(g.price)})},[]);return{onCrosshairMoved:u,crosshairPrice:n}};const Hr=t=>Math.floor(Math.log10(t)),et=t=>{const[n="1"]=t.split(",");return Hr(+n)},pe=({symbolInfo:t,minTick:n})=>{const{pricescale:a=2}=t||{};return n==="default"||n===void 0?Hr(a):et(n)},br=t=>(n,a)=>{try{return n?{format:u=>{const s=pe({symbolInfo:n,minTick:a});return t.format({symbolInfo:n,minTick:a,value:u,precision:s})}}:null}catch{return null}},Mr=10**12,rt=10**9,tt=10**6,jr=10**3,gr=t=>{const n=t.toString(),[,a=""]=n.split(".");return Math.min(a.length,3)},nt=({value:t})=>{const n=Math.abs(t);if(n>=Mr){const a=t/Mr;return{value:a,unit:"T",precision:gr(a)}}if(n>=rt){const a=t/rt;return{value:a,unit:"B",precision:gr(a)}}if(n>=tt){const a=t/tt;return{value:a,unit:"M",precision:gr(a)}}if(n>=jr){const a=t/jr;return{value:a,unit:"K",precision:gr(a)}}return{value:t,unit:"",precision:gr(t)}},ut=t=>(n,a,u)=>{try{return a&&n.type==="volume"?{format:s=>s===void 0?"":t.formatVolume({format:n,symbolInfo:a,precision:u,value:s,volumeDetail:nt({value:s})})}:null}catch{return null}};var lt=function(){var t=(0,Vr.r)(),n=t.format,a=(0,r.useMemo)(function(){return br({format:function(s){var g=s.value,S=s.precision;return n(g,{precision:S})}})},[n]),u=(0,r.useMemo)(function(){return ut({formatVolume:function(s){var g=s.value,S=s.volumeDetail;if(g===void 0||!S)return"";var K=n(S.value,{precision:S.precision});return"".concat(K).concat(S.unit)}})},[n]);return{priceFormatterFactory:a,studyFormatterFactory:u}},dt=e(74069),ir=function(t){var n=(0,dt.K)(t),a=(0,r.useMemo)(function(){return n.map(function(u){var s=u.id,g=u.time,S=u.isBuy,K=u.price,Y=u.tooltip;return{id:s,text:"",price:K,quantity:"",time:Math.round(g/1e3),isBuy:S,tooltip:Y}})},[n]);return{executionOrders:a}},at=function(t){var n=t.namespace,a=t.symbol,u=(0,Br.j)({namespace:n,symbol:a});return(0,r.useMemo)(function(){return u.map(function(s){var g=s.id,S=s.price,K=s.formattedPrice,Y=s.sideText,H=s.showCloseButton,j=s.isPriceChangeable,$=s.cancelTooltipText,f=s.onClick,l=s.onCancel,T=s.onPriceChanged;return{id:g,price:+S,text:"".concat(Y," ").concat(K),editable:j,cancellable:H,cancelTooltip:$,type:c.ND.PriceAlert,onCancel:H?l:void 0,onMove:j?function(W){var _=W.price;T({currentPrice:_})}:void 0,onClick:f}})},[u])},vt=e(90664),ot=function(t){var n=t.symbol,a=(0,vt.w)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){var s=u.id,g=u.text,S=u.price;return{id:s,text:g,price:+S,type:c.ND.BreakEvenPrice}})},[a])},cr=e(82508),ft=e(91025),mt=e(36077),Rr=function(t){var n=t.symbol,a=(0,mt.c)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){return(0,ft._)((0,cr._)({},u),{type:c.ND.LiquidationPrice})})},[a])},pt=function(t){var n=t.namespace,a=t.symbol,u=at({namespace:n,symbol:a}),s=ot({symbol:a}),g=Rr({symbol:a});return(0,r.useMemo)(function(){return(0,d._)(u).concat((0,d._)(s),(0,d._)(g))},[u,s,g])},st=e(42902),Ir=e(90170),yt=e(63345),ht=e(48210),Pr=e(51289),xr=e(24326),$r=e(90922),kr=e(34235),it=e(63762),Ft=function(t){var n=(0,Fe.f0)(t),a=n.pricePrecision,u=n.isCM,s=(0,st.yD)(t)||{},g=s.lastPrice,S=g===void 0?"0":g,K=(0,kr.Bp)(u),Y=(0,it.HY)(t),H=(0,kr.gv)(u),j=(0,kr.WF)(u);return(0,r.useMemo)(function(){return(0,$r.qF)({orders:(0,d._)(K).concat((0,d._)(H),(0,d._)(j)),symbol:t}).map(function($){return Y({order:$,lastPrice:S,pricePrecision:a})})},[S,K,H,a,t,Y])},gt=e(31542),Tt=function(t){var n=t.type;(0,gt.u4)("webClick",{module:"chart",elementId:n})},Nt=function(t){var n=t.symbol,a=t.namespace,u=(0,r.useState)(0),s=u[0],g=u[1],S=(0,r.useCallback)(function(){return g(function(N){return N+1})},[]),K=Ft(n),Y=(0,xr.IT)({namespace:a}),H=(0,Fe.f0)(n),j=H.isDelivery,$=H.contractType,f=(0,st.yD)(n)||{},l=f.lastPrice,T=l===void 0?"0":l,W=(0,yt.s)(n),_=(0,it.YZ)({symbol:n,afterConfirmationModalIsShown:S}),O=(0,it.u8)(n),G=(0,it.jn)(n),te=(0,r.useCallback)(function(N){return(0,ht.D)({order:N,isDelivery:j,symbol:j?N.symbol:"".concat(N.symbol,"_").concat($),module:"tradingview_kline"})},[$,j]),J=(0,r.useMemo)(function(){return K.map(function(N){var w=N.order,re=N.price,ie=N.quantity,le=N.quantityText,de=N.isBuy,Be=N.text,ve=N.priceTooltipText,Ce=N.pnlText,ce=(0,Pr.$L)(w),Ke=(0,Pr.OX)(w),Ze=(0,Pr.xL)(w),Ge=ce==="LIMIT",Xe=(0,Ir.Y)(w);return{id:String(Ke),text:Be,price:+re,quantity:le,isBuy:de,editable:Xe&&G,tooltip:ve,modifyTooltip:w.closePosition||!Ge?"":void 0,onMove:G?function(Ie){var He=Ie.price;te(w),Tt({type:"TV_vertical"}),_({order:w,price:+He,quantity:+Ze,lastPrice:T})}:void 0,onModify:w.closePosition||!Ge||!Xe?void 0:function(){Tt({type:"TV_amountclick"}),O({order:w,price:re.toString(),quantity:ie,lastPrice:T})},onCancel:function(){Tt({type:"TV_cancel"}),W(w)}}})},[G,K,T,te,_,O,W,s]),oe=(0,r.useMemo)(function(){return Y.map(function(N){var w=N.id,re=N.value,ie=N.text,le=N.onChange,de=N.isBuy,Be=N.type;return{id:w,price:+re,quantity:"",text:ie,isBuy:de,editable:!0,limitOrderType:c.aA.OrderPreview,onMove:function(ve){var Ce=ve.price;(0,xr.eP)({namespace:a,orderType:Be}),le({id:w,newPrice:Ce})},onMoving:function(ve){var Ce=ve.price;return le({id:w,newPrice:Ce})}}})},[Y,a]),Te=(0,r.useMemo)(function(){return(0,d._)(J).concat((0,d._)(oe))},[J,oe]);return{orderLines:Te}},Vt=e(17855),Bt=function(t,n){var a=(0,r.useState)(t()),u=a[0],s=a[1];return(0,Vt.A)(function(){return s(t())},n),u},Kt=e(58656),Ht=function(t){var n=t.symbol,a=(0,Kt.Q)({symbol:n})||[],u=Bt(function(){return a.map(function(s){var g=s.position,S=s.pnl,K=s.pnlInNumber,Y=s.pnlText,H=s.price,j=s.quantity,$=s.reverseText,f=s.isBuy,l=s.displayReverseOrderModal,T=s.displayClosePositionModal;return{id:g.id||"".concat(g.symbol,"-").concat(g.positionSide),PNL:K,text:"".concat(Y," ").concat(S),price:H,quantity:j,tooltip:"",reverseTooltip:$,isBuy:f,onReverse:l,onClose:function(){T(),(0,gt.u4)("$WebClick",{item:1,module:"xxx",$element_id:"kline_close_position"})}}})},[a]);return{positionLines:u}},jt=e(47354),bt=e.n(jt),$t=e(87017),Ut=e(99168),Yt=e(3742),Wt=e(43335),Gt=e(15540),zt=e(20163),Qt=e(47255);const Jt=t=>({endTime:n})=>(n??0)===t;var Zt=e(36488),Ct=e(48018),Xt=e(82911),We=e(69578),wt=e(53208),Rt=e(19862),qt=e(8874),en=e(26679),rn=e(66547),It=(function(){var t=(0,o._)(function(n){var a,u,s,g,S,K,Y,H,j;return(0,i._)(this,function($){switch($.label){case 0:a=n.onFetchKlineData,u=n.startTime,s=n.endTime,g=n.interval,S=n.limit,K=S===void 0?1e3:S,Y=[],H=[],j=s,$.label=1;case 1:return[4,a((0,cr._)({interval:g,limit:K},j===0?{}:{endTime:j}))];case 2:if(H=$.sent(),H.length===0)return[3,4];H.forEach(function(f){var l=(0,v._)(f,8),T=l[0],W=l[1],_=l[2],O=l[3],G=l[4],te=l[5],J=l[7];Y.push({time:+T,open:+W,high:+_,low:+O,close:+G,volume:+te,quantity:+J})}),j=H[0][0]-1,$.label=3;case 3:if(j>u)return[3,1];$.label=4;case 4:return Y.sort(function(f,l){return f.time-l.time}),[2,Y]}})});return function(a){return t.apply(this,arguments)}})(),tn=function(t){var n=t.firstDataRequest,a=t.hasRequested,u=t.rangeEndDate;return n&&!a?0:u*1e3},nn=e(78142),an=e(16238),St=e.n(an),on=e(98087),xt=e.n(on),sn=e(55009),cn=e.n(sn),kt=e(56477);const Et=t=>cn()(t.chartsCount(),n=>t.chart(n)),un=async t=>{try{const{schema:n,data:a}=await t.exportData({includedStudies:[]}),u=xt()(a)?.[n.findIndex(s=>s.type==="time")];return u!==void 0&&Number.isFinite(u)?u*1e3:void 0}catch{return}},ln=({getWidget:t,...n})=>{const a=new Map,u=new Map,s=()=>{try{const f=t();return f?St()(Et(f).map(l=>l.symbolExt()?.ticker)):[]}catch{return[]}},g=f=>{const l=s();return!l.length||l.includes(f)},S=f=>{const l=u.get(f);if(l)return l;const T={controller:new AbortController,claimedAsMain:!1};return u.set(f,T),T},K=f=>{const l=u.get(f);l&&(l.controller.abort(),u.delete(f))},Y=()=>{const f=s();f.length&&Array.from(u.keys()).forEach(l=>{const T=u.get(l);if(T){if(f.includes(l)){T.claimedAsMain=!0;return}a.has(l)||T.claimedAsMain&&K(l)}})},H=(f,l)=>{f.closed||l.time{f.held=[];let T=l;try{const _=(await f.options.fetchSince(l)).filter(O=>O.time>=l&&!f.closed);_.forEach(O=>{f.highWater=Math.max(f.highWater,O.time),f.options.onTick(O)}),T=xt()(_)?.time??l}finally{const W=f.held??[];f.held=void 0,W.filter(_=>_.time>=T).forEach(_=>{H(f,_)})}},$=(0,kt.k)({...n,onCheck:Y,repair:async()=>{const f=t();if(f)try{const l=Et(f),T=St()(l.map(_=>{const O=a.get(_.symbolExt()?.ticker??"");return O&&{chart:_,feed:O}}));if(!T.length){f.resetCache(),l.forEach(_=>{_.resetData()});return}const W=St()(await Promise.all(T.map(async({chart:_,feed:O})=>{const G=await un(_);return G===void 0?void 0:{feed:O,fromMs:G}})));await Promise.allSettled(W.map(({feed:_,fromMs:O})=>j(_,O)))}catch{}}});return{openFeed:f=>{const l={options:f,held:void 0,highWater:0,closed:!1,detector:void 0};return l.detector=(0,kt.k)({...n,subscribeSignals:f.subscribeSignals,repair:async()=>{l.closed||!l.highWater||g(f.ticker)||await j(l,l.highWater)}}),a.set(f.ticker,l),g(f.ticker)&&$.resubscribeSignals(f.subscribeSignals),{push:T=>{l.held?l.held.push(T):H(l,T)},close:()=>{l.closed=!0,l.detector?.dispose(),a.get(f.ticker)===l&&a.delete(f.ticker)}}},subscribeWake:$.subscribeWake,getSignal:f=>{const l=S(f);return s().includes(f)&&(l.claimedAsMain=!0),l.controller.signal},dispose:()=>{u.forEach(({controller:f})=>{f.abort()}),u.clear(),a.forEach(f=>{f.detector?.dispose()}),a.clear(),$.dispose()}}};var dn=e(71730),vn=e(36620),fn=function(t){var n=t.namespace,a=k.y$(n),u=(0,vn.l)().getSocketSignals,s=(0,r.useRef)();(0,r.useEffect)(function(){return function(){var S;(S=s.current)===null||S===void 0||S.instance.dispose(),s.current=void 0}},[]);var g=(0,r.useCallback)(function(S){var K,Y,H=S;if(((K=s.current)===null||K===void 0?void 0:K.key)===H)return s.current.instance;(Y=s.current)===null||Y===void 0||Y.instance.dispose();var j=nn.wr(S),$=ln({getWidget:function(){return a.getState().tradingViewReference},checkPeriodMs:dn.D(j)});return s.current={key:H,instance:$},$},[a]);return(0,r.useMemo)(function(){return{ensureRecovery:g,getSocketSignals:u}},[g,u])},mn="tv_web_error",pn=[Ye.bt.TRADING,Ye.bt.DELIVERING],Re;(function(t){t.Futures="futures",t.FuturesMark="futures-mark",t.FuturesIndex="futures-index",t.Delivery="delivery",t.DeliveryMark="delivery-mark",t.DeliveryIndex="delivery-index"})(Re||(Re={}));var Mt=function(t){var n=t.symbol,a=t.description,u=t.productType,s=t.pricePrecision,g=t.params,S=t.supportedResolutions,K=S===void 0?F:S;return{symbol:n,description:a,tickSize:Math.pow(10,s),exchange:"Binance",supportedResolutions:K,type:u,params:g}},yn=function(t){var n=t.namespace,a=t.useMiniTicker,u=a===void 0?!1:a,s=t.isCM,g=c.y$(n),S=k.y$(n),K=(0,$t.useQueryClient)(),Y=fn({namespace:n}),H=Y.ensureRecovery,j=Y.getSocketSignals,$=(0,r.useRef)(0),f=(0,r.useRef)(!1),l=(0,r.useMemo)(function(){return bt()(function(){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsStart",catch:function(Q){var ee=Q.error,ae=Q.track;ae({GET_BAR_START_TYPE:ee})},log:function(Q){var ee=Q.track;ee({GET_BAR_START_TYPE:"SUCCESS"})}})})},[]),T=(0,r.useMemo)(function(){return bt()(function(z){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsEnd",catch:function(ee){var ae=ee.error,Se=ee.track;Se({GET_BAR_END_TYPE:ae,duration_:(0,We.Pu)(z-$.current)})},log:function(ee){var ae=ee.track;ae({GET_BAR_END_TYPE:"SUCCESS",duration_:(0,We.Pu)(z-$.current)})}})})},[]),W=(0,Nr.o)("trd-chart","trade-ui"),_=W.getI18n,O=(0,he.ud)(),G=O.getI18n,te=(0,v._)(g(c.Ir),1),J=te[0],oe=(0,v._)(S(k.A7),2),Te=oe[1],N=(0,se.Ye)(),w=(0,Zt.JD)(s).isContUnit,re=(0,se.y_)(),ie=(0,r.useRef)(N),le=(0,r.useRef)(J),de=(0,r.useRef)(re);(0,r.useEffect)(function(){ie.current=N},[N]),(0,r.useEffect)(function(){le.current=J},[J]),(0,r.useEffect)(function(){de.current=re},[re]);var Be=(0,r.useMemo)(function(){return w?Rt.b:Rt.S},[w]),ve=(0,r.useMemo)(function(){return G("usdtFutures-13",{defaultValue:"Futures"})||""},[G]),Ce=(0,r.useMemo)(function(){return G("inverseFutures-13",{defaultValue:"Delivery"})||""},[G]),ce=(0,r.useMemo)(function(){return _("indexPrice",{defaultValue:"Index Price"})||""},[_]),Ke=(0,r.useMemo)(function(){return _("markPrice",{defaultValue:"Mark Price"})||""},[_]),Ze=(0,r.useMemo)(function(){return _("lastPrice",{defaultValue:"Last Price"})||""},[_]),Ge=(0,r.useMemo)(function(){return{exchanges:[{name:"Binance",value:"Binance",desc:""}],symbols_types:[{name:ve,value:Re.Futures},{name:"".concat(ve," ").concat(Ke),value:Re.FuturesMark},{name:"".concat(ve," ").concat(ce),value:Re.FuturesIndex},{name:Ce,value:Re.Delivery},{name:"".concat(Ce," ").concat(Ke),value:Re.DeliveryMark},{name:"".concat(Ce," ").concat(ce),value:Re.DeliveryIndex}]}},[Ce,ve,ce,Ke]),Xe=(0,r.useCallback)((0,o._)(function(){var z,Q,ee;return(0,i._)(this,function(ae){switch(ae.label){case 0:return ae.trys.push([0,2,,3]),[4,(0,en.c)(K)];case 1:return ae.sent(),[3,3];case 2:return z=ae.sent(),(0,gt.u4)(mn,{type:"onFetchSymbolsInfo",message:z.message,business:"futures"}),[3,3];case 3:return Q=ie.current,ee=Object.keys(Q).reduce(function(Se,xe){var ne=Q[xe]||Ct.o,ke=ne.symbol,Me=ne.pricePrecision,fe=ne.isDelivery,Pe=ne.contractStatus;if(!pn.includes(Pe))return Se;var _e=(0,Yt.S)(ne,G),je=_e.symbol,Ee=_e.contractTypeI18n,me=_e.deliveryDateFormat,ye="".concat(je," ").concat(Ee).concat(me?" ".concat(me):""),$e="".concat(ye," ").concat(Ze),Oe="".concat(ye," ").concat(ce),lr="".concat(ye," ").concat(Ke);return Se.concat([Mt({symbol:ke,description:$e,productType:fe?Re.Delivery:Re.Futures,pricePrecision:Me,params:{priceType:c.SJ.Last}}),Mt({symbol:ke,description:Oe,productType:fe?Re.DeliveryIndex:Re.FuturesIndex,pricePrecision:Me,params:{priceType:c.SJ.Index}}),Mt({symbol:ke,description:lr,productType:fe?Re.DeliveryMark:Re.FuturesMark,pricePrecision:(0,Xt.X)(Me),params:{priceType:c.SJ.Mark}})])},[]),[2,ee]}})}),[G,ce,Ke,Ze,K]),Ie=(0,r.useCallback)(function(z){var Q=z.ticker,ee=z.symbol,ae=z.interval,Se=z.priceType,xe=z.retryUntil,ne=ie.current[ee]||Ct.o,ke=ne.pair,Me=ne.contractType,fe=ne.contractVal,Pe=ne.isDelivery,_e=Pe?Be(fe):function(Oe){return Oe},je=(0,wt.h)({symbol:ee,pair:ke,contractType:Me,isDelivery:Pe,priceType:Se}),Ee,me,ye=function(Oe){return zt.y("tv","".concat(Pe?"cm":"um","|").concat(ee,"|").concat(Se,"|").concat(Oe.interval,"|").concat((Ee=Oe.limit)!==null&&Ee!==void 0?Ee:"","|").concat((me=Oe.endTime)!==null&&me!==void 0?me:""))};if(xe===void 0)return{transform:_e,onFetchKlineData:je};var $e=H(ae);return{transform:_e,onFetchKlineData:Qt.b(je,{enabled:Jt(xe),key:ye,signal:$e.getSignal(Q),subscribeWake:$e.subscribeWake})}},[H,Be]),He=(0,r.useCallback)((function(){var z=(0,o._)(function(Q){var ee,ae,Se,xe,ne,ke,Me,fe,Pe,_e,je,Ee,me,ye,$e,Oe,lr,Or;return(0,i._)(this,function(dr){switch(dr.label){case 0:return ee=Q.binanceInterval,ae=Q.rangeStartDate,Se=Q.rangeEndDate,xe=Q.tickerInfo,ne=Q.symbolInfo,ke=Q.firstDataRequest,$.current=Date.now(),l(),Me=xe.symbol,fe=Me===void 0?"":Me,Pe=xe.params,_e=Pe.priceType,je=_e===void 0?le.current:_e,de.current({symbol:fe,priceType:je})?(T(Date.now()),[2,[]]):(Ee=Math.max(0,ae*1e3),me=tn({firstDataRequest:ke,hasRequested:f.current,rangeEndDate:Se}),f.current=!0,$e=Ie({ticker:(ye=ne.ticker)!==null&&ye!==void 0?ye:"",symbol:fe,interval:ee,priceType:je,retryUntil:ke?me:void 0}),Oe=$e.transform,lr=$e.onFetchKlineData,[4,It({onFetchKlineData:lr,startTime:Ee,endTime:me,interval:ee})]);case 1:return Or=dr.sent(),T(Date.now()),[2,Or.map(Oe)]}})});return function(Q){return z.apply(this,arguments)}})(),[Ie,T,l]),ur=(0,r.useCallback)(function(z){var Q=z.binanceInterval,ee=z.callback,ae=z.tickerInfo,Se=z.symbolInfo,xe=ae.symbol,ne=xe===void 0?"":xe,ke=ae.params,Me=ke.priceType,fe=Me===void 0?le.current:Me;if(de.current({symbol:ne,priceType:fe}))return function(){return null};var Pe=ie.current[ne]||Ct.o,_e=Pe.pair,je=Pe.contractType,Ee=Pe.isDelivery,me,ye=(me=Se.ticker)!==null&&me!==void 0?me:"",$e=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe}).transform,Oe=H(Q),lr=Oe.openFeed({ticker:ye,onTick:ee,subscribeSignals:j(Ee),fetchSince:(function(){var dr=(0,o._)(function(Lr){var Ur,Yr,ct,De;return(0,i._)(this,function(ze){switch(ze.label){case 0:return Ur=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe,retryUntil:0}),Yr=Ur.transform,ct=Ur.onFetchKlineData,[4,It({onFetchKlineData:ct,startTime:Lr,endTime:0,interval:Q})];case 1:return De=ze.sent(),[2,De.map(Yr)]}})});return function(Lr){return dr.apply(this,arguments)}})()}),Or=(0,rn.B)({symbol:ne,pair:_e,contractType:je,interval:Q,priceType:fe,isDelivery:Ee,useMiniTicker:u,callback:function(dr){lr.push($e(dr))}});return function(){lr.close(),Or()}},[Ie,H,j,u]),Tr=(0,r.useCallback)((0,o._)(function(){var z;return(0,i._)(this,function(Q){switch(Q.label){case 0:return[4,K.fetchQuery(Wt.Bz.SERVER_TIME(s),function(){return(0,Gt.l)(s)},{})];case 1:return z=Q.sent(),[2,Math.floor(z/1e3)]}})}),[K,s]),_r=(0,r.useCallback)(function(z){return(0,cr._)({},z.template,(0,qt.sp)(s))},[s]),Cr=(0,r.useMemo)(function(){return ue()},[]),Er=Cr({config:Ge,onFetchSymbolsInfo:Xe,onFetchBars:He,onSubscribeBars:ur,onFetchServerTimeApi:Tr,onTransformSymbolInfo:_r}),we=Er.datafeed,Dr=Er.symbolsInfo;return(0,Ut.A)(function(){return Te()},[w]),{datafeed:we,symbolsInfo:Dr}},Pt="trd_chart_tv_load_timeout",hn=function(){var t=(0,We.PM)({id:"chartReady"}),n=(0,We.PM)({id:"getBarsStart",timeout:3}),a=(0,We.PM)({id:"getBarsEnd",timeout:6}),u=(0,We.PM)({id:"chartResponsive",timeout:3});We.HP.init({step:t._(n)._(a)._(u),eventId:Pt}),(0,We.HP)({eventId:Pt,stepId:"chartReady",catch:function(g){var S=g.error,K=g.track;K({CHART_READY_TYPE:S,connection:JSON.stringify(navigator.connection)})},log:function(g){var S=g.track;S({CHART_READY_TYPE:"SUCCESS",connection:JSON.stringify(navigator.connection)})}})},gn=function(t){(0,We.HP)({eventId:Pt,stepId:"chartResponsive",catch:function(a){var u=a.error,s=a.track;s({CHART_RESPONSIVE_TYPE:u,connection:JSON.stringify(navigator.connection)})},log:function(a){var u=a.track;u({CHART_RESPONSIVE_TYPE:"SUCCESS",duration_:(0,We.Pu)(Date.now()-t)})}})},Tn=[Qe.ZE,Qe.F3],Dt=function(t){var n=t.symbol,a=t.namespace,u=t.useMiniTicker,s=u===void 0?!1:u,g=c.y$(a),S=k.y$(a),K=(0,v._)(g(c.A8),1),Y=K[0],H=(0,v._)(g(c.Ir),2),j=H[0],$=H[1],f=(0,v._)(S(k.yj),1),l=f[0],T=(0,v._)(g(c.e6),1),W=T[0],_=(0,v._)(g(c._b),1),O=_[0],G=(0,he.ot)(),te=(0,pr.K7)(),J=te.pathname.includes(Ae.Q2),oe=te.pathname.includes(Ae.eq),Te=(0,r.useMemo)(function(){return J?(0,d._)(B.oT).concat(["hide_left_toolbar_by_default"]):B.oT},[J]),N=(0,se.Ye)(),w=(0,se.v3)({symbol:n}),re=(0,Fe.es)(n),ie=yn({namespace:a,useMiniTicker:s,isCM:re}),le=ie.datafeed,de=ie.symbolsInfo,Be=(0,Je.DP)(),ve=Be.theme,Ce=ve==="light"||ve==="light_glacier"?"light":"dark",ce=(0,yr.Z)(Ce),Ke=(0,r.useRef)(-1),Ze=Ht({symbol:n}).positionLines,Ge=Nt({symbol:n,namespace:a}).orderLines,Xe=ir({symbol:n}).executionOrders,Ie=pt({namespace:a,symbol:n}),He=(0,se.y0)(),ur=(0,r.useRef)(He),Tr=(0,r.useRef)(N),_r=(0,r.useRef)(l),Cr=(0,r.useRef)(J),Er=(0,r.useRef)(oe),we=(0,r.useRef)(de);(0,r.useEffect)(function(){we.current=de},[de]),(0,r.useEffect)(function(){ur.current=He},[He]),(0,r.useEffect)(function(){Tr.current=N},[N]),(0,r.useEffect)(function(){_r.current=l},[l]),(0,r.useEffect)(function(){Cr.current=J},[J]),(0,r.useEffect)(function(){Er.current=oe},[oe]),(0,Ve.a)(n,s),(0,sr.A)({symbol:n,interval:Y,priceType:j});var Dr=(0,r.useCallback)(function(){Ke.current=Date.now()},[]),z=(0,r.useCallback)((0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})()),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er=="string"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}),[a]),Q=(0,r.useMemo)(function(){return z},[z]),ee=(0,or.R)(),ae=(0,Ne.fQ)(a),Se=(0,Ne.m3)(a),xe=(0,Ne.S_)(a),ne=(0,Ne.Ds)(a),ke=(0,r.useRef)(n),Me=(0,r.useRef)(Y),fe=(0,r.useCallback)(function(){return ee("tradingView"),xe({panel_index:0,symbol:n,interval:Y}),hn()},[Y,ee,n,xe]),Pe=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be,vr,fr,er=(fr=(vr=(be=qe.symbolExt())===null||be===void 0?void 0:be.name)!==null&&vr!==void 0?vr:qe.symbol())!==null&&fr!==void 0?fr:"";ae({panel_index:ze,symbol:er,previous_symbol:ke.current}),ke.current=er},[ae]),_e=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be=qe.resolution();Se({panel_index:ze,interval:be,previous_interval:Me.current}),Me.current=be},[Se]),je=(0,r.useCallback)(function(){gn(Ke.current)},[]),Ee=(0,r.useCallback)(function(){},[]),me=(0,r.useMemo)(function(){return J?Ee:void 0},[Ee,J]),ye=(0,r.useCallback)((function(){var De=(0,o._)(function(ze){var qe,be,vr,fr,er,nr,mr,ar,Sr;return(0,i._)(this,function(Wr){switch(Wr.label){case 0:return qe=ze.chart,be=qe.symbolExt(),be?(vr=b()(we.current,function(Gr){return Gr.ticker===be.ticker||Gr.symbol===be.ticker}),fr=vr||{},er=fr.params,nr=er===void 0?{}:er,mr=nr.priceType,ar=mr===void 0?"":mr,ar&&$(ar),Sr=Tr.current[be.name],Sr?Er.current||Cr.current?[2]:[4,z()]:[2]):[2];case 1:return Wr.sent(),G({stateSymbol:Sr.symbol,isDeliveryColumn:Sr.isDelivery}),[2]}})});return function(ze){return De.apply(this,arguments)}})(),[G,z,$]),$e=qr(),Oe=$e.onCrosshairMoved,lr=$e.crosshairPrice,Or=wr({namespace:a,symbol:n,priceType:j,crosshairPrice:lr}),dr=lt(),Lr=(0,r.useMemo)(function(){return L({symbol:n,params:{priceType:j}})},[n,j]),Ur=(0,r.useMemo)(function(){return{symbol:Lr,theme:ce,limitOrders:Ge,positionOrders:Ze,executionOrders:Xe,labelLines:Ie,initialConfig:{tvConfig:{symbol:Lr,library_path:"/".concat(Ye.u_,"/trading-platform-30/"),theme:ce,datafeed:le,enabled_features:Te,context_menu:Or,custom_formatters:dr},onCrosshairMoved:Oe,onScriptLoaded:Dr,onSave:Q,onChartReadyDone:fe,onFirstTimeDataLoadedDone:je,onInitChart:me,onActiveChartChanged:ye,onSymbolChanged:Pe,onIntervalChanged:_e}}},[Lr,le,Te,Xe,Ie,Or,dr,Oe,ye,fe,je,me,_e,Q,Dr,Pe,Ge,Ze,ce]),Yr=Le({namespace:a,type:"tv"}),ct=(0,r.useCallback)(function(){ne({panel_index:0,symbol:n}),Yr()},[Yr,n,ne]);return(0,p.jsxs)(D.t,{name:"trading_view",onReset:ct,children:[(0,p.jsx)(Ue.A,{namespace:a,tradingViewProps:Ur}),w&&j==="index"&&O===c.Ev.Single&&W===c.tU.TradingView&&(0,p.jsx)(rr.B,{})]})};const Cn=Dt}'; + function createMirrorFactory(__dispatchChartMirror) { + if (typeof __dispatchChartMirror !== "function") throw new Error("Mirror dispatch must be a function"); + return { 70940(Z, x, e) { + "use strict"; + e.r(x), e.d(x, { TradingView: () => Dt, default: () => Cn }); + var o = e(49253), v = e(77135), d = e(75510), i = e(42288), p = e(31085), r = e(41594), M = e(43917), h = e.n(M), P = e(81936), b = e.n(P), V = e(7980), U = e.n(V), D = e(76469), c = e(4260), k = e(53837), B = e(27571), C = e(45250), y = e(87646), A = e.n(y), q = e(80817); + const E = (0, C.range)(1, 1380), m = (0, C.range)(1, 365).map((t) => `${t}D`), R = (0, C.range)(1, 52).map((t) => `${t}W`), I = (0, C.range)(1, 12).map((t) => `${t}M`), F = ["1S", ...E, ...m, ...R, ...I], L = ({ symbol: t, params: n }) => { + const a = new URLSearchParams(n); + return `${t}@${a.toString()}`; + }, X = (t) => { + const [n, a] = t.split("@"), s = new URLSearchParams(a).entries(), g = A()(Array.from(s)); + return { symbol: n, params: g }; + }, ue = () => { + const t = /* @__PURE__ */ new Map(); + let n = {}, a = [], u = { onReady: () => null, resolveSymbol: () => null, searchSymbols: () => null, getBars: () => null, subscribeBars: () => null, unsubscribeBars: () => null }; + return ({ config: s, onSymbolResolving: g, onSymbolResolved: S, onFetchSymbolsInfo: K, onFetchBars: Y, onSubscribeBars: H, onUnsubscribeBars: j, onFetchServerTimeApi: $, onTransformSymbolInfo: f = ({ template: T }) => T, onSearchSymbols: l = () => true }) => (u = { onReady: (T) => { + setTimeout(async () => { + const _ = (await K()).map((O) => ({ ...O, ticker: L({ symbol: O.symbol, params: O.params || {} }) })); + a = _.map(({ ticker: O, symbol: G, description: te = "", exchange: J = "", type: oe = "" }) => ({ symbol: G, ticker: O, description: te, exchange: J, type: oe })), n = (0, C.keyBy)(_, "ticker"), T({ exchanges: [], symbols_types: [], supported_resolutions: F, supports_marks: false, supports_timescale_marks: false, supports_time: true, ...s }); + }); + }, resolveSymbol: async (T, W, _) => { + const O = n[T]; + if (!O) { + _(`cannot resolve symbol - ${T}`); + return; + } + const G = f({ info: O, template: { description: O.description || "", fractional: false, has_seconds: true, seconds_multipliers: ["1"], has_intraday: true, intraday_multipliers: ["1", "3", "5", "15", "30", "60", "120", "240", "360", "480", "720"], has_daily: true, daily_multipliers: ["1"], has_weekly_and_monthly: true, weekly_multipliers: ["1"], monthly_multipliers: ["1"], minmov: 1, minmove2: 0, name: O.symbol, ticker: O.ticker, timezone: "Etc/UTC", pricescale: O.tickSize, session: "24x7", type: O.type || "", exchange: O.exchange || "", listed_exchange: O.exchange || "", format: "price", supported_resolutions: F, visible_plots_set: "ohlcv", volume_precision: 3 } }); + if (g) { + const te = X(G.ticker || ""); + if (!await g({ symbol: T, info: G, tickerInfo: te })) { + _(`symbol resolving error - ${T}`); + return; + } + } + setTimeout(() => { + W(G), S?.(G); + }); + }, searchSymbols: (T, W, _, O) => { + const G = new RegExp(T, "i"), te = new RegExp(W, "i"), J = new RegExp(_, "i"), oe = a.filter((Te) => { + const { symbol: N, ticker: w, description: re = "", exchange: ie = "", type: le = "" } = Te; + return [N, w, re].some((de) => G.test(de || "")) && te.test(ie) && J.test(le) && l?.({ item: Te }); + }); + O(oe); + }, getBars: async (T, W, _, O, G) => { + const { from: te, to: J, firstDataRequest: oe } = _, Te = (0, q.Cz)(W); + try { + if (J >= 0) { + const N = X(T.ticker || ""), w = await Y({ symbolInfo: T, resolution: W, rangeStartDate: te, rangeEndDate: J, tickerInfo: N, binanceInterval: Te, firstDataRequest: oe }); + O(w, { noData: w.length === 0 }); + } else O([], { noData: true }); + } catch (N) { + G(N.toString()); + } + }, subscribeBars: (T, W, _, O, G) => { + const te = X(T.ticker || ""), J = (0, q.Cz)(W), oe = H({ symbolInfo: T, resolution: W, callback: _, onResetCacheNeededCallback: G, tickerInfo: te, binanceInterval: J }); + t.set(O, oe); + }, unsubscribeBars: (T) => { + t?.get(T)?.(), t?.delete(T), j?.(T); + }, getServerTime: async (T) => { + try { + const W = await $(); + T(W); + } catch (W) { + console.warn(W), T(Date.now() / 1e3); + } + } }, { datafeed: u, observers: t, symbolsInfo: n }); + }, Le = ({ namespace: t, type: n }) => { + const a = (0, r.useMemo)(() => h().createInstance({ name: t }), [t]); + return (0, r.useCallback)(() => { + n === "tv" && a.removeItem("myTradingView"); + }, [a, n]); + }; + var Ue = e(76535), pr = e(26531), Ye = e(17409), Qe = e(94652), Ae = e(76830), he = e(48651), Fe = e(79344), se = e(51029), Je = e(36307), or = e(25379), yr = e(44745), Ne = e(15727), rr = e(45358), sr = e(82071), Ve = e(85411), ge = e(94889), hr = e(94547), tr = e(80686), Ar = e(61875); + function Fr(t) { + return (0, ge._)(t) || (0, hr._)(t) || (0, Ar._)(t) || (0, tr._)(); + } + var Nr = e(92873), zr = e(35630), Vr = e(12055), Qr = e(79515), Br = e(80737), Jr = e(40545), Zr = e(98107), Kr = e(51846), Xr = "Chart.Crosshair.PlusButton.DrawHorizontalLine", wr = function(t) { + var n = t.namespace, a = t.symbol, u = t.priceType, s = t.crosshairPrice, g = (0, Nr.o)("", "kline-ui").getI18n, S = (0, Vr.r)().format, K = k.y$(n), Y = (0, Br.S)({ namespace: n }), H = (0, Jr.hw)(), j = (0, Zr.c)(), $ = (0, Qr.nH)(), f = (0, se.Ye)(), l = (0, Kr.E)({ symbol: a }), T = (0, Fe.f0)(a), W = T.isCM, _ = (0, r.useRef)(s); + (0, r.useEffect)(function() { + _.current = s; + }, [s]); + var O = (0, r.useRef)(H); + (0, r.useEffect)(function() { + O.current = H; + }, [H]); + var G = (0, r.useRef)(j); + (0, r.useEffect)(function() { + G.current = j; + }, [j]); + var te = (0, r.useRef)(u); + (0, r.useEffect)(function() { + te.current = u; + }, [u]); + var J = (0, r.useRef)($); + (0, r.useEffect)(function() { + J.current = $; + }, [$]); + var oe = (0, r.useRef)(f); + (0, r.useEffect)(function() { + oe.current = f; + }, [f]); + var Te = (0, r.useRef)(l); + (0, r.useEffect)(function() { + Te.current = l; + }, [l]); + var N = (0, r.useRef)(W); + return (0, r.useEffect)(function() { + N.current = W; + }, [W]), { items_processor: (function() { + var w = (0, o._)(function(re, ie, le) { + var de, Be, ve, Ce, ce, Ke, Ze, Ge, Xe, Ie, He, ur, Tr, _r, Cr; + return (0, i._)(this, function(Er) { + if (Be = K.getState().tradingViewReference, !Be) return console.warn("[context-menu] trading view reference is not found"), [2, re]; + if (Be.chartsCount() > 1) return [2, re]; + if (ve = Be.activeChart().symbolExt(), !ve) return [2, re]; + switch (Ce = ve.name, ce = _.current, Ke = le.menuName === "CrosshairMenuView" ? "chart-trading" : "context-menu", Ze = { price: "".concat(ce.value), formattedPrice: ce.formatted, source: Ke, chartType: "tradingview" }, Ge = (de = oe.current[Ce]) === null || de === void 0 ? void 0 : de.baseAsset, Xe = g("trade", { defaultValue: "Trade" }), Ie = S(ce.formatted), He = G.current && te.current === c.SJ.Last && J.current && Ge ? [ie.createAction({ actionId: "Chart.PlaceOrder.BuyLimit", label: "".concat(Xe, " ").concat(Ge, " @ ").concat(Ie, " ").concat(g("limit", { defaultValue: "Limit" })), onExecute: function() { + return Te.current.onClickBuyLimit(Ze); + } }), ie.createAction({ actionId: "Chart.PlaceOrder.BuyStop", label: "".concat(Xe, " ").concat(Ge, " @ ").concat(Ie, " ").concat(g("stop", { defaultValue: "Stop" })), onExecute: function() { + return Te.current.onClickBuyStop(Ze); + } })] : [], ur = O.current ? ie.createAction({ actionId: "Chart.PriceAlert.CreateAtCrosshair", label: g("create-alert-at", { defaultValue: "Create Alert at {{price}}", price: ce.formatted }), onExecute: function() { + Y({ symbol: Ce, price: ce.formatted, mode: zr.n.CREATE }); + } }) : null, re.forEach(function(we) { + if (!(!("execute" in we) || !("getState" in we))) { + var Dr = we.getState(); + if (Dr.actionId === Xr) { + var z = we.execute.bind(we); + we.execute = function() { + (0, Kr.A)({ elementId: "draw_horizontal_line", isDelivery: N.current, chartType: "tradingview" }), z(); + }; + } + } + }), le.menuName) { + case "CrosshairMenuView": + return ur ? [2, [ur].concat((0, d._)(He), (0, d._)(re.slice()))] : [2, (0, d._)(He).concat((0, d._)(re.slice()))]; + case "ChartContextMenu": + return ur ? (Tr = Fr(re), _r = Tr[0], Cr = Tr.slice(1), [2, [_r, ur].concat((0, d._)(He), (0, d._)(Cr))]) : [2, (0, d._)(He).concat((0, d._)(re.slice()))]; + default: + return [2, re]; + } + return [2]; + }); + }); + return function(re, ie, le) { + return w.apply(this, arguments); + }; + })() }; + }, qr = function() { + var t = (0, r.useState)({ value: NaN, formatted: "" }), n = t[0], a = t[1], u = (0, r.useCallback)(function(s) { + var g = s.crosshairData, S = s.activeChart; + S && a({ value: g.price, formatted: S.priceFormatter().format(g.price) }); + }, []); + return { onCrosshairMoved: u, crosshairPrice: n }; + }; + const Hr = (t) => Math.floor(Math.log10(t)), et = (t) => { + const [n = "1"] = t.split(","); + return Hr(+n); + }, pe = ({ symbolInfo: t, minTick: n }) => { + const { pricescale: a = 2 } = t || {}; + return n === "default" || n === void 0 ? Hr(a) : et(n); + }, br = (t) => (n, a) => { + try { + return n ? { format: (u) => { + const s = pe({ symbolInfo: n, minTick: a }); + return t.format({ symbolInfo: n, minTick: a, value: u, precision: s }); + } } : null; + } catch { + return null; + } + }, Mr = 10 ** 12, rt = 10 ** 9, tt = 10 ** 6, jr = 10 ** 3, gr = (t) => { + const n = t.toString(), [, a = ""] = n.split("."); + return Math.min(a.length, 3); + }, nt = ({ value: t }) => { + const n = Math.abs(t); + if (n >= Mr) { + const a = t / Mr; + return { value: a, unit: "T", precision: gr(a) }; + } + if (n >= rt) { + const a = t / rt; + return { value: a, unit: "B", precision: gr(a) }; + } + if (n >= tt) { + const a = t / tt; + return { value: a, unit: "M", precision: gr(a) }; + } + if (n >= jr) { + const a = t / jr; + return { value: a, unit: "K", precision: gr(a) }; + } + return { value: t, unit: "", precision: gr(t) }; + }, ut = (t) => (n, a, u) => { + try { + return a && n.type === "volume" ? { format: (s) => s === void 0 ? "" : t.formatVolume({ format: n, symbolInfo: a, precision: u, value: s, volumeDetail: nt({ value: s }) }) } : null; + } catch { + return null; + } + }; + var lt = function() { + var t = (0, Vr.r)(), n = t.format, a = (0, r.useMemo)(function() { + return br({ format: function(s) { + var g = s.value, S = s.precision; + return n(g, { precision: S }); + } }); + }, [n]), u = (0, r.useMemo)(function() { + return ut({ formatVolume: function(s) { + var g = s.value, S = s.volumeDetail; + if (g === void 0 || !S) return ""; + var K = n(S.value, { precision: S.precision }); + return "".concat(K).concat(S.unit); + } }); + }, [n]); + return { priceFormatterFactory: a, studyFormatterFactory: u }; + }, dt = e(74069), ir = function(t) { + var n = (0, dt.K)(t), a = (0, r.useMemo)(function() { + return n.map(function(u) { + var s = u.id, g = u.time, S = u.isBuy, K = u.price, Y = u.tooltip; + return { id: s, text: "", price: K, quantity: "", time: Math.round(g / 1e3), isBuy: S, tooltip: Y }; + }); + }, [n]); + return { executionOrders: a }; + }, at = function(t) { + var n = t.namespace, a = t.symbol, u = (0, Br.j)({ namespace: n, symbol: a }); + return (0, r.useMemo)(function() { + return u.map(function(s) { + var g = s.id, S = s.price, K = s.formattedPrice, Y = s.sideText, H = s.showCloseButton, j = s.isPriceChangeable, $ = s.cancelTooltipText, f = s.onClick, l = s.onCancel, T = s.onPriceChanged; + return { id: g, price: +S, text: "".concat(Y, " ").concat(K), editable: j, cancellable: H, cancelTooltip: $, type: c.ND.PriceAlert, onCancel: H ? l : void 0, onMove: j ? function(W) { + var _ = W.price; + T({ currentPrice: _ }); + } : void 0, onClick: f }; + }); + }, [u]); + }, vt = e(90664), ot = function(t) { + var n = t.symbol, a = (0, vt.w)({ symbol: n }); + return (0, r.useMemo)(function() { + return a.map(function(u) { + var s = u.id, g = u.text, S = u.price; + return { id: s, text: g, price: +S, type: c.ND.BreakEvenPrice }; + }); + }, [a]); + }, cr = e(82508), ft = e(91025), mt = e(36077), Rr = function(t) { + var n = t.symbol, a = (0, mt.c)({ symbol: n }); + return (0, r.useMemo)(function() { + return a.map(function(u) { + return (0, ft._)((0, cr._)({}, u), { type: c.ND.LiquidationPrice }); + }); + }, [a]); + }, pt = function(t) { + var n = t.namespace, a = t.symbol, u = at({ namespace: n, symbol: a }), s = ot({ symbol: a }), g = Rr({ symbol: a }); + return (0, r.useMemo)(function() { + return (0, d._)(u).concat((0, d._)(s), (0, d._)(g)); + }, [u, s, g]); + }, st = e(42902), Ir = e(90170), yt = e(63345), ht = e(48210), Pr = e(51289), xr = e(24326), $r = e(90922), kr = e(34235), it = e(63762), Ft = function(t) { + var n = (0, Fe.f0)(t), a = n.pricePrecision, u = n.isCM, s = (0, st.yD)(t) || {}, g = s.lastPrice, S = g === void 0 ? "0" : g, K = (0, kr.Bp)(u), Y = (0, it.HY)(t), H = (0, kr.gv)(u), j = (0, kr.WF)(u); + return (0, r.useMemo)(function() { + return (0, $r.qF)({ orders: (0, d._)(K).concat((0, d._)(H), (0, d._)(j)), symbol: t }).map(function($) { + return Y({ order: $, lastPrice: S, pricePrecision: a }); + }); + }, [S, K, H, a, t, Y]); + }, gt = e(31542), Tt = function(t) { + var n = t.type; + (0, gt.u4)("webClick", { module: "chart", elementId: n }); + }, Nt = function(t) { + var n = t.symbol, a = t.namespace, u = (0, r.useState)(0), s = u[0], g = u[1], S = (0, r.useCallback)(function() { + return g(function(N) { + return N + 1; + }); + }, []), K = Ft(n), Y = (0, xr.IT)({ namespace: a }), H = (0, Fe.f0)(n), j = H.isDelivery, $ = H.contractType, f = (0, st.yD)(n) || {}, l = f.lastPrice, T = l === void 0 ? "0" : l, W = (0, yt.s)(n), _ = (0, it.YZ)({ symbol: n, afterConfirmationModalIsShown: S }), O = (0, it.u8)(n), G = (0, it.jn)(n), te = (0, r.useCallback)(function(N) { + return (0, ht.D)({ order: N, isDelivery: j, symbol: j ? N.symbol : "".concat(N.symbol, "_").concat($), module: "tradingview_kline" }); + }, [$, j]), J = (0, r.useMemo)(function() { + return K.map(function(N) { + var w = N.order, re = N.price, ie = N.quantity, le = N.quantityText, de = N.isBuy, Be = N.text, ve = N.priceTooltipText, Ce = N.pnlText, ce = (0, Pr.$L)(w), Ke = (0, Pr.OX)(w), Ze = (0, Pr.xL)(w), Ge = ce === "LIMIT", Xe = (0, Ir.Y)(w); + return { id: String(Ke), text: Be, price: +re, quantity: le, isBuy: de, editable: Xe && G, tooltip: ve, modifyTooltip: w.closePosition || !Ge ? "" : void 0, onMove: G ? function(Ie) { + var He = Ie.price; + te(w), Tt({ type: "TV_vertical" }), _({ order: w, price: +He, quantity: +Ze, lastPrice: T }); + } : void 0, onModify: w.closePosition || !Ge || !Xe ? void 0 : function() { + Tt({ type: "TV_amountclick" }), O({ order: w, price: re.toString(), quantity: ie, lastPrice: T }); + }, onCancel: function() { + Tt({ type: "TV_cancel" }), W(w); + } }; + }); + }, [G, K, T, te, _, O, W, s]), oe = (0, r.useMemo)(function() { + return Y.map(function(N) { + var w = N.id, re = N.value, ie = N.text, le = N.onChange, de = N.isBuy, Be = N.type; + return { id: w, price: +re, quantity: "", text: ie, isBuy: de, editable: true, limitOrderType: c.aA.OrderPreview, onMove: function(ve) { + var Ce = ve.price; + (0, xr.eP)({ namespace: a, orderType: Be }), le({ id: w, newPrice: Ce }); + }, onMoving: function(ve) { + var Ce = ve.price; + return le({ id: w, newPrice: Ce }); + } }; + }); + }, [Y, a]), Te = (0, r.useMemo)(function() { + return (0, d._)(J).concat((0, d._)(oe)); + }, [J, oe]); + return { orderLines: Te }; + }, Vt = e(17855), Bt = function(t, n) { + var a = (0, r.useState)(t()), u = a[0], s = a[1]; + return (0, Vt.A)(function() { + return s(t()); + }, n), u; + }, Kt = e(58656), Ht = function(t) { + var n = t.symbol, a = (0, Kt.Q)({ symbol: n }) || [], u = Bt(function() { + return a.map(function(s) { + var g = s.position, S = s.pnl, K = s.pnlInNumber, Y = s.pnlText, H = s.price, j = s.quantity, $ = s.reverseText, f = s.isBuy, l = s.displayReverseOrderModal, T = s.displayClosePositionModal; + return { id: g.id || "".concat(g.symbol, "-").concat(g.positionSide), PNL: K, text: "".concat(Y, " ").concat(S), price: H, quantity: j, tooltip: "", reverseTooltip: $, isBuy: f, onReverse: l, onClose: function() { + T(), (0, gt.u4)("$WebClick", { item: 1, module: "xxx", $element_id: "kline_close_position" }); + } }; + }); + }, [a]); + return { positionLines: u }; + }, jt = e(47354), bt = e.n(jt), $t = e(87017), Ut = e(99168), Yt = e(3742), Wt = e(43335), Gt = e(15540), zt = e(20163), Qt = e(47255); + const Jt = (t) => ({ endTime: n }) => (n ?? 0) === t; + var Zt = e(36488), Ct = e(48018), Xt = e(82911), We = e(69578), wt = e(53208), Rt = e(19862), qt = e(8874), en = e(26679), rn = e(66547), It = (function() { + var t = (0, o._)(function(n) { + var a, u, s, g, S, K, Y, H, j; + return (0, i._)(this, function($) { + switch ($.label) { + case 0: + a = n.onFetchKlineData, u = n.startTime, s = n.endTime, g = n.interval, S = n.limit, K = S === void 0 ? 1e3 : S, Y = [], H = [], j = s, $.label = 1; + case 1: + return [4, a((0, cr._)({ interval: g, limit: K }, j === 0 ? {} : { endTime: j }))]; + case 2: + if (H = $.sent(), H.length === 0) return [3, 4]; + H.forEach(function(f) { + var l = (0, v._)(f, 8), T = l[0], W = l[1], _ = l[2], O = l[3], G = l[4], te = l[5], J = l[7]; + Y.push({ time: +T, open: +W, high: +_, low: +O, close: +G, volume: +te, quantity: +J }); + }), j = H[0][0] - 1, $.label = 3; + case 3: + if (j > u) return [3, 1]; + $.label = 4; + case 4: + return Y.sort(function(f, l) { + return f.time - l.time; + }), [2, Y]; + } + }); + }); + return function(a) { + return t.apply(this, arguments); + }; + })(), tn = function(t) { + var n = t.firstDataRequest, a = t.hasRequested, u = t.rangeEndDate; + return n && !a ? 0 : u * 1e3; + }, nn = e(78142), an = e(16238), St = e.n(an), on = e(98087), xt = e.n(on), sn = e(55009), cn = e.n(sn), kt = e(56477); + const Et = (t) => cn()(t.chartsCount(), (n) => t.chart(n)), un = async (t) => { + try { + const { schema: n, data: a } = await t.exportData({ includedStudies: [] }), u = xt()(a)?.[n.findIndex((s) => s.type === "time")]; + return u !== void 0 && Number.isFinite(u) ? u * 1e3 : void 0; + } catch { + return; + } + }, ln = ({ getWidget: t, ...n }) => { + const a = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Map(), s = () => { + try { + const f = t(); + return f ? St()(Et(f).map((l) => l.symbolExt()?.ticker)) : []; + } catch { + return []; + } + }, g = (f) => { + const l = s(); + return !l.length || l.includes(f); + }, S = (f) => { + const l = u.get(f); + if (l) return l; + const T = { controller: new AbortController(), claimedAsMain: false }; + return u.set(f, T), T; + }, K = (f) => { + const l = u.get(f); + l && (l.controller.abort(), u.delete(f)); + }, Y = () => { + const f = s(); + f.length && Array.from(u.keys()).forEach((l) => { + const T = u.get(l); + if (T) { + if (f.includes(l)) { + T.claimedAsMain = true; + return; + } + a.has(l) || T.claimedAsMain && K(l); + } + }); + }, H = (f, l) => { + f.closed || l.time < f.highWater || (f.highWater = l.time, f.options.onTick(l), f.detector?.onBar(l), g(f.options.ticker) && $.onBar(l)); + }, j = async (f, l) => { + f.held = []; + let T = l; + try { + const _ = (await f.options.fetchSince(l)).filter((O) => O.time >= l && !f.closed); + _.forEach((O) => { + f.highWater = Math.max(f.highWater, O.time), f.options.onTick(O); + }), T = xt()(_)?.time ?? l; + } finally { + const W = f.held ?? []; + f.held = void 0, W.filter((_) => _.time >= T).forEach((_) => { + H(f, _); + }); + } + }, $ = (0, kt.k)({ ...n, onCheck: Y, repair: async () => { + const f = t(); + if (f) try { + const l = Et(f), T = St()(l.map((_) => { + const O = a.get(_.symbolExt()?.ticker ?? ""); + return O && { chart: _, feed: O }; + })); + if (!T.length) { + f.resetCache(), l.forEach((_) => { + _.resetData(); + }); + return; + } + const W = St()(await Promise.all(T.map(async ({ chart: _, feed: O }) => { + const G = await un(_); + return G === void 0 ? void 0 : { feed: O, fromMs: G }; + }))); + await Promise.allSettled(W.map(({ feed: _, fromMs: O }) => j(_, O))); + } catch { + } + } }); + return { openFeed: (f) => { + const l = { options: f, held: void 0, highWater: 0, closed: false, detector: void 0 }; + return l.detector = (0, kt.k)({ ...n, subscribeSignals: f.subscribeSignals, repair: async () => { + l.closed || !l.highWater || g(f.ticker) || await j(l, l.highWater); + } }), a.set(f.ticker, l), g(f.ticker) && $.resubscribeSignals(f.subscribeSignals), { push: (T) => { + l.held ? l.held.push(T) : H(l, T); + }, close: () => { + l.closed = true, l.detector?.dispose(), a.get(f.ticker) === l && a.delete(f.ticker); + } }; + }, subscribeWake: $.subscribeWake, getSignal: (f) => { + const l = S(f); + return s().includes(f) && (l.claimedAsMain = true), l.controller.signal; + }, dispose: () => { + u.forEach(({ controller: f }) => { + f.abort(); + }), u.clear(), a.forEach((f) => { + f.detector?.dispose(); + }), a.clear(), $.dispose(); + } }; + }; + var dn = e(71730), vn = e(36620), fn = function(t) { + var n = t.namespace, a = k.y$(n), u = (0, vn.l)().getSocketSignals, s = (0, r.useRef)(); + (0, r.useEffect)(function() { + return function() { + var S; + (S = s.current) === null || S === void 0 || S.instance.dispose(), s.current = void 0; + }; + }, []); + var g = (0, r.useCallback)(function(S) { + var K, Y, H = S; + if (((K = s.current) === null || K === void 0 ? void 0 : K.key) === H) return s.current.instance; + (Y = s.current) === null || Y === void 0 || Y.instance.dispose(); + var j = nn.wr(S), $ = ln({ getWidget: function() { + return a.getState().tradingViewReference; + }, checkPeriodMs: dn.D(j) }); + return s.current = { key: H, instance: $ }, $; + }, [a]); + return (0, r.useMemo)(function() { + return { ensureRecovery: g, getSocketSignals: u }; + }, [g, u]); + }, mn = "tv_web_error", pn = [Ye.bt.TRADING, Ye.bt.DELIVERING], Re; + (function(t) { + t.Futures = "futures", t.FuturesMark = "futures-mark", t.FuturesIndex = "futures-index", t.Delivery = "delivery", t.DeliveryMark = "delivery-mark", t.DeliveryIndex = "delivery-index"; + })(Re || (Re = {})); + var Mt = function(t) { + var n = t.symbol, a = t.description, u = t.productType, s = t.pricePrecision, g = t.params, S = t.supportedResolutions, K = S === void 0 ? F : S; + return { symbol: n, description: a, tickSize: Math.pow(10, s), exchange: "Binance", supportedResolutions: K, type: u, params: g }; + }, yn = function(t) { + var n = t.namespace, a = t.useMiniTicker, u = a === void 0 ? false : a, s = t.isCM, g = c.y$(n), S = k.y$(n), K = (0, $t.useQueryClient)(), Y = fn({ namespace: n }), H = Y.ensureRecovery, j = Y.getSocketSignals, $ = (0, r.useRef)(0), f = (0, r.useRef)(false), l = (0, r.useMemo)(function() { + return bt()(function() { + return (0, We.HP)({ eventId: "trd_chart_tv_load_timeout", stepId: "getBarsStart", catch: function(Q) { + var ee = Q.error, ae = Q.track; + ae({ GET_BAR_START_TYPE: ee }); + }, log: function(Q) { + var ee = Q.track; + ee({ GET_BAR_START_TYPE: "SUCCESS" }); + } }); + }); + }, []), T = (0, r.useMemo)(function() { + return bt()(function(z) { + return (0, We.HP)({ eventId: "trd_chart_tv_load_timeout", stepId: "getBarsEnd", catch: function(ee) { + var ae = ee.error, Se = ee.track; + Se({ GET_BAR_END_TYPE: ae, duration_: (0, We.Pu)(z - $.current) }); + }, log: function(ee) { + var ae = ee.track; + ae({ GET_BAR_END_TYPE: "SUCCESS", duration_: (0, We.Pu)(z - $.current) }); + } }); + }); + }, []), W = (0, Nr.o)("trd-chart", "trade-ui"), _ = W.getI18n, O = (0, he.ud)(), G = O.getI18n, te = (0, v._)(g(c.Ir), 1), J = te[0], oe = (0, v._)(S(k.A7), 2), Te = oe[1], N = (0, se.Ye)(), w = (0, Zt.JD)(s).isContUnit, re = (0, se.y_)(), ie = (0, r.useRef)(N), le = (0, r.useRef)(J), de = (0, r.useRef)(re); + (0, r.useEffect)(function() { + ie.current = N; + }, [N]), (0, r.useEffect)(function() { + le.current = J; + }, [J]), (0, r.useEffect)(function() { + de.current = re; + }, [re]); + var Be = (0, r.useMemo)(function() { + return w ? Rt.b : Rt.S; + }, [w]), ve = (0, r.useMemo)(function() { + return G("usdtFutures-13", { defaultValue: "Futures" }) || ""; + }, [G]), Ce = (0, r.useMemo)(function() { + return G("inverseFutures-13", { defaultValue: "Delivery" }) || ""; + }, [G]), ce = (0, r.useMemo)(function() { + return _("indexPrice", { defaultValue: "Index Price" }) || ""; + }, [_]), Ke = (0, r.useMemo)(function() { + return _("markPrice", { defaultValue: "Mark Price" }) || ""; + }, [_]), Ze = (0, r.useMemo)(function() { + return _("lastPrice", { defaultValue: "Last Price" }) || ""; + }, [_]), Ge = (0, r.useMemo)(function() { + return { exchanges: [{ name: "Binance", value: "Binance", desc: "" }], symbols_types: [{ name: ve, value: Re.Futures }, { name: "".concat(ve, " ").concat(Ke), value: Re.FuturesMark }, { name: "".concat(ve, " ").concat(ce), value: Re.FuturesIndex }, { name: Ce, value: Re.Delivery }, { name: "".concat(Ce, " ").concat(Ke), value: Re.DeliveryMark }, { name: "".concat(Ce, " ").concat(ce), value: Re.DeliveryIndex }] }; + }, [Ce, ve, ce, Ke]), Xe = (0, r.useCallback)((0, o._)(function() { + var z, Q, ee; + return (0, i._)(this, function(ae) { + switch (ae.label) { + case 0: + return ae.trys.push([0, 2, , 3]), [4, (0, en.c)(K)]; + case 1: + return ae.sent(), [3, 3]; + case 2: + return z = ae.sent(), (0, gt.u4)(mn, { type: "onFetchSymbolsInfo", message: z.message, business: "futures" }), [3, 3]; + case 3: + return Q = ie.current, ee = Object.keys(Q).reduce(function(Se, xe) { + var ne = Q[xe] || Ct.o, ke = ne.symbol, Me = ne.pricePrecision, fe = ne.isDelivery, Pe = ne.contractStatus; + if (!pn.includes(Pe)) return Se; + var _e = (0, Yt.S)(ne, G), je = _e.symbol, Ee = _e.contractTypeI18n, me = _e.deliveryDateFormat, ye = "".concat(je, " ").concat(Ee).concat(me ? " ".concat(me) : ""), $e = "".concat(ye, " ").concat(Ze), Oe = "".concat(ye, " ").concat(ce), lr = "".concat(ye, " ").concat(Ke); + return Se.concat([Mt({ symbol: ke, description: $e, productType: fe ? Re.Delivery : Re.Futures, pricePrecision: Me, params: { priceType: c.SJ.Last } }), Mt({ symbol: ke, description: Oe, productType: fe ? Re.DeliveryIndex : Re.FuturesIndex, pricePrecision: Me, params: { priceType: c.SJ.Index } }), Mt({ symbol: ke, description: lr, productType: fe ? Re.DeliveryMark : Re.FuturesMark, pricePrecision: (0, Xt.X)(Me), params: { priceType: c.SJ.Mark } })]); + }, []), [2, ee]; + } + }); + }), [G, ce, Ke, Ze, K]), Ie = (0, r.useCallback)(function(z) { + var Q = z.ticker, ee = z.symbol, ae = z.interval, Se = z.priceType, xe = z.retryUntil, ne = ie.current[ee] || Ct.o, ke = ne.pair, Me = ne.contractType, fe = ne.contractVal, Pe = ne.isDelivery, _e = Pe ? Be(fe) : function(Oe) { + return Oe; + }, je = (0, wt.h)({ symbol: ee, pair: ke, contractType: Me, isDelivery: Pe, priceType: Se }), Ee, me, ye = function(Oe) { + return zt.y("tv", "".concat(Pe ? "cm" : "um", "|").concat(ee, "|").concat(Se, "|").concat(Oe.interval, "|").concat((Ee = Oe.limit) !== null && Ee !== void 0 ? Ee : "", "|").concat((me = Oe.endTime) !== null && me !== void 0 ? me : "")); + }; + if (xe === void 0) return { transform: _e, onFetchKlineData: je }; + var $e = H(ae); + return { transform: _e, onFetchKlineData: Qt.b(je, { enabled: Jt(xe), key: ye, signal: $e.getSignal(Q), subscribeWake: $e.subscribeWake }) }; + }, [H, Be]), He = (0, r.useCallback)((function() { + var z = (0, o._)(function(Q) { + var ee, ae, Se, xe, ne, ke, Me, fe, Pe, _e, je, Ee, me, ye, $e, Oe, lr, Or; + return (0, i._)(this, function(dr) { + switch (dr.label) { + case 0: + return ee = Q.binanceInterval, ae = Q.rangeStartDate, Se = Q.rangeEndDate, xe = Q.tickerInfo, ne = Q.symbolInfo, ke = Q.firstDataRequest, $.current = Date.now(), l(), Me = xe.symbol, fe = Me === void 0 ? "" : Me, Pe = xe.params, _e = Pe.priceType, je = _e === void 0 ? le.current : _e, de.current({ symbol: fe, priceType: je }) ? (T(Date.now()), [2, []]) : (Ee = Math.max(0, ae * 1e3), me = tn({ firstDataRequest: ke, hasRequested: f.current, rangeEndDate: Se }), f.current = true, $e = Ie({ ticker: (ye = ne.ticker) !== null && ye !== void 0 ? ye : "", symbol: fe, interval: ee, priceType: je, retryUntil: ke ? me : void 0 }), Oe = $e.transform, lr = $e.onFetchKlineData, [4, It({ onFetchKlineData: lr, startTime: Ee, endTime: me, interval: ee })]); + case 1: + return Or = dr.sent(), T(Date.now()), [2, Or.map(Oe)]; + } + }); + }); + return function(Q) { + return z.apply(this, arguments); + }; + })(), [Ie, T, l]), ur = (0, r.useCallback)(function(z) { + var Q = z.binanceInterval, ee = z.callback, ae = z.tickerInfo, Se = z.symbolInfo, xe = ae.symbol, ne = xe === void 0 ? "" : xe, ke = ae.params, Me = ke.priceType, fe = Me === void 0 ? le.current : Me; + if (de.current({ symbol: ne, priceType: fe })) return function() { + return null; + }; + var Pe = ie.current[ne] || Ct.o, _e = Pe.pair, je = Pe.contractType, Ee = Pe.isDelivery, me, ye = (me = Se.ticker) !== null && me !== void 0 ? me : "", $e = Ie({ ticker: ye, symbol: ne, interval: Q, priceType: fe }).transform, Oe = H(Q), lr = Oe.openFeed({ ticker: ye, onTick: ee, subscribeSignals: j(Ee), fetchSince: (function() { + var dr = (0, o._)(function(Lr) { + var Ur, Yr, ct, De; + return (0, i._)(this, function(ze) { + switch (ze.label) { + case 0: + return Ur = Ie({ ticker: ye, symbol: ne, interval: Q, priceType: fe, retryUntil: 0 }), Yr = Ur.transform, ct = Ur.onFetchKlineData, [4, It({ onFetchKlineData: ct, startTime: Lr, endTime: 0, interval: Q })]; + case 1: + return De = ze.sent(), [2, De.map(Yr)]; + } + }); + }); + return function(Lr) { + return dr.apply(this, arguments); + }; + })() }), Or = (0, rn.B)({ symbol: ne, pair: _e, contractType: je, interval: Q, priceType: fe, isDelivery: Ee, useMiniTicker: u, callback: function(dr) { + lr.push($e(dr)); + } }); + return function() { + lr.close(), Or(); + }; + }, [Ie, H, j, u]), Tr = (0, r.useCallback)((0, o._)(function() { + var z; + return (0, i._)(this, function(Q) { + switch (Q.label) { + case 0: + return [4, K.fetchQuery(Wt.Bz.SERVER_TIME(s), function() { + return (0, Gt.l)(s); + }, {})]; + case 1: + return z = Q.sent(), [2, Math.floor(z / 1e3)]; + } + }); + }), [K, s]), _r = (0, r.useCallback)(function(z) { + return (0, cr._)({}, z.template, (0, qt.sp)(s)); + }, [s]), Cr = (0, r.useMemo)(function() { + return ue(); + }, []), Er = Cr({ config: Ge, onFetchSymbolsInfo: Xe, onFetchBars: He, onSubscribeBars: ur, onFetchServerTimeApi: Tr, onTransformSymbolInfo: _r }), we = Er.datafeed, Dr = Er.symbolsInfo; + return (0, Ut.A)(function() { + return Te(); + }, [w]), { datafeed: we, symbolsInfo: Dr }; + }, Pt = "trd_chart_tv_load_timeout", hn = function() { + var t = (0, We.PM)({ id: "chartReady" }), n = (0, We.PM)({ id: "getBarsStart", timeout: 3 }), a = (0, We.PM)({ id: "getBarsEnd", timeout: 6 }), u = (0, We.PM)({ id: "chartResponsive", timeout: 3 }); + We.HP.init({ step: t._(n)._(a)._(u), eventId: Pt }), (0, We.HP)({ eventId: Pt, stepId: "chartReady", catch: function(g) { + var S = g.error, K = g.track; + K({ CHART_READY_TYPE: S, connection: JSON.stringify(navigator.connection) }); + }, log: function(g) { + var S = g.track; + S({ CHART_READY_TYPE: "SUCCESS", connection: JSON.stringify(navigator.connection) }); + } }); + }, gn = function(t) { + (0, We.HP)({ eventId: Pt, stepId: "chartResponsive", catch: function(a) { + var u = a.error, s = a.track; + s({ CHART_RESPONSIVE_TYPE: u, connection: JSON.stringify(navigator.connection) }); + }, log: function(a) { + var u = a.track; + u({ CHART_RESPONSIVE_TYPE: "SUCCESS", duration_: (0, We.Pu)(Date.now() - t) }); + } }); + }, Tn = [Qe.ZE, Qe.F3], Dt = function(t) { + var n = t.symbol, a = t.namespace, u = t.useMiniTicker, s = u === void 0 ? false : u, g = c.y$(a), S = k.y$(a), K = (0, v._)(g(c.A8), 1), Y = K[0], H = (0, v._)(g(c.Ir), 2), j = H[0], $ = H[1], f = (0, v._)(S(k.yj), 1), l = f[0], T = (0, v._)(g(c.e6), 1), W = T[0], _ = (0, v._)(g(c._b), 1), O = _[0], G = (0, he.ot)(), te = (0, pr.K7)(), J = te.pathname.includes(Ae.Q2), oe = te.pathname.includes(Ae.eq), Te = (0, r.useMemo)(function() { + return J ? (0, d._)(B.oT).concat(["hide_left_toolbar_by_default"]) : B.oT; + }, [J]), N = (0, se.Ye)(), w = (0, se.v3)({ symbol: n }), re = (0, Fe.es)(n), ie = yn({ namespace: a, useMiniTicker: s, isCM: re }), le = ie.datafeed, de = ie.symbolsInfo, Be = (0, Je.DP)(), ve = Be.theme, Ce = ve === "light" || ve === "light_glacier" ? "light" : "dark", ce = (0, yr.Z)(Ce), Ke = (0, r.useRef)(-1), Ze = Ht({ symbol: n }).positionLines, Ge = Nt({ symbol: n, namespace: a }).orderLines, Xe = ir({ symbol: n }).executionOrders, Ie = pt({ namespace: a, symbol: n }), He = (0, se.y0)(), ur = (0, r.useRef)(He), Tr = (0, r.useRef)(N), _r = (0, r.useRef)(l), Cr = (0, r.useRef)(J), Er = (0, r.useRef)(oe), we = (0, r.useRef)(de); + (0, r.useEffect)(function() { + we.current = de; + }, [de]), (0, r.useEffect)(function() { + ur.current = He; + }, [He]), (0, r.useEffect)(function() { + Tr.current = N; + }, [N]), (0, r.useEffect)(function() { + _r.current = l; + }, [l]), (0, r.useEffect)(function() { + Cr.current = J; + }, [J]), (0, r.useEffect)(function() { + Er.current = oe; + }, [oe]), (0, Ve.a)(n, s), (0, sr.A)({ symbol: n, interval: Y, priceType: j }); + var Dr = (0, r.useCallback)(function() { + Ke.current = Date.now(); + }, []), z = (0, r.useCallback)((0, o._)(function() { + var De, ze, qe, be, vr, fr, er; + return (0, i._)(this, function(nr) { + switch (nr.label) { + case 0: + if (Cr.current || !_r.current) return [2, Promise.resolve()]; + nr.label = 1; + case 1: + return nr.trys.push([1, 5, , 6]), De = h().createInstance({ name: a }), [4, De.keys()]; + case 2: + return ze = nr.sent(), qe = ze.filter(function(mr) { + return /^((#TV_SYMBOL-)|(myTradingView))/g.test(mr); + }), be = qe.map((function() { + var mr = (0, o._)(function(ar) { + return (0, i._)(this, function(Sr) { + return [2, De.getItem(ar)]; + }); + }); + return function(ar) { + return mr.apply(this, arguments); + }; + })()), [4, Promise.all(be)]; + case 3: + return vr = nr.sent(), fr = U()(qe, vr), [4, Promise.all(Tn.map((function() { + var mr = (0, o._)(function(ar) { + var Sr, Wr; + return (0, i._)(this, function(Gr) { + switch (Gr.label) { + case 0: + return a === ar ? [2, Promise.resolve()] : (Sr = h().createInstance({ name: ar }), Wr = __dispatchChartMirror(Sr, fr, () => fr.map((function() { + var Sn = (0, o._)(function(Lt) { + var _t, Ot, At; + return (0, i._)(this, function(Mn) { + return _t = (0, v._)(Lt, 2), Ot = _t[0], At = _t[1], Ot ? [2, Sr.setItem(Ot, At)] : [2, Promise.resolve()]; + }); + }); + return function(Lt) { + return Sn.apply(this, arguments); + }; + })())), [4, Promise.all(Wr)]); + case 1: + return Gr.sent(), [2, Promise.resolve()]; + } + }); + }); + return function(ar) { + return mr.apply(this, arguments); + }; + })()))]; + case 4: + return nr.sent(), [2, Promise.resolve()]; + case 5: + return er = nr.sent(), console.warn(typeof er == "string" ? er : er.message), [2, Promise.resolve()]; + case 6: + return [2]; + } + }); + }), [a]), Q = (0, r.useMemo)(function() { + return z; + }, [z]), ee = (0, or.R)(), ae = (0, Ne.fQ)(a), Se = (0, Ne.m3)(a), xe = (0, Ne.S_)(a), ne = (0, Ne.Ds)(a), ke = (0, r.useRef)(n), Me = (0, r.useRef)(Y), fe = (0, r.useCallback)(function() { + return ee("tradingView"), xe({ panel_index: 0, symbol: n, interval: Y }), hn(); + }, [Y, ee, n, xe]), Pe = (0, r.useCallback)(function(De) { + var ze = De.index, qe = De.chart, be, vr, fr, er = (fr = (vr = (be = qe.symbolExt()) === null || be === void 0 ? void 0 : be.name) !== null && vr !== void 0 ? vr : qe.symbol()) !== null && fr !== void 0 ? fr : ""; + ae({ panel_index: ze, symbol: er, previous_symbol: ke.current }), ke.current = er; + }, [ae]), _e = (0, r.useCallback)(function(De) { + var ze = De.index, qe = De.chart, be = qe.resolution(); + Se({ panel_index: ze, interval: be, previous_interval: Me.current }), Me.current = be; + }, [Se]), je = (0, r.useCallback)(function() { + gn(Ke.current); + }, []), Ee = (0, r.useCallback)(function() { + }, []), me = (0, r.useMemo)(function() { + return J ? Ee : void 0; + }, [Ee, J]), ye = (0, r.useCallback)((function() { + var De = (0, o._)(function(ze) { + var qe, be, vr, fr, er, nr, mr, ar, Sr; + return (0, i._)(this, function(Wr) { + switch (Wr.label) { + case 0: + return qe = ze.chart, be = qe.symbolExt(), be ? (vr = b()(we.current, function(Gr) { + return Gr.ticker === be.ticker || Gr.symbol === be.ticker; + }), fr = vr || {}, er = fr.params, nr = er === void 0 ? {} : er, mr = nr.priceType, ar = mr === void 0 ? "" : mr, ar && $(ar), Sr = Tr.current[be.name], Sr ? Er.current || Cr.current ? [2] : [4, z()] : [2]) : [2]; + case 1: + return Wr.sent(), G({ stateSymbol: Sr.symbol, isDeliveryColumn: Sr.isDelivery }), [2]; + } + }); + }); + return function(ze) { + return De.apply(this, arguments); + }; + })(), [G, z, $]), $e = qr(), Oe = $e.onCrosshairMoved, lr = $e.crosshairPrice, Or = wr({ namespace: a, symbol: n, priceType: j, crosshairPrice: lr }), dr = lt(), Lr = (0, r.useMemo)(function() { + return L({ symbol: n, params: { priceType: j } }); + }, [n, j]), Ur = (0, r.useMemo)(function() { + return { symbol: Lr, theme: ce, limitOrders: Ge, positionOrders: Ze, executionOrders: Xe, labelLines: Ie, initialConfig: { tvConfig: { symbol: Lr, library_path: "/".concat(Ye.u_, "/trading-platform-30/"), theme: ce, datafeed: le, enabled_features: Te, context_menu: Or, custom_formatters: dr }, onCrosshairMoved: Oe, onScriptLoaded: Dr, onSave: Q, onChartReadyDone: fe, onFirstTimeDataLoadedDone: je, onInitChart: me, onActiveChartChanged: ye, onSymbolChanged: Pe, onIntervalChanged: _e } }; + }, [Lr, le, Te, Xe, Ie, Or, dr, Oe, ye, fe, je, me, _e, Q, Dr, Pe, Ge, Ze, ce]), Yr = Le({ namespace: a, type: "tv" }), ct = (0, r.useCallback)(function() { + ne({ panel_index: 0, symbol: n }), Yr(); + }, [Yr, n, ne]); + return (0, p.jsxs)(D.t, { name: "trading_view", onReset: ct, children: [(0, p.jsx)(Ue.A, { namespace: a, tradingViewProps: Ur }), w && j === "index" && O === c.Ev.Single && W === c.tU.TradingView && (0, p.jsx)(rr.B, {})] }); + }; + const Cn = Dt; + } }[70940]; + } + function replaceChartMirrorFactory(originalFactory, dispatch) { + if (typeof originalFactory !== "function" || Function.prototype.toString.call(originalFactory) !== originalFactorySource) { + throw new Error("Mirror factory source does not match the pinned public module"); + } + return createMirrorFactory(dispatch); + } + + // src/binance-orderbook-trade/chart-storage/json.js + var encoder = new TextEncoder(); + var nativeObjectSource = Function.prototype.toString.call(Object); + function assert(condition, message) { + if (!condition) throw new Error(message); + } + function isPlainJsonObject(value) { + const prototype = Object.getPrototypeOf(value); + if (prototype === null || Object.getPrototypeOf(prototype) !== null) return false; + const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor"); + if (!constructor || !Object.hasOwn(constructor, "value") || typeof constructor.value !== "function") return false; + if (Function.prototype.toString.call(constructor.value) !== nativeObjectSource) return false; + const descriptor = Object.getOwnPropertyDescriptor(constructor.value, "prototype"); + return !!descriptor && Object.hasOwn(descriptor, "value") && descriptor.value === prototype; + } + function byteLength(value) { + return encoder.encode(JSON.stringify(value)).byteLength; + } + function snapshotJson(value, budget = { remaining: Infinity }, seen = /* @__PURE__ */ new Set()) { + function consume(bytes) { + budget.remaining -= bytes; + assert(budget.remaining >= 0, "Chart JSON byte limit exceeded"); + } + if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") { + assert(typeof value !== "number" || Number.isFinite(value), "Chart JSON numbers must be finite"); + consume(byteLength(value)); + return value; + } + assert(typeof value === "object", "Chart values must be JSON"); + assert(!seen.has(value), "Chart JSON must not contain shared references or cycles"); + const array = Array.isArray(value); + assert(array || isPlainJsonObject(value), "Chart values must be plain JSON"); + const descriptors = Object.getOwnPropertyDescriptors(value); + assert(Object.getOwnPropertySymbols(value).length === 0, "Chart JSON must not contain symbol properties"); + const keys = Object.keys(descriptors).filter((key) => !(array && key === "length")); + if (array) assert(keys.length === value.length && keys.every((key, index) => key === String(index)), "Chart JSON arrays must be dense"); + seen.add(value); + consume(2 + Math.max(0, keys.length - 1)); + const result = array ? [] : {}; + for (const key of keys) { + const descriptor = descriptors[key]; + assert(descriptor.enumerable && Object.hasOwn(descriptor, "value"), "Chart JSON requires enumerable data properties"); + if (!array) consume(byteLength(key) + 1); + Object.defineProperty(result, key, { + value: snapshotJson(descriptor.value, budget, seen), + enumerable: true, + writable: true, + configurable: true + }); + } + return result; + } + function equalJson(first, second) { + if (Object.is(first, second)) return true; + if (first === null || second === null || typeof first !== "object" || typeof second !== "object") return false; + if (Array.isArray(first) !== Array.isArray(second)) return false; + const firstKeys = Object.keys(first); + const secondKeys = Object.keys(second); + return firstKeys.length === secondKeys.length && firstKeys.every((key, index) => key === secondKeys[index] && equalJson(first[key], second[key])); + } + + // src/binance-orderbook-trade/chart-storage/mirror-writer.js + var DEFAULT_LIMITS = Object.freeze({ + maxEntries: 512, + maxBatchBytes: 4 * 1024 * 1024, + maxReadBytes: 4 * 1024 * 1024, + maxPendingBatches: 16, + maxPendingBytes: 64 * 1024 * 1024 + }); + function assert2(condition, message) { + if (!condition) throw new Error(message); + } + function createChartMirrorWriter({ limits: suppliedLimits = {} } = {}) { + const limits = { ...DEFAULT_LIMITS, ...suppliedLimits }; + for (const [key, value] of Object.entries(limits)) { + assert2(Object.hasOwn(DEFAULT_LIMITS, key), `Unknown mirror limit: ${key}`); + assert2(Number.isSafeInteger(value) && value > 0, `${key} must be a positive safe integer`); + } + let phase = "active"; + let stopping; + let resolveDrain; + const stats = { + acceptedBatches: 0, + rejectedBatches: 0, + failedBatches: 0, + pendingBatches: 0, + pendingBytes: 0, + peakPendingBatches: 0, + peakPendingBytes: 0, + transactions: 0, + committedTransactions: 0, + abortedTransactions: 0, + committedWrites: 0, + skippedWrites: 0, + nativePassthroughBatches: 0 + }; + function stop() { + if (phase === "active") { + phase = "draining"; + const drained = stats.pendingBatches === 0 ? Promise.resolve() : new Promise((resolve) => { + resolveDrain = resolve; + }); + stopping = drained.then(() => { + phase = "stopped"; + }); + } + return stopping; + } + function release(work) { + stats.pendingBatches -= 1; + stats.pendingBytes -= work.bytes; + if (phase === "draining" && stats.pendingBatches === 0) resolveDrain(); + } + function nativeBeforeTransaction(work, nativeThunk) { + stats.rejectedBatches += 1; + stats.nativePassthroughBatches += 1; + stats.pendingBytes -= work.bytes; + work.bytes = 0; + let results; + try { + results = nativeThunk(); + } catch (error) { + release(work); + return Promise.reject(error); + } + Promise.allSettled(results).then(() => release(work)); + return Promise.all(results); + } + function transact(transaction, entries) { + return new Promise((resolve, reject) => { + let index = 0; + let writes = 0; + let skipped = 0; + let failure; + const budget = { remaining: limits.maxReadBytes }; + transaction.oncomplete = () => { + stats.committedTransactions += 1; + stats.committedWrites += writes; + stats.skippedWrites += skipped; + resolve(); + }; + transaction.onabort = () => { + stats.abortedTransactions += 1; + reject(failure || transaction.error || new DOMException("Chart mirror batch aborted", "AbortError")); + }; + transaction.onerror = (event) => { + failure = event.target.error; + }; + function guard(action) { + try { + action(); + } catch (error) { + failure = error; + try { + transaction.abort(); + } catch (abortError) { + if (abortError instanceof DOMException && abortError.name === "InvalidStateError") return; + throw abortError; + } + } + } + let store; + function advance() { + if (index === entries.length) return; + const [key, value] = entries[index]; + function next() { + index += 1; + advance(); + } + function put() { + const request = store.put(value === null ? void 0 : value, key); + writes += 1; + request.onsuccess = () => guard(next); + } + if (budget.remaining <= 0) { + put(); + return; + } + const read = store.get(key); + read.onsuccess = () => guard(() => { + let current; + try { + current = snapshotJson(read.result === void 0 ? null : read.result, budget); + } catch { + budget.remaining = 0; + put(); + return; + } + function writeIfChanged(present) { + if (present && equalJson(current, value)) { + skipped += 1; + next(); + } else put(); + } + if (read.result === void 0 && value === null) { + const exists = store.getKey(key); + exists.onsuccess = () => guard(() => writeIfChanged(exists.result !== void 0)); + } else writeIfChanged(read.result !== void 0); + }); + } + guard(() => { + store = transaction.objectStore("keyvaluepairs"); + advance(); + }); + }); + } + async function write(target, entries, work, nativeThunk) { + let transaction; + try { + await target.ready(); + assert2(target.driver() === "asyncStorage", "Chart mirror requires the native IndexedDB driver"); + const info = target._dbInfo; + assert2( + (info.name === "chart_futures" || info.name === "chart_delivery") && info.storeName === "keyvaluepairs", + "Unexpected chart mirror database configuration" + ); + transaction = info.db.transaction("keyvaluepairs", "readwrite"); + } catch { + return nativeBeforeTransaction(work, nativeThunk); + } + stats.acceptedBatches += 1; + stats.transactions += 1; + try { + await transact(transaction, entries); + } catch (error) { + stats.failedBatches += 1; + stop(); + throw error; + } finally { + release(work); + } + } + function dispatch(target, entries, nativeThunk) { + assert2(typeof nativeThunk === "function", "Chart mirror dispatch requires its original native expression"); + if (phase === "stopped") return nativeThunk(); + if (phase === "draining") return [stopping.then(() => Promise.all(nativeThunk()))]; + const work = { bytes: 0 }; + stats.pendingBatches += 1; + let copied; + try { + assert2(Array.isArray(entries), "Chart mirror entries must be an array"); + if (entries.length === 0) { + release(work); + return []; + } + stats.peakPendingBatches = Math.max(stats.peakPendingBatches, stats.pendingBatches); + assert2(entries.length <= limits.maxEntries, "Chart mirror entry limit exceeded"); + assert2(stats.pendingBatches <= limits.maxPendingBatches, "Chart mirror pending batch limit exceeded"); + const budget = { remaining: limits.maxBatchBytes }; + const seen = /* @__PURE__ */ new Set(); + const keys = /* @__PURE__ */ new Set(); + copied = entries.map((entry) => { + assert2(Array.isArray(entry) && entry.length === 2, "Chart mirror entry must be a key/value pair"); + const [key, value] = entry; + assert2( + typeof key === "string" && (key.startsWith("#TV_SYMBOL-") || key.startsWith("myTradingView")), + "Chart mirror received a non-chart key" + ); + assert2(!keys.has(key), "Chart mirror keys must be unique"); + keys.add(key); + budget.remaining -= byteLength(key) + 3; + return [key, snapshotJson(value, budget, seen)]; + }); + const bytes = byteLength(copied); + assert2(bytes <= limits.maxBatchBytes, "Chart mirror batch byte limit exceeded"); + assert2(stats.pendingBytes + bytes <= limits.maxPendingBytes, "Chart mirror pending byte limit exceeded"); + work.bytes = bytes; + stats.pendingBytes += bytes; + stats.peakPendingBytes = Math.max(stats.peakPendingBytes, stats.pendingBytes); + } catch { + return [nativeBeforeTransaction(work, nativeThunk)]; + } + return [write(target, copied, work, nativeThunk)]; + } + return Object.freeze({ dispatch, getStats: () => ({ ...stats }), getPhase: () => phase, stop }); + } + + // src/binance-orderbook-trade/chart-storage/runtime.js + var CAPTURE_DEADLINE_MS = 3e4; + function startChartStorageOptimizer() { + if (!isChartStoragePage()) throw new Error("Chart storage requires a top-level Binance trading page"); + const writer = createChartMirrorWriter(); + const state = { status: "waiting", reason: null, attempts: 0, matches: 0, executions: 0 }; + let observer; + let timer; + let stopping; + function snapshot() { + const stats = writer.getStats(); + return { + ...state, + status: stats.failedBatches > 0 ? "native_after_failure" : state.status, + phase: writer.getPhase(), + writer: stats + }; + } + function stop(reason = "manual") { + if (stopping) return stopping; + state.status = "stopping"; + state.reason = reason; + clearTimeout(timer); + self.removeEventListener("pagehide", onPageHide); + if (observer) observer.stop(); + stopping = writer.stop().then(() => { + state.status = "native"; + return snapshot(); + }); + return stopping; + } + function onPageHide() { + void stop("pagehide"); + } + function dispatch(target, entries, nativeThunk) { + if (!isChartStoragePage()) void stop("scope_changed"); + return writer.dispatch(target, entries, nativeThunk); + } + try { + observer = observeChartStorageBootstrap({ + replaceMirrorFactory(original) { + if (!isChartStoragePage()) { + void stop("scope_changed"); + return original; + } + state.attempts += 1; + let replacement; + try { + replacement = replaceChartMirrorFactory(original, dispatch); + } catch { + void stop("source_mismatch"); + return original; + } + state.matches += 1; + return function(...args) { + state.executions += 1; + return Reflect.apply(replacement, this, args); + }; + }, + onCapture() { + clearTimeout(timer); + state.status = "active"; + } + }); + } catch { + void stop("bootstrap_unavailable"); + return Object.freeze({ snapshot, stop }); + } + observer.captured.catch(() => { + if (!stopping) void stop("capture_failed"); + }); + self.addEventListener("pagehide", onPageHide, { once: true }); + timer = setTimeout(() => { + void stop("capture_deadline"); + }, CAPTURE_DEADLINE_MS); + return Object.freeze({ snapshot, stop }); + } + + // src/binance-orderbook-trade/chart-storage/install.js + function installChartStorageOptimizer() { + if (!isChartStoragePage() || Object.hasOwn(self, "__BINANCE_CHART_STORAGE__")) return; + Object.defineProperty(self, "__BINANCE_CHART_STORAGE__", { + value: startChartStorageOptimizer(), + configurable: true + }); + } + // src/binance-orderbook-trade/index.user.js + installChartStorageOptimizer(); (function initializeOrderbookTrade() { "use strict"; + if (!document.documentElement) { + const rootObserver = new MutationObserver(() => { + if (!document.documentElement) return; + rootObserver.disconnect(); + initializeOrderbookTrade(); + }); + rootObserver.observe(document, { childList: true }); + return; + } function isFuturesTradingPage() { return isFuturesTradingPathname(location.pathname); } diff --git a/src/binance-orderbook-trade/chart-storage/bootstrap.js b/src/binance-orderbook-trade/chart-storage/bootstrap.js new file mode 100644 index 0000000..fbded35 --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/bootstrap.js @@ -0,0 +1,161 @@ +import { isChartStoragePage } from './scope.js'; + +const QUEUE_NAME = 'webpackChunkfutures_trade_ui'; + +/** + * Observe first module execution before Rspack installs its chunk dispatcher. + * By default this only captures localForage. An explicit mirror replacer selects + * module 70940 and runs before registration; it never calls a storage method. + * The caller must enter before the host runtime on a supported trading page. + */ +export function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } = {}) { + if (!isChartStoragePage()) throw new Error('Storage bootstrap requires a top-level Binance trading page'); + if (onCapture !== undefined && typeof onCapture !== 'function') throw new Error('onCapture must be a synchronous function'); + if (replaceMirrorFactory !== undefined && typeof replaceMirrorFactory !== 'function') throw new Error('replaceMirrorFactory must be a synchronous function'); + const moduleId = replaceMirrorFactory === undefined ? '43917' : '70940'; + + const globalDescriptor = Object.getOwnPropertyDescriptor(self, QUEUE_NAME); + if (globalDescriptor && !Object.hasOwn(globalDescriptor, 'value')) throw new Error('Storage bootstrap requires a native queue property'); + const queue = globalDescriptor && globalDescriptor.value !== undefined ? globalDescriptor.value : []; + const nativeAppend = Array.prototype.push; + if (!Array.isArray(queue) || queue.length !== 0 || queue.push !== nativeAppend || Object.hasOwn(queue, 'push')) { + throw new Error('Storage bootstrap must start before runtime on a native empty queue'); + } + if (globalDescriptor && globalDescriptor.value === undefined && !globalDescriptor.configurable) { + throw new Error('Storage bootstrap cannot own the existing global queue property'); + } + const createdQueue = !globalDescriptor || globalDescriptor.value === undefined; + if (createdQueue) Object.defineProperty(self, QUEUE_NAME, { value: queue, writable: true, configurable: true, enumerable: true }); + + let runtimePush; + let inFlightAppends = 0; + let active = true; + let originalFactory; + let observedFactory; + let observedChunk; + let runtimeRequire; + let resolveCapture; + let rejectCapture; + const captured = new Promise((resolve, reject) => { resolveCapture = resolve; rejectCapture = reject; }); + + function restore() { + if (runtimeRequire && runtimeRequire.m[moduleId] === observedFactory) runtimeRequire.m[moduleId] = originalFactory; + if (observedChunk && observedChunk[1][moduleId] === observedFactory) observedChunk[1][moduleId] = originalFactory; + const descriptor = Object.getOwnPropertyDescriptor(queue, 'push'); + if (!descriptor || descriptor.get !== getPush || descriptor.set !== setPush || !descriptor.configurable) { + throw new Error('Storage bootstrap lost queue accessor ownership'); + } + if (runtimePush) { + Object.defineProperty(queue, 'push', { value: runtimePush, writable: true, configurable: true, enumerable: true }); + } else { + delete queue.push; + if (createdQueue && inFlightAppends === 0 && queue.length === 0 && Object.getOwnPropertyDescriptor(self, QUEUE_NAME)?.value === queue) { + if (globalDescriptor) Object.defineProperty(self, QUEUE_NAME, globalDescriptor); + else delete self[QUEUE_NAME]; + } + } + } + + /** Observational failure is isolated from Rspack's persistent module-error cache. */ + function finish(succeeded, result) { + if (!active) return; + active = false; + try { + restore(); + } catch (cleanupError) { + if (succeeded) { + succeeded = false; + result = cleanupError; + } + } + if (succeeded) resolveCapture(result); + else rejectCapture(result); + } + + function prepareChunk(chunk) { + if (!active) return chunk; + try { + if (!Array.isArray(chunk) || !Array.isArray(chunk[0]) || !chunk[1] || typeof chunk[1] !== 'object') { + throw new Error('Storage bootstrap observed an unsupported chunk shape'); + } + if (!Object.hasOwn(chunk[1], moduleId)) return chunk; + if (originalFactory) throw new Error('Storage module registered twice before capture'); + originalFactory = chunk[1][moduleId]; + if (typeof originalFactory !== 'function') throw new Error('Storage module factory must be a function'); + const hostRuntime = chunk[2]; + if (hostRuntime !== undefined && typeof hostRuntime !== 'function') throw new Error('Storage chunk runtime callback must be a function'); + const executionFactory = replaceMirrorFactory === undefined ? originalFactory : replaceMirrorFactory(originalFactory); + if (!active) return chunk; + if (typeof executionFactory !== 'function') throw new Error('Mirror replacement must return a factory synchronously'); + observedFactory = function (module, exports, require) { + runtimeRequire = require; + let returned; + try { + returned = executionFactory.call(this, module, exports, require); + } catch (hostError) { + finish(false, hostError); + throw hostError; + } + if (active) { + try { + const result = onCapture === undefined ? undefined : onCapture(module.exports); + if (result && typeof result.then === 'function') throw new Error('Storage observation callback must complete synchronously'); + finish(true, module.exports); + } catch (observationError) { + finish(false, observationError); + } + } + return returned; + }; + observedChunk = [chunk[0], { ...chunk[1], [moduleId]: observedFactory }, require => { + runtimeRequire = require; + if (hostRuntime !== undefined) return hostRuntime(require); + }]; + return observedChunk; + } catch (observationError) { + // Invalid or duplicate registrations still reach the host's original dispatcher. + finish(false, observationError); + return chunk; + } + } + + /** Rspack retains this parent callback; after takeover it must only append. */ + function preRuntimePush(...chunks) { + if (!active || runtimePush) return nativeAppend.apply(this, chunks); + inFlightAppends += 1; + try { + return nativeAppend.apply(this, chunks.map(prepareChunk)); + } finally { + inFlightAppends -= 1; + } + } + + function runtimeObservingPush(chunk) { + return runtimePush.call(this, prepareChunk(chunk)); + } + + function getPush() { + return runtimePush ? runtimeObservingPush : preRuntimePush; + } + + function setPush(dispatcher) { + if (!active) { + Object.defineProperty(queue, 'push', { value: dispatcher, writable: true, configurable: true, enumerable: true }); + return; + } + if (runtimePush || typeof dispatcher !== 'function') { + finish(false, new Error('Storage bootstrap observed an unexpected push replacement')); + Object.defineProperty(queue, 'push', { value: dispatcher, writable: true, configurable: true, enumerable: true }); + return; + } + runtimePush = dispatcher; + } + + Object.defineProperty(queue, 'push', { get: getPush, set: setPush, enumerable: true, configurable: true }); + return Object.freeze({ + captured, + stop() { + finish(false, new Error('Storage bootstrap stopped before module execution')); + }, + }); +} diff --git a/src/binance-orderbook-trade/chart-storage/install.js b/src/binance-orderbook-trade/chart-storage/install.js new file mode 100644 index 0000000..ff71d4c --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/install.js @@ -0,0 +1,10 @@ +import { startChartStorageOptimizer } from './runtime.js'; +import { isChartStoragePage } from './scope.js'; + +/** Run before orderbook initialization without widening its frame or route policy. */ +export function installChartStorageOptimizer() { + if (!isChartStoragePage() || Object.hasOwn(self, '__BINANCE_CHART_STORAGE__')) return; + Object.defineProperty(self, '__BINANCE_CHART_STORAGE__', { + value: startChartStorageOptimizer(), configurable: true, + }); +} diff --git a/src/binance-orderbook-trade/chart-storage/json.js b/src/binance-orderbook-trade/chart-storage/json.js new file mode 100644 index 0000000..2181628 --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/json.js @@ -0,0 +1,69 @@ +const encoder = new TextEncoder(); +const nativeObjectSource = Function.prototype.toString.call(Object); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +/** TradingView snapshots may originate in an iframe with a different Object prototype. */ +function isPlainJsonObject(value) { + const prototype = Object.getPrototypeOf(value); + if (prototype === null || Object.getPrototypeOf(prototype) !== null) return false; + const constructor = Object.getOwnPropertyDescriptor(prototype, 'constructor'); + if (!constructor || !Object.hasOwn(constructor, 'value') || typeof constructor.value !== 'function') return false; + if (Function.prototype.toString.call(constructor.value) !== nativeObjectSource) return false; + const descriptor = Object.getOwnPropertyDescriptor(constructor.value, 'prototype'); + return !!descriptor && Object.hasOwn(descriptor, 'value') && descriptor.value === prototype; +} + +export function byteLength(value) { + return encoder.encode(JSON.stringify(value)).byteLength; +} + +/** + * Snapshot only JSON trees without invoking getters or changing structured-clone + * identity. A global seen set rejects aliases as well as cycles. The shared byte + * budget bounds traversal before cloning the rest of an oversized snapshot. + */ +export function snapshotJson(value, budget = { remaining: Infinity }, seen = new Set()) { + function consume(bytes) { + budget.remaining -= bytes; + assert(budget.remaining >= 0, 'Chart JSON byte limit exceeded'); + } + if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number') { + assert(typeof value !== 'number' || Number.isFinite(value), 'Chart JSON numbers must be finite'); + consume(byteLength(value)); + return value; + } + assert(typeof value === 'object', 'Chart values must be JSON'); + assert(!seen.has(value), 'Chart JSON must not contain shared references or cycles'); + const array = Array.isArray(value); + assert(array || isPlainJsonObject(value), 'Chart values must be plain JSON'); + const descriptors = Object.getOwnPropertyDescriptors(value); + assert(Object.getOwnPropertySymbols(value).length === 0, 'Chart JSON must not contain symbol properties'); + const keys = Object.keys(descriptors).filter(key => !(array && key === 'length')); + if (array) assert(keys.length === value.length && keys.every((key, index) => key === String(index)), 'Chart JSON arrays must be dense'); + seen.add(value); + consume(2 + Math.max(0, keys.length - 1)); + const result = array ? [] : {}; + for (const key of keys) { + const descriptor = descriptors[key]; + assert(descriptor.enumerable && Object.hasOwn(descriptor, 'value'), 'Chart JSON requires enumerable data properties'); + if (!array) consume(byteLength(key) + 1); + Object.defineProperty(result, key, { + value: snapshotJson(descriptor.value, budget, seen), enumerable: true, writable: true, configurable: true, + }); + } + return result; +} + +/** Signed zero and property insertion order remain observable through native reads. */ +export function equalJson(first, second) { + if (Object.is(first, second)) return true; + if (first === null || second === null || typeof first !== 'object' || typeof second !== 'object') return false; + if (Array.isArray(first) !== Array.isArray(second)) return false; + const firstKeys = Object.keys(first); + const secondKeys = Object.keys(second); + return firstKeys.length === secondKeys.length + && firstKeys.every((key, index) => key === secondKeys[index] && equalJson(first[key], second[key])); +} diff --git a/src/binance-orderbook-trade/chart-storage/mirror-module.js b/src/binance-orderbook-trade/chart-storage/mirror-module.js new file mode 100644 index 0000000..48b4c61 --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/mirror-module.js @@ -0,0 +1,16 @@ +/* Generated from the pinned public 70940 factory; see mirror-scoped-manifest.json. */ +const originalFactorySource = "70940(Z,x,e){\"use strict\";e.r(x),e.d(x,{TradingView:()=>Dt,default:()=>Cn});var o=e(49253),v=e(77135),d=e(75510),i=e(42288),p=e(31085),r=e(41594),M=e(43917),h=e.n(M),P=e(81936),b=e.n(P),V=e(7980),U=e.n(V),D=e(76469),c=e(4260),k=e(53837),B=e(27571),C=e(45250),y=e(87646),A=e.n(y),q=e(80817);const E=(0,C.range)(1,1380),m=(0,C.range)(1,365).map(t=>`${t}D`),R=(0,C.range)(1,52).map(t=>`${t}W`),I=(0,C.range)(1,12).map(t=>`${t}M`),F=[\"1S\",...E,...m,...R,...I],L=({symbol:t,params:n})=>{const a=new URLSearchParams(n);return`${t}@${a.toString()}`},X=t=>{const[n,a]=t.split(\"@\"),s=new URLSearchParams(a).entries(),g=A()(Array.from(s));return{symbol:n,params:g}},ue=()=>{const t=new Map;let n={},a=[],u={onReady:()=>null,resolveSymbol:()=>null,searchSymbols:()=>null,getBars:()=>null,subscribeBars:()=>null,unsubscribeBars:()=>null};return({config:s,onSymbolResolving:g,onSymbolResolved:S,onFetchSymbolsInfo:K,onFetchBars:Y,onSubscribeBars:H,onUnsubscribeBars:j,onFetchServerTimeApi:$,onTransformSymbolInfo:f=({template:T})=>T,onSearchSymbols:l=()=>!0})=>(u={onReady:T=>{setTimeout(async()=>{const _=(await K()).map(O=>({...O,ticker:L({symbol:O.symbol,params:O.params||{}})}));a=_.map(({ticker:O,symbol:G,description:te=\"\",exchange:J=\"\",type:oe=\"\"})=>({symbol:G,ticker:O,description:te,exchange:J,type:oe})),n=(0,C.keyBy)(_,\"ticker\"),T({exchanges:[],symbols_types:[],supported_resolutions:F,supports_marks:!1,supports_timescale_marks:!1,supports_time:!0,...s})})},resolveSymbol:async(T,W,_)=>{const O=n[T];if(!O){_(`cannot resolve symbol - ${T}`);return}const G=f({info:O,template:{description:O.description||\"\",fractional:!1,has_seconds:!0,seconds_multipliers:[\"1\"],has_intraday:!0,intraday_multipliers:[\"1\",\"3\",\"5\",\"15\",\"30\",\"60\",\"120\",\"240\",\"360\",\"480\",\"720\"],has_daily:!0,daily_multipliers:[\"1\"],has_weekly_and_monthly:!0,weekly_multipliers:[\"1\"],monthly_multipliers:[\"1\"],minmov:1,minmove2:0,name:O.symbol,ticker:O.ticker,timezone:\"Etc/UTC\",pricescale:O.tickSize,session:\"24x7\",type:O.type||\"\",exchange:O.exchange||\"\",listed_exchange:O.exchange||\"\",format:\"price\",supported_resolutions:F,visible_plots_set:\"ohlcv\",volume_precision:3}});if(g){const te=X(G.ticker||\"\");if(!await g({symbol:T,info:G,tickerInfo:te})){_(`symbol resolving error - ${T}`);return}}setTimeout(()=>{W(G),S?.(G)})},searchSymbols:(T,W,_,O)=>{const G=new RegExp(T,\"i\"),te=new RegExp(W,\"i\"),J=new RegExp(_,\"i\"),oe=a.filter(Te=>{const{symbol:N,ticker:w,description:re=\"\",exchange:ie=\"\",type:le=\"\"}=Te;return[N,w,re].some(de=>G.test(de||\"\"))&&te.test(ie)&&J.test(le)&&l?.({item:Te})});O(oe)},getBars:async(T,W,_,O,G)=>{const{from:te,to:J,firstDataRequest:oe}=_,Te=(0,q.Cz)(W);try{if(J>=0){const N=X(T.ticker||\"\"),w=await Y({symbolInfo:T,resolution:W,rangeStartDate:te,rangeEndDate:J,tickerInfo:N,binanceInterval:Te,firstDataRequest:oe});O(w,{noData:w.length===0})}else O([],{noData:!0})}catch(N){G(N.toString())}},subscribeBars:(T,W,_,O,G)=>{const te=X(T.ticker||\"\"),J=(0,q.Cz)(W),oe=H({symbolInfo:T,resolution:W,callback:_,onResetCacheNeededCallback:G,tickerInfo:te,binanceInterval:J});t.set(O,oe)},unsubscribeBars:T=>{t?.get(T)?.(),t?.delete(T),j?.(T)},getServerTime:async T=>{try{const W=await $();T(W)}catch(W){console.warn(W),T(Date.now()/1e3)}}},{datafeed:u,observers:t,symbolsInfo:n})},Le=({namespace:t,type:n})=>{const a=(0,r.useMemo)(()=>h().createInstance({name:t}),[t]);return(0,r.useCallback)(()=>{n===\"tv\"&&a.removeItem(\"myTradingView\")},[a,n])};var Ue=e(76535),pr=e(26531),Ye=e(17409),Qe=e(94652),Ae=e(76830),he=e(48651),Fe=e(79344),se=e(51029),Je=e(36307),or=e(25379),yr=e(44745),Ne=e(15727),rr=e(45358),sr=e(82071),Ve=e(85411),ge=e(94889),hr=e(94547),tr=e(80686),Ar=e(61875);function Fr(t){return(0,ge._)(t)||(0,hr._)(t)||(0,Ar._)(t)||(0,tr._)()}var Nr=e(92873),zr=e(35630),Vr=e(12055),Qr=e(79515),Br=e(80737),Jr=e(40545),Zr=e(98107),Kr=e(51846),Xr=\"Chart.Crosshair.PlusButton.DrawHorizontalLine\",wr=function(t){var n=t.namespace,a=t.symbol,u=t.priceType,s=t.crosshairPrice,g=(0,Nr.o)(\"\",\"kline-ui\").getI18n,S=(0,Vr.r)().format,K=k.y$(n),Y=(0,Br.S)({namespace:n}),H=(0,Jr.hw)(),j=(0,Zr.c)(),$=(0,Qr.nH)(),f=(0,se.Ye)(),l=(0,Kr.E)({symbol:a}),T=(0,Fe.f0)(a),W=T.isCM,_=(0,r.useRef)(s);(0,r.useEffect)(function(){_.current=s},[s]);var O=(0,r.useRef)(H);(0,r.useEffect)(function(){O.current=H},[H]);var G=(0,r.useRef)(j);(0,r.useEffect)(function(){G.current=j},[j]);var te=(0,r.useRef)(u);(0,r.useEffect)(function(){te.current=u},[u]);var J=(0,r.useRef)($);(0,r.useEffect)(function(){J.current=$},[$]);var oe=(0,r.useRef)(f);(0,r.useEffect)(function(){oe.current=f},[f]);var Te=(0,r.useRef)(l);(0,r.useEffect)(function(){Te.current=l},[l]);var N=(0,r.useRef)(W);return(0,r.useEffect)(function(){N.current=W},[W]),{items_processor:(function(){var w=(0,o._)(function(re,ie,le){var de,Be,ve,Ce,ce,Ke,Ze,Ge,Xe,Ie,He,ur,Tr,_r,Cr;return(0,i._)(this,function(Er){if(Be=K.getState().tradingViewReference,!Be)return console.warn(\"[context-menu] trading view reference is not found\"),[2,re];if(Be.chartsCount()>1)return[2,re];if(ve=Be.activeChart().symbolExt(),!ve)return[2,re];switch(Ce=ve.name,ce=_.current,Ke=le.menuName===\"CrosshairMenuView\"?\"chart-trading\":\"context-menu\",Ze={price:\"\".concat(ce.value),formattedPrice:ce.formatted,source:Ke,chartType:\"tradingview\"},Ge=(de=oe.current[Ce])===null||de===void 0?void 0:de.baseAsset,Xe=g(\"trade\",{defaultValue:\"Trade\"}),Ie=S(ce.formatted),He=G.current&&te.current===c.SJ.Last&&J.current&&Ge?[ie.createAction({actionId:\"Chart.PlaceOrder.BuyLimit\",label:\"\".concat(Xe,\" \").concat(Ge,\" @ \").concat(Ie,\" \").concat(g(\"limit\",{defaultValue:\"Limit\"})),onExecute:function(){return Te.current.onClickBuyLimit(Ze)}}),ie.createAction({actionId:\"Chart.PlaceOrder.BuyStop\",label:\"\".concat(Xe,\" \").concat(Ge,\" @ \").concat(Ie,\" \").concat(g(\"stop\",{defaultValue:\"Stop\"})),onExecute:function(){return Te.current.onClickBuyStop(Ze)}})]:[],ur=O.current?ie.createAction({actionId:\"Chart.PriceAlert.CreateAtCrosshair\",label:g(\"create-alert-at\",{defaultValue:\"Create Alert at {{price}}\",price:ce.formatted}),onExecute:function(){Y({symbol:Ce,price:ce.formatted,mode:zr.n.CREATE})}}):null,re.forEach(function(we){if(!(!(\"execute\"in we)||!(\"getState\"in we))){var Dr=we.getState();if(Dr.actionId===Xr){var z=we.execute.bind(we);we.execute=function(){(0,Kr.A)({elementId:\"draw_horizontal_line\",isDelivery:N.current,chartType:\"tradingview\"}),z()}}}}),le.menuName){case\"CrosshairMenuView\":return ur?[2,[ur].concat((0,d._)(He),(0,d._)(re.slice()))]:[2,(0,d._)(He).concat((0,d._)(re.slice()))];case\"ChartContextMenu\":return ur?(Tr=Fr(re),_r=Tr[0],Cr=Tr.slice(1),[2,[_r,ur].concat((0,d._)(He),(0,d._)(Cr))]):[2,(0,d._)(He).concat((0,d._)(re.slice()))];default:return[2,re]}return[2]})});return function(re,ie,le){return w.apply(this,arguments)}})()}},qr=function(){var t=(0,r.useState)({value:NaN,formatted:\"\"}),n=t[0],a=t[1],u=(0,r.useCallback)(function(s){var g=s.crosshairData,S=s.activeChart;S&&a({value:g.price,formatted:S.priceFormatter().format(g.price)})},[]);return{onCrosshairMoved:u,crosshairPrice:n}};const Hr=t=>Math.floor(Math.log10(t)),et=t=>{const[n=\"1\"]=t.split(\",\");return Hr(+n)},pe=({symbolInfo:t,minTick:n})=>{const{pricescale:a=2}=t||{};return n===\"default\"||n===void 0?Hr(a):et(n)},br=t=>(n,a)=>{try{return n?{format:u=>{const s=pe({symbolInfo:n,minTick:a});return t.format({symbolInfo:n,minTick:a,value:u,precision:s})}}:null}catch{return null}},Mr=10**12,rt=10**9,tt=10**6,jr=10**3,gr=t=>{const n=t.toString(),[,a=\"\"]=n.split(\".\");return Math.min(a.length,3)},nt=({value:t})=>{const n=Math.abs(t);if(n>=Mr){const a=t/Mr;return{value:a,unit:\"T\",precision:gr(a)}}if(n>=rt){const a=t/rt;return{value:a,unit:\"B\",precision:gr(a)}}if(n>=tt){const a=t/tt;return{value:a,unit:\"M\",precision:gr(a)}}if(n>=jr){const a=t/jr;return{value:a,unit:\"K\",precision:gr(a)}}return{value:t,unit:\"\",precision:gr(t)}},ut=t=>(n,a,u)=>{try{return a&&n.type===\"volume\"?{format:s=>s===void 0?\"\":t.formatVolume({format:n,symbolInfo:a,precision:u,value:s,volumeDetail:nt({value:s})})}:null}catch{return null}};var lt=function(){var t=(0,Vr.r)(),n=t.format,a=(0,r.useMemo)(function(){return br({format:function(s){var g=s.value,S=s.precision;return n(g,{precision:S})}})},[n]),u=(0,r.useMemo)(function(){return ut({formatVolume:function(s){var g=s.value,S=s.volumeDetail;if(g===void 0||!S)return\"\";var K=n(S.value,{precision:S.precision});return\"\".concat(K).concat(S.unit)}})},[n]);return{priceFormatterFactory:a,studyFormatterFactory:u}},dt=e(74069),ir=function(t){var n=(0,dt.K)(t),a=(0,r.useMemo)(function(){return n.map(function(u){var s=u.id,g=u.time,S=u.isBuy,K=u.price,Y=u.tooltip;return{id:s,text:\"\",price:K,quantity:\"\",time:Math.round(g/1e3),isBuy:S,tooltip:Y}})},[n]);return{executionOrders:a}},at=function(t){var n=t.namespace,a=t.symbol,u=(0,Br.j)({namespace:n,symbol:a});return(0,r.useMemo)(function(){return u.map(function(s){var g=s.id,S=s.price,K=s.formattedPrice,Y=s.sideText,H=s.showCloseButton,j=s.isPriceChangeable,$=s.cancelTooltipText,f=s.onClick,l=s.onCancel,T=s.onPriceChanged;return{id:g,price:+S,text:\"\".concat(Y,\" \").concat(K),editable:j,cancellable:H,cancelTooltip:$,type:c.ND.PriceAlert,onCancel:H?l:void 0,onMove:j?function(W){var _=W.price;T({currentPrice:_})}:void 0,onClick:f}})},[u])},vt=e(90664),ot=function(t){var n=t.symbol,a=(0,vt.w)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){var s=u.id,g=u.text,S=u.price;return{id:s,text:g,price:+S,type:c.ND.BreakEvenPrice}})},[a])},cr=e(82508),ft=e(91025),mt=e(36077),Rr=function(t){var n=t.symbol,a=(0,mt.c)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){return(0,ft._)((0,cr._)({},u),{type:c.ND.LiquidationPrice})})},[a])},pt=function(t){var n=t.namespace,a=t.symbol,u=at({namespace:n,symbol:a}),s=ot({symbol:a}),g=Rr({symbol:a});return(0,r.useMemo)(function(){return(0,d._)(u).concat((0,d._)(s),(0,d._)(g))},[u,s,g])},st=e(42902),Ir=e(90170),yt=e(63345),ht=e(48210),Pr=e(51289),xr=e(24326),$r=e(90922),kr=e(34235),it=e(63762),Ft=function(t){var n=(0,Fe.f0)(t),a=n.pricePrecision,u=n.isCM,s=(0,st.yD)(t)||{},g=s.lastPrice,S=g===void 0?\"0\":g,K=(0,kr.Bp)(u),Y=(0,it.HY)(t),H=(0,kr.gv)(u),j=(0,kr.WF)(u);return(0,r.useMemo)(function(){return(0,$r.qF)({orders:(0,d._)(K).concat((0,d._)(H),(0,d._)(j)),symbol:t}).map(function($){return Y({order:$,lastPrice:S,pricePrecision:a})})},[S,K,H,a,t,Y])},gt=e(31542),Tt=function(t){var n=t.type;(0,gt.u4)(\"webClick\",{module:\"chart\",elementId:n})},Nt=function(t){var n=t.symbol,a=t.namespace,u=(0,r.useState)(0),s=u[0],g=u[1],S=(0,r.useCallback)(function(){return g(function(N){return N+1})},[]),K=Ft(n),Y=(0,xr.IT)({namespace:a}),H=(0,Fe.f0)(n),j=H.isDelivery,$=H.contractType,f=(0,st.yD)(n)||{},l=f.lastPrice,T=l===void 0?\"0\":l,W=(0,yt.s)(n),_=(0,it.YZ)({symbol:n,afterConfirmationModalIsShown:S}),O=(0,it.u8)(n),G=(0,it.jn)(n),te=(0,r.useCallback)(function(N){return(0,ht.D)({order:N,isDelivery:j,symbol:j?N.symbol:\"\".concat(N.symbol,\"_\").concat($),module:\"tradingview_kline\"})},[$,j]),J=(0,r.useMemo)(function(){return K.map(function(N){var w=N.order,re=N.price,ie=N.quantity,le=N.quantityText,de=N.isBuy,Be=N.text,ve=N.priceTooltipText,Ce=N.pnlText,ce=(0,Pr.$L)(w),Ke=(0,Pr.OX)(w),Ze=(0,Pr.xL)(w),Ge=ce===\"LIMIT\",Xe=(0,Ir.Y)(w);return{id:String(Ke),text:Be,price:+re,quantity:le,isBuy:de,editable:Xe&&G,tooltip:ve,modifyTooltip:w.closePosition||!Ge?\"\":void 0,onMove:G?function(Ie){var He=Ie.price;te(w),Tt({type:\"TV_vertical\"}),_({order:w,price:+He,quantity:+Ze,lastPrice:T})}:void 0,onModify:w.closePosition||!Ge||!Xe?void 0:function(){Tt({type:\"TV_amountclick\"}),O({order:w,price:re.toString(),quantity:ie,lastPrice:T})},onCancel:function(){Tt({type:\"TV_cancel\"}),W(w)}}})},[G,K,T,te,_,O,W,s]),oe=(0,r.useMemo)(function(){return Y.map(function(N){var w=N.id,re=N.value,ie=N.text,le=N.onChange,de=N.isBuy,Be=N.type;return{id:w,price:+re,quantity:\"\",text:ie,isBuy:de,editable:!0,limitOrderType:c.aA.OrderPreview,onMove:function(ve){var Ce=ve.price;(0,xr.eP)({namespace:a,orderType:Be}),le({id:w,newPrice:Ce})},onMoving:function(ve){var Ce=ve.price;return le({id:w,newPrice:Ce})}}})},[Y,a]),Te=(0,r.useMemo)(function(){return(0,d._)(J).concat((0,d._)(oe))},[J,oe]);return{orderLines:Te}},Vt=e(17855),Bt=function(t,n){var a=(0,r.useState)(t()),u=a[0],s=a[1];return(0,Vt.A)(function(){return s(t())},n),u},Kt=e(58656),Ht=function(t){var n=t.symbol,a=(0,Kt.Q)({symbol:n})||[],u=Bt(function(){return a.map(function(s){var g=s.position,S=s.pnl,K=s.pnlInNumber,Y=s.pnlText,H=s.price,j=s.quantity,$=s.reverseText,f=s.isBuy,l=s.displayReverseOrderModal,T=s.displayClosePositionModal;return{id:g.id||\"\".concat(g.symbol,\"-\").concat(g.positionSide),PNL:K,text:\"\".concat(Y,\" \").concat(S),price:H,quantity:j,tooltip:\"\",reverseTooltip:$,isBuy:f,onReverse:l,onClose:function(){T(),(0,gt.u4)(\"$WebClick\",{item:1,module:\"xxx\",$element_id:\"kline_close_position\"})}}})},[a]);return{positionLines:u}},jt=e(47354),bt=e.n(jt),$t=e(87017),Ut=e(99168),Yt=e(3742),Wt=e(43335),Gt=e(15540),zt=e(20163),Qt=e(47255);const Jt=t=>({endTime:n})=>(n??0)===t;var Zt=e(36488),Ct=e(48018),Xt=e(82911),We=e(69578),wt=e(53208),Rt=e(19862),qt=e(8874),en=e(26679),rn=e(66547),It=(function(){var t=(0,o._)(function(n){var a,u,s,g,S,K,Y,H,j;return(0,i._)(this,function($){switch($.label){case 0:a=n.onFetchKlineData,u=n.startTime,s=n.endTime,g=n.interval,S=n.limit,K=S===void 0?1e3:S,Y=[],H=[],j=s,$.label=1;case 1:return[4,a((0,cr._)({interval:g,limit:K},j===0?{}:{endTime:j}))];case 2:if(H=$.sent(),H.length===0)return[3,4];H.forEach(function(f){var l=(0,v._)(f,8),T=l[0],W=l[1],_=l[2],O=l[3],G=l[4],te=l[5],J=l[7];Y.push({time:+T,open:+W,high:+_,low:+O,close:+G,volume:+te,quantity:+J})}),j=H[0][0]-1,$.label=3;case 3:if(j>u)return[3,1];$.label=4;case 4:return Y.sort(function(f,l){return f.time-l.time}),[2,Y]}})});return function(a){return t.apply(this,arguments)}})(),tn=function(t){var n=t.firstDataRequest,a=t.hasRequested,u=t.rangeEndDate;return n&&!a?0:u*1e3},nn=e(78142),an=e(16238),St=e.n(an),on=e(98087),xt=e.n(on),sn=e(55009),cn=e.n(sn),kt=e(56477);const Et=t=>cn()(t.chartsCount(),n=>t.chart(n)),un=async t=>{try{const{schema:n,data:a}=await t.exportData({includedStudies:[]}),u=xt()(a)?.[n.findIndex(s=>s.type===\"time\")];return u!==void 0&&Number.isFinite(u)?u*1e3:void 0}catch{return}},ln=({getWidget:t,...n})=>{const a=new Map,u=new Map,s=()=>{try{const f=t();return f?St()(Et(f).map(l=>l.symbolExt()?.ticker)):[]}catch{return[]}},g=f=>{const l=s();return!l.length||l.includes(f)},S=f=>{const l=u.get(f);if(l)return l;const T={controller:new AbortController,claimedAsMain:!1};return u.set(f,T),T},K=f=>{const l=u.get(f);l&&(l.controller.abort(),u.delete(f))},Y=()=>{const f=s();f.length&&Array.from(u.keys()).forEach(l=>{const T=u.get(l);if(T){if(f.includes(l)){T.claimedAsMain=!0;return}a.has(l)||T.claimedAsMain&&K(l)}})},H=(f,l)=>{f.closed||l.time{f.held=[];let T=l;try{const _=(await f.options.fetchSince(l)).filter(O=>O.time>=l&&!f.closed);_.forEach(O=>{f.highWater=Math.max(f.highWater,O.time),f.options.onTick(O)}),T=xt()(_)?.time??l}finally{const W=f.held??[];f.held=void 0,W.filter(_=>_.time>=T).forEach(_=>{H(f,_)})}},$=(0,kt.k)({...n,onCheck:Y,repair:async()=>{const f=t();if(f)try{const l=Et(f),T=St()(l.map(_=>{const O=a.get(_.symbolExt()?.ticker??\"\");return O&&{chart:_,feed:O}}));if(!T.length){f.resetCache(),l.forEach(_=>{_.resetData()});return}const W=St()(await Promise.all(T.map(async({chart:_,feed:O})=>{const G=await un(_);return G===void 0?void 0:{feed:O,fromMs:G}})));await Promise.allSettled(W.map(({feed:_,fromMs:O})=>j(_,O)))}catch{}}});return{openFeed:f=>{const l={options:f,held:void 0,highWater:0,closed:!1,detector:void 0};return l.detector=(0,kt.k)({...n,subscribeSignals:f.subscribeSignals,repair:async()=>{l.closed||!l.highWater||g(f.ticker)||await j(l,l.highWater)}}),a.set(f.ticker,l),g(f.ticker)&&$.resubscribeSignals(f.subscribeSignals),{push:T=>{l.held?l.held.push(T):H(l,T)},close:()=>{l.closed=!0,l.detector?.dispose(),a.get(f.ticker)===l&&a.delete(f.ticker)}}},subscribeWake:$.subscribeWake,getSignal:f=>{const l=S(f);return s().includes(f)&&(l.claimedAsMain=!0),l.controller.signal},dispose:()=>{u.forEach(({controller:f})=>{f.abort()}),u.clear(),a.forEach(f=>{f.detector?.dispose()}),a.clear(),$.dispose()}}};var dn=e(71730),vn=e(36620),fn=function(t){var n=t.namespace,a=k.y$(n),u=(0,vn.l)().getSocketSignals,s=(0,r.useRef)();(0,r.useEffect)(function(){return function(){var S;(S=s.current)===null||S===void 0||S.instance.dispose(),s.current=void 0}},[]);var g=(0,r.useCallback)(function(S){var K,Y,H=S;if(((K=s.current)===null||K===void 0?void 0:K.key)===H)return s.current.instance;(Y=s.current)===null||Y===void 0||Y.instance.dispose();var j=nn.wr(S),$=ln({getWidget:function(){return a.getState().tradingViewReference},checkPeriodMs:dn.D(j)});return s.current={key:H,instance:$},$},[a]);return(0,r.useMemo)(function(){return{ensureRecovery:g,getSocketSignals:u}},[g,u])},mn=\"tv_web_error\",pn=[Ye.bt.TRADING,Ye.bt.DELIVERING],Re;(function(t){t.Futures=\"futures\",t.FuturesMark=\"futures-mark\",t.FuturesIndex=\"futures-index\",t.Delivery=\"delivery\",t.DeliveryMark=\"delivery-mark\",t.DeliveryIndex=\"delivery-index\"})(Re||(Re={}));var Mt=function(t){var n=t.symbol,a=t.description,u=t.productType,s=t.pricePrecision,g=t.params,S=t.supportedResolutions,K=S===void 0?F:S;return{symbol:n,description:a,tickSize:Math.pow(10,s),exchange:\"Binance\",supportedResolutions:K,type:u,params:g}},yn=function(t){var n=t.namespace,a=t.useMiniTicker,u=a===void 0?!1:a,s=t.isCM,g=c.y$(n),S=k.y$(n),K=(0,$t.useQueryClient)(),Y=fn({namespace:n}),H=Y.ensureRecovery,j=Y.getSocketSignals,$=(0,r.useRef)(0),f=(0,r.useRef)(!1),l=(0,r.useMemo)(function(){return bt()(function(){return(0,We.HP)({eventId:\"trd_chart_tv_load_timeout\",stepId:\"getBarsStart\",catch:function(Q){var ee=Q.error,ae=Q.track;ae({GET_BAR_START_TYPE:ee})},log:function(Q){var ee=Q.track;ee({GET_BAR_START_TYPE:\"SUCCESS\"})}})})},[]),T=(0,r.useMemo)(function(){return bt()(function(z){return(0,We.HP)({eventId:\"trd_chart_tv_load_timeout\",stepId:\"getBarsEnd\",catch:function(ee){var ae=ee.error,Se=ee.track;Se({GET_BAR_END_TYPE:ae,duration_:(0,We.Pu)(z-$.current)})},log:function(ee){var ae=ee.track;ae({GET_BAR_END_TYPE:\"SUCCESS\",duration_:(0,We.Pu)(z-$.current)})}})})},[]),W=(0,Nr.o)(\"trd-chart\",\"trade-ui\"),_=W.getI18n,O=(0,he.ud)(),G=O.getI18n,te=(0,v._)(g(c.Ir),1),J=te[0],oe=(0,v._)(S(k.A7),2),Te=oe[1],N=(0,se.Ye)(),w=(0,Zt.JD)(s).isContUnit,re=(0,se.y_)(),ie=(0,r.useRef)(N),le=(0,r.useRef)(J),de=(0,r.useRef)(re);(0,r.useEffect)(function(){ie.current=N},[N]),(0,r.useEffect)(function(){le.current=J},[J]),(0,r.useEffect)(function(){de.current=re},[re]);var Be=(0,r.useMemo)(function(){return w?Rt.b:Rt.S},[w]),ve=(0,r.useMemo)(function(){return G(\"usdtFutures-13\",{defaultValue:\"Futures\"})||\"\"},[G]),Ce=(0,r.useMemo)(function(){return G(\"inverseFutures-13\",{defaultValue:\"Delivery\"})||\"\"},[G]),ce=(0,r.useMemo)(function(){return _(\"indexPrice\",{defaultValue:\"Index Price\"})||\"\"},[_]),Ke=(0,r.useMemo)(function(){return _(\"markPrice\",{defaultValue:\"Mark Price\"})||\"\"},[_]),Ze=(0,r.useMemo)(function(){return _(\"lastPrice\",{defaultValue:\"Last Price\"})||\"\"},[_]),Ge=(0,r.useMemo)(function(){return{exchanges:[{name:\"Binance\",value:\"Binance\",desc:\"\"}],symbols_types:[{name:ve,value:Re.Futures},{name:\"\".concat(ve,\" \").concat(Ke),value:Re.FuturesMark},{name:\"\".concat(ve,\" \").concat(ce),value:Re.FuturesIndex},{name:Ce,value:Re.Delivery},{name:\"\".concat(Ce,\" \").concat(Ke),value:Re.DeliveryMark},{name:\"\".concat(Ce,\" \").concat(ce),value:Re.DeliveryIndex}]}},[Ce,ve,ce,Ke]),Xe=(0,r.useCallback)((0,o._)(function(){var z,Q,ee;return(0,i._)(this,function(ae){switch(ae.label){case 0:return ae.trys.push([0,2,,3]),[4,(0,en.c)(K)];case 1:return ae.sent(),[3,3];case 2:return z=ae.sent(),(0,gt.u4)(mn,{type:\"onFetchSymbolsInfo\",message:z.message,business:\"futures\"}),[3,3];case 3:return Q=ie.current,ee=Object.keys(Q).reduce(function(Se,xe){var ne=Q[xe]||Ct.o,ke=ne.symbol,Me=ne.pricePrecision,fe=ne.isDelivery,Pe=ne.contractStatus;if(!pn.includes(Pe))return Se;var _e=(0,Yt.S)(ne,G),je=_e.symbol,Ee=_e.contractTypeI18n,me=_e.deliveryDateFormat,ye=\"\".concat(je,\" \").concat(Ee).concat(me?\" \".concat(me):\"\"),$e=\"\".concat(ye,\" \").concat(Ze),Oe=\"\".concat(ye,\" \").concat(ce),lr=\"\".concat(ye,\" \").concat(Ke);return Se.concat([Mt({symbol:ke,description:$e,productType:fe?Re.Delivery:Re.Futures,pricePrecision:Me,params:{priceType:c.SJ.Last}}),Mt({symbol:ke,description:Oe,productType:fe?Re.DeliveryIndex:Re.FuturesIndex,pricePrecision:Me,params:{priceType:c.SJ.Index}}),Mt({symbol:ke,description:lr,productType:fe?Re.DeliveryMark:Re.FuturesMark,pricePrecision:(0,Xt.X)(Me),params:{priceType:c.SJ.Mark}})])},[]),[2,ee]}})}),[G,ce,Ke,Ze,K]),Ie=(0,r.useCallback)(function(z){var Q=z.ticker,ee=z.symbol,ae=z.interval,Se=z.priceType,xe=z.retryUntil,ne=ie.current[ee]||Ct.o,ke=ne.pair,Me=ne.contractType,fe=ne.contractVal,Pe=ne.isDelivery,_e=Pe?Be(fe):function(Oe){return Oe},je=(0,wt.h)({symbol:ee,pair:ke,contractType:Me,isDelivery:Pe,priceType:Se}),Ee,me,ye=function(Oe){return zt.y(\"tv\",\"\".concat(Pe?\"cm\":\"um\",\"|\").concat(ee,\"|\").concat(Se,\"|\").concat(Oe.interval,\"|\").concat((Ee=Oe.limit)!==null&&Ee!==void 0?Ee:\"\",\"|\").concat((me=Oe.endTime)!==null&&me!==void 0?me:\"\"))};if(xe===void 0)return{transform:_e,onFetchKlineData:je};var $e=H(ae);return{transform:_e,onFetchKlineData:Qt.b(je,{enabled:Jt(xe),key:ye,signal:$e.getSignal(Q),subscribeWake:$e.subscribeWake})}},[H,Be]),He=(0,r.useCallback)((function(){var z=(0,o._)(function(Q){var ee,ae,Se,xe,ne,ke,Me,fe,Pe,_e,je,Ee,me,ye,$e,Oe,lr,Or;return(0,i._)(this,function(dr){switch(dr.label){case 0:return ee=Q.binanceInterval,ae=Q.rangeStartDate,Se=Q.rangeEndDate,xe=Q.tickerInfo,ne=Q.symbolInfo,ke=Q.firstDataRequest,$.current=Date.now(),l(),Me=xe.symbol,fe=Me===void 0?\"\":Me,Pe=xe.params,_e=Pe.priceType,je=_e===void 0?le.current:_e,de.current({symbol:fe,priceType:je})?(T(Date.now()),[2,[]]):(Ee=Math.max(0,ae*1e3),me=tn({firstDataRequest:ke,hasRequested:f.current,rangeEndDate:Se}),f.current=!0,$e=Ie({ticker:(ye=ne.ticker)!==null&&ye!==void 0?ye:\"\",symbol:fe,interval:ee,priceType:je,retryUntil:ke?me:void 0}),Oe=$e.transform,lr=$e.onFetchKlineData,[4,It({onFetchKlineData:lr,startTime:Ee,endTime:me,interval:ee})]);case 1:return Or=dr.sent(),T(Date.now()),[2,Or.map(Oe)]}})});return function(Q){return z.apply(this,arguments)}})(),[Ie,T,l]),ur=(0,r.useCallback)(function(z){var Q=z.binanceInterval,ee=z.callback,ae=z.tickerInfo,Se=z.symbolInfo,xe=ae.symbol,ne=xe===void 0?\"\":xe,ke=ae.params,Me=ke.priceType,fe=Me===void 0?le.current:Me;if(de.current({symbol:ne,priceType:fe}))return function(){return null};var Pe=ie.current[ne]||Ct.o,_e=Pe.pair,je=Pe.contractType,Ee=Pe.isDelivery,me,ye=(me=Se.ticker)!==null&&me!==void 0?me:\"\",$e=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe}).transform,Oe=H(Q),lr=Oe.openFeed({ticker:ye,onTick:ee,subscribeSignals:j(Ee),fetchSince:(function(){var dr=(0,o._)(function(Lr){var Ur,Yr,ct,De;return(0,i._)(this,function(ze){switch(ze.label){case 0:return Ur=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe,retryUntil:0}),Yr=Ur.transform,ct=Ur.onFetchKlineData,[4,It({onFetchKlineData:ct,startTime:Lr,endTime:0,interval:Q})];case 1:return De=ze.sent(),[2,De.map(Yr)]}})});return function(Lr){return dr.apply(this,arguments)}})()}),Or=(0,rn.B)({symbol:ne,pair:_e,contractType:je,interval:Q,priceType:fe,isDelivery:Ee,useMiniTicker:u,callback:function(dr){lr.push($e(dr))}});return function(){lr.close(),Or()}},[Ie,H,j,u]),Tr=(0,r.useCallback)((0,o._)(function(){var z;return(0,i._)(this,function(Q){switch(Q.label){case 0:return[4,K.fetchQuery(Wt.Bz.SERVER_TIME(s),function(){return(0,Gt.l)(s)},{})];case 1:return z=Q.sent(),[2,Math.floor(z/1e3)]}})}),[K,s]),_r=(0,r.useCallback)(function(z){return(0,cr._)({},z.template,(0,qt.sp)(s))},[s]),Cr=(0,r.useMemo)(function(){return ue()},[]),Er=Cr({config:Ge,onFetchSymbolsInfo:Xe,onFetchBars:He,onSubscribeBars:ur,onFetchServerTimeApi:Tr,onTransformSymbolInfo:_r}),we=Er.datafeed,Dr=Er.symbolsInfo;return(0,Ut.A)(function(){return Te()},[w]),{datafeed:we,symbolsInfo:Dr}},Pt=\"trd_chart_tv_load_timeout\",hn=function(){var t=(0,We.PM)({id:\"chartReady\"}),n=(0,We.PM)({id:\"getBarsStart\",timeout:3}),a=(0,We.PM)({id:\"getBarsEnd\",timeout:6}),u=(0,We.PM)({id:\"chartResponsive\",timeout:3});We.HP.init({step:t._(n)._(a)._(u),eventId:Pt}),(0,We.HP)({eventId:Pt,stepId:\"chartReady\",catch:function(g){var S=g.error,K=g.track;K({CHART_READY_TYPE:S,connection:JSON.stringify(navigator.connection)})},log:function(g){var S=g.track;S({CHART_READY_TYPE:\"SUCCESS\",connection:JSON.stringify(navigator.connection)})}})},gn=function(t){(0,We.HP)({eventId:Pt,stepId:\"chartResponsive\",catch:function(a){var u=a.error,s=a.track;s({CHART_RESPONSIVE_TYPE:u,connection:JSON.stringify(navigator.connection)})},log:function(a){var u=a.track;u({CHART_RESPONSIVE_TYPE:\"SUCCESS\",duration_:(0,We.Pu)(Date.now()-t)})}})},Tn=[Qe.ZE,Qe.F3],Dt=function(t){var n=t.symbol,a=t.namespace,u=t.useMiniTicker,s=u===void 0?!1:u,g=c.y$(a),S=k.y$(a),K=(0,v._)(g(c.A8),1),Y=K[0],H=(0,v._)(g(c.Ir),2),j=H[0],$=H[1],f=(0,v._)(S(k.yj),1),l=f[0],T=(0,v._)(g(c.e6),1),W=T[0],_=(0,v._)(g(c._b),1),O=_[0],G=(0,he.ot)(),te=(0,pr.K7)(),J=te.pathname.includes(Ae.Q2),oe=te.pathname.includes(Ae.eq),Te=(0,r.useMemo)(function(){return J?(0,d._)(B.oT).concat([\"hide_left_toolbar_by_default\"]):B.oT},[J]),N=(0,se.Ye)(),w=(0,se.v3)({symbol:n}),re=(0,Fe.es)(n),ie=yn({namespace:a,useMiniTicker:s,isCM:re}),le=ie.datafeed,de=ie.symbolsInfo,Be=(0,Je.DP)(),ve=Be.theme,Ce=ve===\"light\"||ve===\"light_glacier\"?\"light\":\"dark\",ce=(0,yr.Z)(Ce),Ke=(0,r.useRef)(-1),Ze=Ht({symbol:n}).positionLines,Ge=Nt({symbol:n,namespace:a}).orderLines,Xe=ir({symbol:n}).executionOrders,Ie=pt({namespace:a,symbol:n}),He=(0,se.y0)(),ur=(0,r.useRef)(He),Tr=(0,r.useRef)(N),_r=(0,r.useRef)(l),Cr=(0,r.useRef)(J),Er=(0,r.useRef)(oe),we=(0,r.useRef)(de);(0,r.useEffect)(function(){we.current=de},[de]),(0,r.useEffect)(function(){ur.current=He},[He]),(0,r.useEffect)(function(){Tr.current=N},[N]),(0,r.useEffect)(function(){_r.current=l},[l]),(0,r.useEffect)(function(){Cr.current=J},[J]),(0,r.useEffect)(function(){Er.current=oe},[oe]),(0,Ve.a)(n,s),(0,sr.A)({symbol:n,interval:Y,priceType:j});var Dr=(0,r.useCallback)(function(){Ke.current=Date.now()},[]),z=(0,r.useCallback)((0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})()),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er==\"string\"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}),[a]),Q=(0,r.useMemo)(function(){return z},[z]),ee=(0,or.R)(),ae=(0,Ne.fQ)(a),Se=(0,Ne.m3)(a),xe=(0,Ne.S_)(a),ne=(0,Ne.Ds)(a),ke=(0,r.useRef)(n),Me=(0,r.useRef)(Y),fe=(0,r.useCallback)(function(){return ee(\"tradingView\"),xe({panel_index:0,symbol:n,interval:Y}),hn()},[Y,ee,n,xe]),Pe=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be,vr,fr,er=(fr=(vr=(be=qe.symbolExt())===null||be===void 0?void 0:be.name)!==null&&vr!==void 0?vr:qe.symbol())!==null&&fr!==void 0?fr:\"\";ae({panel_index:ze,symbol:er,previous_symbol:ke.current}),ke.current=er},[ae]),_e=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be=qe.resolution();Se({panel_index:ze,interval:be,previous_interval:Me.current}),Me.current=be},[Se]),je=(0,r.useCallback)(function(){gn(Ke.current)},[]),Ee=(0,r.useCallback)(function(){},[]),me=(0,r.useMemo)(function(){return J?Ee:void 0},[Ee,J]),ye=(0,r.useCallback)((function(){var De=(0,o._)(function(ze){var qe,be,vr,fr,er,nr,mr,ar,Sr;return(0,i._)(this,function(Wr){switch(Wr.label){case 0:return qe=ze.chart,be=qe.symbolExt(),be?(vr=b()(we.current,function(Gr){return Gr.ticker===be.ticker||Gr.symbol===be.ticker}),fr=vr||{},er=fr.params,nr=er===void 0?{}:er,mr=nr.priceType,ar=mr===void 0?\"\":mr,ar&&$(ar),Sr=Tr.current[be.name],Sr?Er.current||Cr.current?[2]:[4,z()]:[2]):[2];case 1:return Wr.sent(),G({stateSymbol:Sr.symbol,isDeliveryColumn:Sr.isDelivery}),[2]}})});return function(ze){return De.apply(this,arguments)}})(),[G,z,$]),$e=qr(),Oe=$e.onCrosshairMoved,lr=$e.crosshairPrice,Or=wr({namespace:a,symbol:n,priceType:j,crosshairPrice:lr}),dr=lt(),Lr=(0,r.useMemo)(function(){return L({symbol:n,params:{priceType:j}})},[n,j]),Ur=(0,r.useMemo)(function(){return{symbol:Lr,theme:ce,limitOrders:Ge,positionOrders:Ze,executionOrders:Xe,labelLines:Ie,initialConfig:{tvConfig:{symbol:Lr,library_path:\"/\".concat(Ye.u_,\"/trading-platform-30/\"),theme:ce,datafeed:le,enabled_features:Te,context_menu:Or,custom_formatters:dr},onCrosshairMoved:Oe,onScriptLoaded:Dr,onSave:Q,onChartReadyDone:fe,onFirstTimeDataLoadedDone:je,onInitChart:me,onActiveChartChanged:ye,onSymbolChanged:Pe,onIntervalChanged:_e}}},[Lr,le,Te,Xe,Ie,Or,dr,Oe,ye,fe,je,me,_e,Q,Dr,Pe,Ge,Ze,ce]),Yr=Le({namespace:a,type:\"tv\"}),ct=(0,r.useCallback)(function(){ne({panel_index:0,symbol:n}),Yr()},[Yr,n,ne]);return(0,p.jsxs)(D.t,{name:\"trading_view\",onReset:ct,children:[(0,p.jsx)(Ue.A,{namespace:a,tradingViewProps:Ur}),w&&j===\"index\"&&O===c.Ev.Single&&W===c.tU.TradingView&&(0,p.jsx)(rr.B,{})]})};const Cn=Dt}"; + +/** Static factory generation preserves the host lexical body except its mirror write expression. */ +export function createMirrorFactory(__dispatchChartMirror) { + if (typeof __dispatchChartMirror !== 'function') throw new Error('Mirror dispatch must be a function'); + return {70940(Z,x,e){"use strict";e.r(x),e.d(x,{TradingView:()=>Dt,default:()=>Cn});var o=e(49253),v=e(77135),d=e(75510),i=e(42288),p=e(31085),r=e(41594),M=e(43917),h=e.n(M),P=e(81936),b=e.n(P),V=e(7980),U=e.n(V),D=e(76469),c=e(4260),k=e(53837),B=e(27571),C=e(45250),y=e(87646),A=e.n(y),q=e(80817);const E=(0,C.range)(1,1380),m=(0,C.range)(1,365).map(t=>`${t}D`),R=(0,C.range)(1,52).map(t=>`${t}W`),I=(0,C.range)(1,12).map(t=>`${t}M`),F=["1S",...E,...m,...R,...I],L=({symbol:t,params:n})=>{const a=new URLSearchParams(n);return`${t}@${a.toString()}`},X=t=>{const[n,a]=t.split("@"),s=new URLSearchParams(a).entries(),g=A()(Array.from(s));return{symbol:n,params:g}},ue=()=>{const t=new Map;let n={},a=[],u={onReady:()=>null,resolveSymbol:()=>null,searchSymbols:()=>null,getBars:()=>null,subscribeBars:()=>null,unsubscribeBars:()=>null};return({config:s,onSymbolResolving:g,onSymbolResolved:S,onFetchSymbolsInfo:K,onFetchBars:Y,onSubscribeBars:H,onUnsubscribeBars:j,onFetchServerTimeApi:$,onTransformSymbolInfo:f=({template:T})=>T,onSearchSymbols:l=()=>!0})=>(u={onReady:T=>{setTimeout(async()=>{const _=(await K()).map(O=>({...O,ticker:L({symbol:O.symbol,params:O.params||{}})}));a=_.map(({ticker:O,symbol:G,description:te="",exchange:J="",type:oe=""})=>({symbol:G,ticker:O,description:te,exchange:J,type:oe})),n=(0,C.keyBy)(_,"ticker"),T({exchanges:[],symbols_types:[],supported_resolutions:F,supports_marks:!1,supports_timescale_marks:!1,supports_time:!0,...s})})},resolveSymbol:async(T,W,_)=>{const O=n[T];if(!O){_(`cannot resolve symbol - ${T}`);return}const G=f({info:O,template:{description:O.description||"",fractional:!1,has_seconds:!0,seconds_multipliers:["1"],has_intraday:!0,intraday_multipliers:["1","3","5","15","30","60","120","240","360","480","720"],has_daily:!0,daily_multipliers:["1"],has_weekly_and_monthly:!0,weekly_multipliers:["1"],monthly_multipliers:["1"],minmov:1,minmove2:0,name:O.symbol,ticker:O.ticker,timezone:"Etc/UTC",pricescale:O.tickSize,session:"24x7",type:O.type||"",exchange:O.exchange||"",listed_exchange:O.exchange||"",format:"price",supported_resolutions:F,visible_plots_set:"ohlcv",volume_precision:3}});if(g){const te=X(G.ticker||"");if(!await g({symbol:T,info:G,tickerInfo:te})){_(`symbol resolving error - ${T}`);return}}setTimeout(()=>{W(G),S?.(G)})},searchSymbols:(T,W,_,O)=>{const G=new RegExp(T,"i"),te=new RegExp(W,"i"),J=new RegExp(_,"i"),oe=a.filter(Te=>{const{symbol:N,ticker:w,description:re="",exchange:ie="",type:le=""}=Te;return[N,w,re].some(de=>G.test(de||""))&&te.test(ie)&&J.test(le)&&l?.({item:Te})});O(oe)},getBars:async(T,W,_,O,G)=>{const{from:te,to:J,firstDataRequest:oe}=_,Te=(0,q.Cz)(W);try{if(J>=0){const N=X(T.ticker||""),w=await Y({symbolInfo:T,resolution:W,rangeStartDate:te,rangeEndDate:J,tickerInfo:N,binanceInterval:Te,firstDataRequest:oe});O(w,{noData:w.length===0})}else O([],{noData:!0})}catch(N){G(N.toString())}},subscribeBars:(T,W,_,O,G)=>{const te=X(T.ticker||""),J=(0,q.Cz)(W),oe=H({symbolInfo:T,resolution:W,callback:_,onResetCacheNeededCallback:G,tickerInfo:te,binanceInterval:J});t.set(O,oe)},unsubscribeBars:T=>{t?.get(T)?.(),t?.delete(T),j?.(T)},getServerTime:async T=>{try{const W=await $();T(W)}catch(W){console.warn(W),T(Date.now()/1e3)}}},{datafeed:u,observers:t,symbolsInfo:n})},Le=({namespace:t,type:n})=>{const a=(0,r.useMemo)(()=>h().createInstance({name:t}),[t]);return(0,r.useCallback)(()=>{n==="tv"&&a.removeItem("myTradingView")},[a,n])};var Ue=e(76535),pr=e(26531),Ye=e(17409),Qe=e(94652),Ae=e(76830),he=e(48651),Fe=e(79344),se=e(51029),Je=e(36307),or=e(25379),yr=e(44745),Ne=e(15727),rr=e(45358),sr=e(82071),Ve=e(85411),ge=e(94889),hr=e(94547),tr=e(80686),Ar=e(61875);function Fr(t){return(0,ge._)(t)||(0,hr._)(t)||(0,Ar._)(t)||(0,tr._)()}var Nr=e(92873),zr=e(35630),Vr=e(12055),Qr=e(79515),Br=e(80737),Jr=e(40545),Zr=e(98107),Kr=e(51846),Xr="Chart.Crosshair.PlusButton.DrawHorizontalLine",wr=function(t){var n=t.namespace,a=t.symbol,u=t.priceType,s=t.crosshairPrice,g=(0,Nr.o)("","kline-ui").getI18n,S=(0,Vr.r)().format,K=k.y$(n),Y=(0,Br.S)({namespace:n}),H=(0,Jr.hw)(),j=(0,Zr.c)(),$=(0,Qr.nH)(),f=(0,se.Ye)(),l=(0,Kr.E)({symbol:a}),T=(0,Fe.f0)(a),W=T.isCM,_=(0,r.useRef)(s);(0,r.useEffect)(function(){_.current=s},[s]);var O=(0,r.useRef)(H);(0,r.useEffect)(function(){O.current=H},[H]);var G=(0,r.useRef)(j);(0,r.useEffect)(function(){G.current=j},[j]);var te=(0,r.useRef)(u);(0,r.useEffect)(function(){te.current=u},[u]);var J=(0,r.useRef)($);(0,r.useEffect)(function(){J.current=$},[$]);var oe=(0,r.useRef)(f);(0,r.useEffect)(function(){oe.current=f},[f]);var Te=(0,r.useRef)(l);(0,r.useEffect)(function(){Te.current=l},[l]);var N=(0,r.useRef)(W);return(0,r.useEffect)(function(){N.current=W},[W]),{items_processor:(function(){var w=(0,o._)(function(re,ie,le){var de,Be,ve,Ce,ce,Ke,Ze,Ge,Xe,Ie,He,ur,Tr,_r,Cr;return(0,i._)(this,function(Er){if(Be=K.getState().tradingViewReference,!Be)return console.warn("[context-menu] trading view reference is not found"),[2,re];if(Be.chartsCount()>1)return[2,re];if(ve=Be.activeChart().symbolExt(),!ve)return[2,re];switch(Ce=ve.name,ce=_.current,Ke=le.menuName==="CrosshairMenuView"?"chart-trading":"context-menu",Ze={price:"".concat(ce.value),formattedPrice:ce.formatted,source:Ke,chartType:"tradingview"},Ge=(de=oe.current[Ce])===null||de===void 0?void 0:de.baseAsset,Xe=g("trade",{defaultValue:"Trade"}),Ie=S(ce.formatted),He=G.current&&te.current===c.SJ.Last&&J.current&&Ge?[ie.createAction({actionId:"Chart.PlaceOrder.BuyLimit",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("limit",{defaultValue:"Limit"})),onExecute:function(){return Te.current.onClickBuyLimit(Ze)}}),ie.createAction({actionId:"Chart.PlaceOrder.BuyStop",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("stop",{defaultValue:"Stop"})),onExecute:function(){return Te.current.onClickBuyStop(Ze)}})]:[],ur=O.current?ie.createAction({actionId:"Chart.PriceAlert.CreateAtCrosshair",label:g("create-alert-at",{defaultValue:"Create Alert at {{price}}",price:ce.formatted}),onExecute:function(){Y({symbol:Ce,price:ce.formatted,mode:zr.n.CREATE})}}):null,re.forEach(function(we){if(!(!("execute"in we)||!("getState"in we))){var Dr=we.getState();if(Dr.actionId===Xr){var z=we.execute.bind(we);we.execute=function(){(0,Kr.A)({elementId:"draw_horizontal_line",isDelivery:N.current,chartType:"tradingview"}),z()}}}}),le.menuName){case"CrosshairMenuView":return ur?[2,[ur].concat((0,d._)(He),(0,d._)(re.slice()))]:[2,(0,d._)(He).concat((0,d._)(re.slice()))];case"ChartContextMenu":return ur?(Tr=Fr(re),_r=Tr[0],Cr=Tr.slice(1),[2,[_r,ur].concat((0,d._)(He),(0,d._)(Cr))]):[2,(0,d._)(He).concat((0,d._)(re.slice()))];default:return[2,re]}return[2]})});return function(re,ie,le){return w.apply(this,arguments)}})()}},qr=function(){var t=(0,r.useState)({value:NaN,formatted:""}),n=t[0],a=t[1],u=(0,r.useCallback)(function(s){var g=s.crosshairData,S=s.activeChart;S&&a({value:g.price,formatted:S.priceFormatter().format(g.price)})},[]);return{onCrosshairMoved:u,crosshairPrice:n}};const Hr=t=>Math.floor(Math.log10(t)),et=t=>{const[n="1"]=t.split(",");return Hr(+n)},pe=({symbolInfo:t,minTick:n})=>{const{pricescale:a=2}=t||{};return n==="default"||n===void 0?Hr(a):et(n)},br=t=>(n,a)=>{try{return n?{format:u=>{const s=pe({symbolInfo:n,minTick:a});return t.format({symbolInfo:n,minTick:a,value:u,precision:s})}}:null}catch{return null}},Mr=10**12,rt=10**9,tt=10**6,jr=10**3,gr=t=>{const n=t.toString(),[,a=""]=n.split(".");return Math.min(a.length,3)},nt=({value:t})=>{const n=Math.abs(t);if(n>=Mr){const a=t/Mr;return{value:a,unit:"T",precision:gr(a)}}if(n>=rt){const a=t/rt;return{value:a,unit:"B",precision:gr(a)}}if(n>=tt){const a=t/tt;return{value:a,unit:"M",precision:gr(a)}}if(n>=jr){const a=t/jr;return{value:a,unit:"K",precision:gr(a)}}return{value:t,unit:"",precision:gr(t)}},ut=t=>(n,a,u)=>{try{return a&&n.type==="volume"?{format:s=>s===void 0?"":t.formatVolume({format:n,symbolInfo:a,precision:u,value:s,volumeDetail:nt({value:s})})}:null}catch{return null}};var lt=function(){var t=(0,Vr.r)(),n=t.format,a=(0,r.useMemo)(function(){return br({format:function(s){var g=s.value,S=s.precision;return n(g,{precision:S})}})},[n]),u=(0,r.useMemo)(function(){return ut({formatVolume:function(s){var g=s.value,S=s.volumeDetail;if(g===void 0||!S)return"";var K=n(S.value,{precision:S.precision});return"".concat(K).concat(S.unit)}})},[n]);return{priceFormatterFactory:a,studyFormatterFactory:u}},dt=e(74069),ir=function(t){var n=(0,dt.K)(t),a=(0,r.useMemo)(function(){return n.map(function(u){var s=u.id,g=u.time,S=u.isBuy,K=u.price,Y=u.tooltip;return{id:s,text:"",price:K,quantity:"",time:Math.round(g/1e3),isBuy:S,tooltip:Y}})},[n]);return{executionOrders:a}},at=function(t){var n=t.namespace,a=t.symbol,u=(0,Br.j)({namespace:n,symbol:a});return(0,r.useMemo)(function(){return u.map(function(s){var g=s.id,S=s.price,K=s.formattedPrice,Y=s.sideText,H=s.showCloseButton,j=s.isPriceChangeable,$=s.cancelTooltipText,f=s.onClick,l=s.onCancel,T=s.onPriceChanged;return{id:g,price:+S,text:"".concat(Y," ").concat(K),editable:j,cancellable:H,cancelTooltip:$,type:c.ND.PriceAlert,onCancel:H?l:void 0,onMove:j?function(W){var _=W.price;T({currentPrice:_})}:void 0,onClick:f}})},[u])},vt=e(90664),ot=function(t){var n=t.symbol,a=(0,vt.w)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){var s=u.id,g=u.text,S=u.price;return{id:s,text:g,price:+S,type:c.ND.BreakEvenPrice}})},[a])},cr=e(82508),ft=e(91025),mt=e(36077),Rr=function(t){var n=t.symbol,a=(0,mt.c)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){return(0,ft._)((0,cr._)({},u),{type:c.ND.LiquidationPrice})})},[a])},pt=function(t){var n=t.namespace,a=t.symbol,u=at({namespace:n,symbol:a}),s=ot({symbol:a}),g=Rr({symbol:a});return(0,r.useMemo)(function(){return(0,d._)(u).concat((0,d._)(s),(0,d._)(g))},[u,s,g])},st=e(42902),Ir=e(90170),yt=e(63345),ht=e(48210),Pr=e(51289),xr=e(24326),$r=e(90922),kr=e(34235),it=e(63762),Ft=function(t){var n=(0,Fe.f0)(t),a=n.pricePrecision,u=n.isCM,s=(0,st.yD)(t)||{},g=s.lastPrice,S=g===void 0?"0":g,K=(0,kr.Bp)(u),Y=(0,it.HY)(t),H=(0,kr.gv)(u),j=(0,kr.WF)(u);return(0,r.useMemo)(function(){return(0,$r.qF)({orders:(0,d._)(K).concat((0,d._)(H),(0,d._)(j)),symbol:t}).map(function($){return Y({order:$,lastPrice:S,pricePrecision:a})})},[S,K,H,a,t,Y])},gt=e(31542),Tt=function(t){var n=t.type;(0,gt.u4)("webClick",{module:"chart",elementId:n})},Nt=function(t){var n=t.symbol,a=t.namespace,u=(0,r.useState)(0),s=u[0],g=u[1],S=(0,r.useCallback)(function(){return g(function(N){return N+1})},[]),K=Ft(n),Y=(0,xr.IT)({namespace:a}),H=(0,Fe.f0)(n),j=H.isDelivery,$=H.contractType,f=(0,st.yD)(n)||{},l=f.lastPrice,T=l===void 0?"0":l,W=(0,yt.s)(n),_=(0,it.YZ)({symbol:n,afterConfirmationModalIsShown:S}),O=(0,it.u8)(n),G=(0,it.jn)(n),te=(0,r.useCallback)(function(N){return(0,ht.D)({order:N,isDelivery:j,symbol:j?N.symbol:"".concat(N.symbol,"_").concat($),module:"tradingview_kline"})},[$,j]),J=(0,r.useMemo)(function(){return K.map(function(N){var w=N.order,re=N.price,ie=N.quantity,le=N.quantityText,de=N.isBuy,Be=N.text,ve=N.priceTooltipText,Ce=N.pnlText,ce=(0,Pr.$L)(w),Ke=(0,Pr.OX)(w),Ze=(0,Pr.xL)(w),Ge=ce==="LIMIT",Xe=(0,Ir.Y)(w);return{id:String(Ke),text:Be,price:+re,quantity:le,isBuy:de,editable:Xe&&G,tooltip:ve,modifyTooltip:w.closePosition||!Ge?"":void 0,onMove:G?function(Ie){var He=Ie.price;te(w),Tt({type:"TV_vertical"}),_({order:w,price:+He,quantity:+Ze,lastPrice:T})}:void 0,onModify:w.closePosition||!Ge||!Xe?void 0:function(){Tt({type:"TV_amountclick"}),O({order:w,price:re.toString(),quantity:ie,lastPrice:T})},onCancel:function(){Tt({type:"TV_cancel"}),W(w)}}})},[G,K,T,te,_,O,W,s]),oe=(0,r.useMemo)(function(){return Y.map(function(N){var w=N.id,re=N.value,ie=N.text,le=N.onChange,de=N.isBuy,Be=N.type;return{id:w,price:+re,quantity:"",text:ie,isBuy:de,editable:!0,limitOrderType:c.aA.OrderPreview,onMove:function(ve){var Ce=ve.price;(0,xr.eP)({namespace:a,orderType:Be}),le({id:w,newPrice:Ce})},onMoving:function(ve){var Ce=ve.price;return le({id:w,newPrice:Ce})}}})},[Y,a]),Te=(0,r.useMemo)(function(){return(0,d._)(J).concat((0,d._)(oe))},[J,oe]);return{orderLines:Te}},Vt=e(17855),Bt=function(t,n){var a=(0,r.useState)(t()),u=a[0],s=a[1];return(0,Vt.A)(function(){return s(t())},n),u},Kt=e(58656),Ht=function(t){var n=t.symbol,a=(0,Kt.Q)({symbol:n})||[],u=Bt(function(){return a.map(function(s){var g=s.position,S=s.pnl,K=s.pnlInNumber,Y=s.pnlText,H=s.price,j=s.quantity,$=s.reverseText,f=s.isBuy,l=s.displayReverseOrderModal,T=s.displayClosePositionModal;return{id:g.id||"".concat(g.symbol,"-").concat(g.positionSide),PNL:K,text:"".concat(Y," ").concat(S),price:H,quantity:j,tooltip:"",reverseTooltip:$,isBuy:f,onReverse:l,onClose:function(){T(),(0,gt.u4)("$WebClick",{item:1,module:"xxx",$element_id:"kline_close_position"})}}})},[a]);return{positionLines:u}},jt=e(47354),bt=e.n(jt),$t=e(87017),Ut=e(99168),Yt=e(3742),Wt=e(43335),Gt=e(15540),zt=e(20163),Qt=e(47255);const Jt=t=>({endTime:n})=>(n??0)===t;var Zt=e(36488),Ct=e(48018),Xt=e(82911),We=e(69578),wt=e(53208),Rt=e(19862),qt=e(8874),en=e(26679),rn=e(66547),It=(function(){var t=(0,o._)(function(n){var a,u,s,g,S,K,Y,H,j;return(0,i._)(this,function($){switch($.label){case 0:a=n.onFetchKlineData,u=n.startTime,s=n.endTime,g=n.interval,S=n.limit,K=S===void 0?1e3:S,Y=[],H=[],j=s,$.label=1;case 1:return[4,a((0,cr._)({interval:g,limit:K},j===0?{}:{endTime:j}))];case 2:if(H=$.sent(),H.length===0)return[3,4];H.forEach(function(f){var l=(0,v._)(f,8),T=l[0],W=l[1],_=l[2],O=l[3],G=l[4],te=l[5],J=l[7];Y.push({time:+T,open:+W,high:+_,low:+O,close:+G,volume:+te,quantity:+J})}),j=H[0][0]-1,$.label=3;case 3:if(j>u)return[3,1];$.label=4;case 4:return Y.sort(function(f,l){return f.time-l.time}),[2,Y]}})});return function(a){return t.apply(this,arguments)}})(),tn=function(t){var n=t.firstDataRequest,a=t.hasRequested,u=t.rangeEndDate;return n&&!a?0:u*1e3},nn=e(78142),an=e(16238),St=e.n(an),on=e(98087),xt=e.n(on),sn=e(55009),cn=e.n(sn),kt=e(56477);const Et=t=>cn()(t.chartsCount(),n=>t.chart(n)),un=async t=>{try{const{schema:n,data:a}=await t.exportData({includedStudies:[]}),u=xt()(a)?.[n.findIndex(s=>s.type==="time")];return u!==void 0&&Number.isFinite(u)?u*1e3:void 0}catch{return}},ln=({getWidget:t,...n})=>{const a=new Map,u=new Map,s=()=>{try{const f=t();return f?St()(Et(f).map(l=>l.symbolExt()?.ticker)):[]}catch{return[]}},g=f=>{const l=s();return!l.length||l.includes(f)},S=f=>{const l=u.get(f);if(l)return l;const T={controller:new AbortController,claimedAsMain:!1};return u.set(f,T),T},K=f=>{const l=u.get(f);l&&(l.controller.abort(),u.delete(f))},Y=()=>{const f=s();f.length&&Array.from(u.keys()).forEach(l=>{const T=u.get(l);if(T){if(f.includes(l)){T.claimedAsMain=!0;return}a.has(l)||T.claimedAsMain&&K(l)}})},H=(f,l)=>{f.closed||l.time{f.held=[];let T=l;try{const _=(await f.options.fetchSince(l)).filter(O=>O.time>=l&&!f.closed);_.forEach(O=>{f.highWater=Math.max(f.highWater,O.time),f.options.onTick(O)}),T=xt()(_)?.time??l}finally{const W=f.held??[];f.held=void 0,W.filter(_=>_.time>=T).forEach(_=>{H(f,_)})}},$=(0,kt.k)({...n,onCheck:Y,repair:async()=>{const f=t();if(f)try{const l=Et(f),T=St()(l.map(_=>{const O=a.get(_.symbolExt()?.ticker??"");return O&&{chart:_,feed:O}}));if(!T.length){f.resetCache(),l.forEach(_=>{_.resetData()});return}const W=St()(await Promise.all(T.map(async({chart:_,feed:O})=>{const G=await un(_);return G===void 0?void 0:{feed:O,fromMs:G}})));await Promise.allSettled(W.map(({feed:_,fromMs:O})=>j(_,O)))}catch{}}});return{openFeed:f=>{const l={options:f,held:void 0,highWater:0,closed:!1,detector:void 0};return l.detector=(0,kt.k)({...n,subscribeSignals:f.subscribeSignals,repair:async()=>{l.closed||!l.highWater||g(f.ticker)||await j(l,l.highWater)}}),a.set(f.ticker,l),g(f.ticker)&&$.resubscribeSignals(f.subscribeSignals),{push:T=>{l.held?l.held.push(T):H(l,T)},close:()=>{l.closed=!0,l.detector?.dispose(),a.get(f.ticker)===l&&a.delete(f.ticker)}}},subscribeWake:$.subscribeWake,getSignal:f=>{const l=S(f);return s().includes(f)&&(l.claimedAsMain=!0),l.controller.signal},dispose:()=>{u.forEach(({controller:f})=>{f.abort()}),u.clear(),a.forEach(f=>{f.detector?.dispose()}),a.clear(),$.dispose()}}};var dn=e(71730),vn=e(36620),fn=function(t){var n=t.namespace,a=k.y$(n),u=(0,vn.l)().getSocketSignals,s=(0,r.useRef)();(0,r.useEffect)(function(){return function(){var S;(S=s.current)===null||S===void 0||S.instance.dispose(),s.current=void 0}},[]);var g=(0,r.useCallback)(function(S){var K,Y,H=S;if(((K=s.current)===null||K===void 0?void 0:K.key)===H)return s.current.instance;(Y=s.current)===null||Y===void 0||Y.instance.dispose();var j=nn.wr(S),$=ln({getWidget:function(){return a.getState().tradingViewReference},checkPeriodMs:dn.D(j)});return s.current={key:H,instance:$},$},[a]);return(0,r.useMemo)(function(){return{ensureRecovery:g,getSocketSignals:u}},[g,u])},mn="tv_web_error",pn=[Ye.bt.TRADING,Ye.bt.DELIVERING],Re;(function(t){t.Futures="futures",t.FuturesMark="futures-mark",t.FuturesIndex="futures-index",t.Delivery="delivery",t.DeliveryMark="delivery-mark",t.DeliveryIndex="delivery-index"})(Re||(Re={}));var Mt=function(t){var n=t.symbol,a=t.description,u=t.productType,s=t.pricePrecision,g=t.params,S=t.supportedResolutions,K=S===void 0?F:S;return{symbol:n,description:a,tickSize:Math.pow(10,s),exchange:"Binance",supportedResolutions:K,type:u,params:g}},yn=function(t){var n=t.namespace,a=t.useMiniTicker,u=a===void 0?!1:a,s=t.isCM,g=c.y$(n),S=k.y$(n),K=(0,$t.useQueryClient)(),Y=fn({namespace:n}),H=Y.ensureRecovery,j=Y.getSocketSignals,$=(0,r.useRef)(0),f=(0,r.useRef)(!1),l=(0,r.useMemo)(function(){return bt()(function(){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsStart",catch:function(Q){var ee=Q.error,ae=Q.track;ae({GET_BAR_START_TYPE:ee})},log:function(Q){var ee=Q.track;ee({GET_BAR_START_TYPE:"SUCCESS"})}})})},[]),T=(0,r.useMemo)(function(){return bt()(function(z){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsEnd",catch:function(ee){var ae=ee.error,Se=ee.track;Se({GET_BAR_END_TYPE:ae,duration_:(0,We.Pu)(z-$.current)})},log:function(ee){var ae=ee.track;ae({GET_BAR_END_TYPE:"SUCCESS",duration_:(0,We.Pu)(z-$.current)})}})})},[]),W=(0,Nr.o)("trd-chart","trade-ui"),_=W.getI18n,O=(0,he.ud)(),G=O.getI18n,te=(0,v._)(g(c.Ir),1),J=te[0],oe=(0,v._)(S(k.A7),2),Te=oe[1],N=(0,se.Ye)(),w=(0,Zt.JD)(s).isContUnit,re=(0,se.y_)(),ie=(0,r.useRef)(N),le=(0,r.useRef)(J),de=(0,r.useRef)(re);(0,r.useEffect)(function(){ie.current=N},[N]),(0,r.useEffect)(function(){le.current=J},[J]),(0,r.useEffect)(function(){de.current=re},[re]);var Be=(0,r.useMemo)(function(){return w?Rt.b:Rt.S},[w]),ve=(0,r.useMemo)(function(){return G("usdtFutures-13",{defaultValue:"Futures"})||""},[G]),Ce=(0,r.useMemo)(function(){return G("inverseFutures-13",{defaultValue:"Delivery"})||""},[G]),ce=(0,r.useMemo)(function(){return _("indexPrice",{defaultValue:"Index Price"})||""},[_]),Ke=(0,r.useMemo)(function(){return _("markPrice",{defaultValue:"Mark Price"})||""},[_]),Ze=(0,r.useMemo)(function(){return _("lastPrice",{defaultValue:"Last Price"})||""},[_]),Ge=(0,r.useMemo)(function(){return{exchanges:[{name:"Binance",value:"Binance",desc:""}],symbols_types:[{name:ve,value:Re.Futures},{name:"".concat(ve," ").concat(Ke),value:Re.FuturesMark},{name:"".concat(ve," ").concat(ce),value:Re.FuturesIndex},{name:Ce,value:Re.Delivery},{name:"".concat(Ce," ").concat(Ke),value:Re.DeliveryMark},{name:"".concat(Ce," ").concat(ce),value:Re.DeliveryIndex}]}},[Ce,ve,ce,Ke]),Xe=(0,r.useCallback)((0,o._)(function(){var z,Q,ee;return(0,i._)(this,function(ae){switch(ae.label){case 0:return ae.trys.push([0,2,,3]),[4,(0,en.c)(K)];case 1:return ae.sent(),[3,3];case 2:return z=ae.sent(),(0,gt.u4)(mn,{type:"onFetchSymbolsInfo",message:z.message,business:"futures"}),[3,3];case 3:return Q=ie.current,ee=Object.keys(Q).reduce(function(Se,xe){var ne=Q[xe]||Ct.o,ke=ne.symbol,Me=ne.pricePrecision,fe=ne.isDelivery,Pe=ne.contractStatus;if(!pn.includes(Pe))return Se;var _e=(0,Yt.S)(ne,G),je=_e.symbol,Ee=_e.contractTypeI18n,me=_e.deliveryDateFormat,ye="".concat(je," ").concat(Ee).concat(me?" ".concat(me):""),$e="".concat(ye," ").concat(Ze),Oe="".concat(ye," ").concat(ce),lr="".concat(ye," ").concat(Ke);return Se.concat([Mt({symbol:ke,description:$e,productType:fe?Re.Delivery:Re.Futures,pricePrecision:Me,params:{priceType:c.SJ.Last}}),Mt({symbol:ke,description:Oe,productType:fe?Re.DeliveryIndex:Re.FuturesIndex,pricePrecision:Me,params:{priceType:c.SJ.Index}}),Mt({symbol:ke,description:lr,productType:fe?Re.DeliveryMark:Re.FuturesMark,pricePrecision:(0,Xt.X)(Me),params:{priceType:c.SJ.Mark}})])},[]),[2,ee]}})}),[G,ce,Ke,Ze,K]),Ie=(0,r.useCallback)(function(z){var Q=z.ticker,ee=z.symbol,ae=z.interval,Se=z.priceType,xe=z.retryUntil,ne=ie.current[ee]||Ct.o,ke=ne.pair,Me=ne.contractType,fe=ne.contractVal,Pe=ne.isDelivery,_e=Pe?Be(fe):function(Oe){return Oe},je=(0,wt.h)({symbol:ee,pair:ke,contractType:Me,isDelivery:Pe,priceType:Se}),Ee,me,ye=function(Oe){return zt.y("tv","".concat(Pe?"cm":"um","|").concat(ee,"|").concat(Se,"|").concat(Oe.interval,"|").concat((Ee=Oe.limit)!==null&&Ee!==void 0?Ee:"","|").concat((me=Oe.endTime)!==null&&me!==void 0?me:""))};if(xe===void 0)return{transform:_e,onFetchKlineData:je};var $e=H(ae);return{transform:_e,onFetchKlineData:Qt.b(je,{enabled:Jt(xe),key:ye,signal:$e.getSignal(Q),subscribeWake:$e.subscribeWake})}},[H,Be]),He=(0,r.useCallback)((function(){var z=(0,o._)(function(Q){var ee,ae,Se,xe,ne,ke,Me,fe,Pe,_e,je,Ee,me,ye,$e,Oe,lr,Or;return(0,i._)(this,function(dr){switch(dr.label){case 0:return ee=Q.binanceInterval,ae=Q.rangeStartDate,Se=Q.rangeEndDate,xe=Q.tickerInfo,ne=Q.symbolInfo,ke=Q.firstDataRequest,$.current=Date.now(),l(),Me=xe.symbol,fe=Me===void 0?"":Me,Pe=xe.params,_e=Pe.priceType,je=_e===void 0?le.current:_e,de.current({symbol:fe,priceType:je})?(T(Date.now()),[2,[]]):(Ee=Math.max(0,ae*1e3),me=tn({firstDataRequest:ke,hasRequested:f.current,rangeEndDate:Se}),f.current=!0,$e=Ie({ticker:(ye=ne.ticker)!==null&&ye!==void 0?ye:"",symbol:fe,interval:ee,priceType:je,retryUntil:ke?me:void 0}),Oe=$e.transform,lr=$e.onFetchKlineData,[4,It({onFetchKlineData:lr,startTime:Ee,endTime:me,interval:ee})]);case 1:return Or=dr.sent(),T(Date.now()),[2,Or.map(Oe)]}})});return function(Q){return z.apply(this,arguments)}})(),[Ie,T,l]),ur=(0,r.useCallback)(function(z){var Q=z.binanceInterval,ee=z.callback,ae=z.tickerInfo,Se=z.symbolInfo,xe=ae.symbol,ne=xe===void 0?"":xe,ke=ae.params,Me=ke.priceType,fe=Me===void 0?le.current:Me;if(de.current({symbol:ne,priceType:fe}))return function(){return null};var Pe=ie.current[ne]||Ct.o,_e=Pe.pair,je=Pe.contractType,Ee=Pe.isDelivery,me,ye=(me=Se.ticker)!==null&&me!==void 0?me:"",$e=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe}).transform,Oe=H(Q),lr=Oe.openFeed({ticker:ye,onTick:ee,subscribeSignals:j(Ee),fetchSince:(function(){var dr=(0,o._)(function(Lr){var Ur,Yr,ct,De;return(0,i._)(this,function(ze){switch(ze.label){case 0:return Ur=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe,retryUntil:0}),Yr=Ur.transform,ct=Ur.onFetchKlineData,[4,It({onFetchKlineData:ct,startTime:Lr,endTime:0,interval:Q})];case 1:return De=ze.sent(),[2,De.map(Yr)]}})});return function(Lr){return dr.apply(this,arguments)}})()}),Or=(0,rn.B)({symbol:ne,pair:_e,contractType:je,interval:Q,priceType:fe,isDelivery:Ee,useMiniTicker:u,callback:function(dr){lr.push($e(dr))}});return function(){lr.close(),Or()}},[Ie,H,j,u]),Tr=(0,r.useCallback)((0,o._)(function(){var z;return(0,i._)(this,function(Q){switch(Q.label){case 0:return[4,K.fetchQuery(Wt.Bz.SERVER_TIME(s),function(){return(0,Gt.l)(s)},{})];case 1:return z=Q.sent(),[2,Math.floor(z/1e3)]}})}),[K,s]),_r=(0,r.useCallback)(function(z){return(0,cr._)({},z.template,(0,qt.sp)(s))},[s]),Cr=(0,r.useMemo)(function(){return ue()},[]),Er=Cr({config:Ge,onFetchSymbolsInfo:Xe,onFetchBars:He,onSubscribeBars:ur,onFetchServerTimeApi:Tr,onTransformSymbolInfo:_r}),we=Er.datafeed,Dr=Er.symbolsInfo;return(0,Ut.A)(function(){return Te()},[w]),{datafeed:we,symbolsInfo:Dr}},Pt="trd_chart_tv_load_timeout",hn=function(){var t=(0,We.PM)({id:"chartReady"}),n=(0,We.PM)({id:"getBarsStart",timeout:3}),a=(0,We.PM)({id:"getBarsEnd",timeout:6}),u=(0,We.PM)({id:"chartResponsive",timeout:3});We.HP.init({step:t._(n)._(a)._(u),eventId:Pt}),(0,We.HP)({eventId:Pt,stepId:"chartReady",catch:function(g){var S=g.error,K=g.track;K({CHART_READY_TYPE:S,connection:JSON.stringify(navigator.connection)})},log:function(g){var S=g.track;S({CHART_READY_TYPE:"SUCCESS",connection:JSON.stringify(navigator.connection)})}})},gn=function(t){(0,We.HP)({eventId:Pt,stepId:"chartResponsive",catch:function(a){var u=a.error,s=a.track;s({CHART_RESPONSIVE_TYPE:u,connection:JSON.stringify(navigator.connection)})},log:function(a){var u=a.track;u({CHART_RESPONSIVE_TYPE:"SUCCESS",duration_:(0,We.Pu)(Date.now()-t)})}})},Tn=[Qe.ZE,Qe.F3],Dt=function(t){var n=t.symbol,a=t.namespace,u=t.useMiniTicker,s=u===void 0?!1:u,g=c.y$(a),S=k.y$(a),K=(0,v._)(g(c.A8),1),Y=K[0],H=(0,v._)(g(c.Ir),2),j=H[0],$=H[1],f=(0,v._)(S(k.yj),1),l=f[0],T=(0,v._)(g(c.e6),1),W=T[0],_=(0,v._)(g(c._b),1),O=_[0],G=(0,he.ot)(),te=(0,pr.K7)(),J=te.pathname.includes(Ae.Q2),oe=te.pathname.includes(Ae.eq),Te=(0,r.useMemo)(function(){return J?(0,d._)(B.oT).concat(["hide_left_toolbar_by_default"]):B.oT},[J]),N=(0,se.Ye)(),w=(0,se.v3)({symbol:n}),re=(0,Fe.es)(n),ie=yn({namespace:a,useMiniTicker:s,isCM:re}),le=ie.datafeed,de=ie.symbolsInfo,Be=(0,Je.DP)(),ve=Be.theme,Ce=ve==="light"||ve==="light_glacier"?"light":"dark",ce=(0,yr.Z)(Ce),Ke=(0,r.useRef)(-1),Ze=Ht({symbol:n}).positionLines,Ge=Nt({symbol:n,namespace:a}).orderLines,Xe=ir({symbol:n}).executionOrders,Ie=pt({namespace:a,symbol:n}),He=(0,se.y0)(),ur=(0,r.useRef)(He),Tr=(0,r.useRef)(N),_r=(0,r.useRef)(l),Cr=(0,r.useRef)(J),Er=(0,r.useRef)(oe),we=(0,r.useRef)(de);(0,r.useEffect)(function(){we.current=de},[de]),(0,r.useEffect)(function(){ur.current=He},[He]),(0,r.useEffect)(function(){Tr.current=N},[N]),(0,r.useEffect)(function(){_r.current=l},[l]),(0,r.useEffect)(function(){Cr.current=J},[J]),(0,r.useEffect)(function(){Er.current=oe},[oe]),(0,Ve.a)(n,s),(0,sr.A)({symbol:n,interval:Y,priceType:j});var Dr=(0,r.useCallback)(function(){Ke.current=Date.now()},[]),z=(0,r.useCallback)((0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=__dispatchChartMirror(Sr,fr,()=>fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})())),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er=="string"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}),[a]),Q=(0,r.useMemo)(function(){return z},[z]),ee=(0,or.R)(),ae=(0,Ne.fQ)(a),Se=(0,Ne.m3)(a),xe=(0,Ne.S_)(a),ne=(0,Ne.Ds)(a),ke=(0,r.useRef)(n),Me=(0,r.useRef)(Y),fe=(0,r.useCallback)(function(){return ee("tradingView"),xe({panel_index:0,symbol:n,interval:Y}),hn()},[Y,ee,n,xe]),Pe=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be,vr,fr,er=(fr=(vr=(be=qe.symbolExt())===null||be===void 0?void 0:be.name)!==null&&vr!==void 0?vr:qe.symbol())!==null&&fr!==void 0?fr:"";ae({panel_index:ze,symbol:er,previous_symbol:ke.current}),ke.current=er},[ae]),_e=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be=qe.resolution();Se({panel_index:ze,interval:be,previous_interval:Me.current}),Me.current=be},[Se]),je=(0,r.useCallback)(function(){gn(Ke.current)},[]),Ee=(0,r.useCallback)(function(){},[]),me=(0,r.useMemo)(function(){return J?Ee:void 0},[Ee,J]),ye=(0,r.useCallback)((function(){var De=(0,o._)(function(ze){var qe,be,vr,fr,er,nr,mr,ar,Sr;return(0,i._)(this,function(Wr){switch(Wr.label){case 0:return qe=ze.chart,be=qe.symbolExt(),be?(vr=b()(we.current,function(Gr){return Gr.ticker===be.ticker||Gr.symbol===be.ticker}),fr=vr||{},er=fr.params,nr=er===void 0?{}:er,mr=nr.priceType,ar=mr===void 0?"":mr,ar&&$(ar),Sr=Tr.current[be.name],Sr?Er.current||Cr.current?[2]:[4,z()]:[2]):[2];case 1:return Wr.sent(),G({stateSymbol:Sr.symbol,isDeliveryColumn:Sr.isDelivery}),[2]}})});return function(ze){return De.apply(this,arguments)}})(),[G,z,$]),$e=qr(),Oe=$e.onCrosshairMoved,lr=$e.crosshairPrice,Or=wr({namespace:a,symbol:n,priceType:j,crosshairPrice:lr}),dr=lt(),Lr=(0,r.useMemo)(function(){return L({symbol:n,params:{priceType:j}})},[n,j]),Ur=(0,r.useMemo)(function(){return{symbol:Lr,theme:ce,limitOrders:Ge,positionOrders:Ze,executionOrders:Xe,labelLines:Ie,initialConfig:{tvConfig:{symbol:Lr,library_path:"/".concat(Ye.u_,"/trading-platform-30/"),theme:ce,datafeed:le,enabled_features:Te,context_menu:Or,custom_formatters:dr},onCrosshairMoved:Oe,onScriptLoaded:Dr,onSave:Q,onChartReadyDone:fe,onFirstTimeDataLoadedDone:je,onInitChart:me,onActiveChartChanged:ye,onSymbolChanged:Pe,onIntervalChanged:_e}}},[Lr,le,Te,Xe,Ie,Or,dr,Oe,ye,fe,je,me,_e,Q,Dr,Pe,Ge,Ze,ce]),Yr=Le({namespace:a,type:"tv"}),ct=(0,r.useCallback)(function(){ne({panel_index:0,symbol:n}),Yr()},[Yr,n,ne]);return(0,p.jsxs)(D.t,{name:"trading_view",onReset:ct,children:[(0,p.jsx)(Ue.A,{namespace:a,tradingViewProps:Ur}),w&&j==="index"&&O===c.Ev.Single&&W===c.tU.TradingView&&(0,p.jsx)(rr.B,{})]})};const Cn=Dt}}[70940]; +} + +/** Registration rejects source drift before any replacement can reach the runtime cache. */ +export function replaceChartMirrorFactory(originalFactory, dispatch) { + if (typeof originalFactory !== 'function' || Function.prototype.toString.call(originalFactory) !== originalFactorySource) { + throw new Error('Mirror factory source does not match the pinned public module'); + } + return createMirrorFactory(dispatch); +} diff --git a/src/binance-orderbook-trade/chart-storage/mirror-writer.js b/src/binance-orderbook-trade/chart-storage/mirror-writer.js new file mode 100644 index 0000000..9b50091 --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/mirror-writer.js @@ -0,0 +1,207 @@ +import { snapshotJson, byteLength, equalJson } from './json.js'; + +const DEFAULT_LIMITS = Object.freeze({ + maxEntries: 512, + maxBatchBytes: 4 * 1024 * 1024, + maxReadBytes: 4 * 1024 * 1024, + maxPendingBatches: 16, + maxPendingBytes: 64 * 1024 * 1024, +}); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +/** + * Batch only the host's explicit mirror destination writes. The runtime owns URL + * admission; localForage retains its instances, API methods and connections. + * Native routing is allowed only before an IndexedDB transaction is created. + */ +export function createChartMirrorWriter({ limits: suppliedLimits = {} } = {}) { + const limits = { ...DEFAULT_LIMITS, ...suppliedLimits }; + for (const [key, value] of Object.entries(limits)) { + assert(Object.hasOwn(DEFAULT_LIMITS, key), `Unknown mirror limit: ${key}`); + assert(Number.isSafeInteger(value) && value > 0, `${key} must be a positive safe integer`); + } + let phase = 'active'; + let stopping; + let resolveDrain; + const stats = { + acceptedBatches: 0, rejectedBatches: 0, failedBatches: 0, + pendingBatches: 0, pendingBytes: 0, peakPendingBatches: 0, peakPendingBytes: 0, + transactions: 0, committedTransactions: 0, abortedTransactions: 0, + committedWrites: 0, skippedWrites: 0, nativePassthroughBatches: 0, + }; + + function stop() { + if (phase === 'active') { + phase = 'draining'; + const drained = stats.pendingBatches === 0 + ? Promise.resolve() : new Promise(resolve => { resolveDrain = resolve; }); + stopping = drained.then(() => { phase = 'stopped'; }); + } + return stopping; + } + + function release(work) { + stats.pendingBatches -= 1; + stats.pendingBytes -= work.bytes; + if (phase === 'draining' && stats.pendingBatches === 0) resolveDrain(); + } + + /** Native Promise.all may reject early; the lifecycle fence waits for every map member. */ + function nativeBeforeTransaction(work, nativeThunk) { + stats.rejectedBatches += 1; + stats.nativePassthroughBatches += 1; + stats.pendingBytes -= work.bytes; + work.bytes = 0; + let results; + try { + results = nativeThunk(); + } catch (error) { + release(work); + return Promise.reject(error); + } + Promise.allSettled(results).then(() => release(work)); + return Promise.all(results); + } + + /** Request callbacks keep the real transaction alive without awaiting another task. */ + function transact(transaction, entries) { + return new Promise((resolve, reject) => { + let index = 0; + let writes = 0; + let skipped = 0; + let failure; + const budget = { remaining: limits.maxReadBytes }; + transaction.oncomplete = () => { + stats.committedTransactions += 1; + stats.committedWrites += writes; + stats.skippedWrites += skipped; + resolve(); + }; + transaction.onabort = () => { + stats.abortedTransactions += 1; + reject(failure || transaction.error || new DOMException('Chart mirror batch aborted', 'AbortError')); + }; + transaction.onerror = event => { failure = event.target.error; }; + + function guard(action) { + try { + action(); + } catch (error) { + failure = error; + try { + transaction.abort(); + } catch (abortError) { + // A concurrent native abort listener may already own the final event. + if (abortError instanceof DOMException && abortError.name === 'InvalidStateError') return; + throw abortError; + } + } + } + + let store; + function advance() { + if (index === entries.length) return; + const [key, value] = entries[index]; + function next() { index += 1; advance(); } + function put() { + const request = store.put(value === null ? undefined : value, key); + writes += 1; + request.onsuccess = () => guard(next); + } + if (budget.remaining <= 0) { put(); return; } + const read = store.get(key); + read.onsuccess = () => guard(() => { + let current; + try { + current = snapshotJson(read.result === undefined ? null : read.result, budget); + } catch { + // Unsupported old values and exhausted comparison budgets only disable + // deduplication; they must not discard this otherwise valid mirror. + budget.remaining = 0; + put(); + return; + } + function writeIfChanged(present) { + if (present && equalJson(current, value)) { skipped += 1; next(); } + else put(); + } + if (read.result === undefined && value === null) { + const exists = store.getKey(key); + exists.onsuccess = () => guard(() => writeIfChanged(exists.result !== undefined)); + } else writeIfChanged(read.result !== undefined); + }); + } + guard(() => { store = transaction.objectStore('keyvaluepairs'); advance(); }); + }); + } + + async function write(target, entries, work, nativeThunk) { + let transaction; + try { + await target.ready(); + assert(target.driver() === 'asyncStorage', 'Chart mirror requires the native IndexedDB driver'); + const info = target._dbInfo; + assert((info.name === 'chart_futures' || info.name === 'chart_delivery') && info.storeName === 'keyvaluepairs', + 'Unexpected chart mirror database configuration'); + transaction = info.db.transaction('keyvaluepairs', 'readwrite'); + } catch { + return nativeBeforeTransaction(work, nativeThunk); + } + stats.acceptedBatches += 1; + stats.transactions += 1; + try { + await transact(transaction, entries); + } catch (error) { + stats.failedBatches += 1; + stop(); + throw error; + } finally { + release(work); + } + } + + function dispatch(target, entries, nativeThunk) { + assert(typeof nativeThunk === 'function', 'Chart mirror dispatch requires its original native expression'); + if (phase === 'stopped') return nativeThunk(); + if (phase === 'draining') return [stopping.then(() => Promise.all(nativeThunk()))]; + + // Register before inspecting caller data: a Proxy can synchronously call stop. + const work = { bytes: 0 }; + stats.pendingBatches += 1; + let copied; + try { + assert(Array.isArray(entries), 'Chart mirror entries must be an array'); + if (entries.length === 0) { release(work); return []; } + stats.peakPendingBatches = Math.max(stats.peakPendingBatches, stats.pendingBatches); + assert(entries.length <= limits.maxEntries, 'Chart mirror entry limit exceeded'); + assert(stats.pendingBatches <= limits.maxPendingBatches, 'Chart mirror pending batch limit exceeded'); + const budget = { remaining: limits.maxBatchBytes }; + const seen = new Set(); + const keys = new Set(); + copied = entries.map(entry => { + assert(Array.isArray(entry) && entry.length === 2, 'Chart mirror entry must be a key/value pair'); + const [key, value] = entry; + assert(typeof key === 'string' && (key.startsWith('#TV_SYMBOL-') || key.startsWith('myTradingView')), + 'Chart mirror received a non-chart key'); + assert(!keys.has(key), 'Chart mirror keys must be unique'); + keys.add(key); + budget.remaining -= byteLength(key) + 3; + return [key, snapshotJson(value, budget, seen)]; + }); + const bytes = byteLength(copied); + assert(bytes <= limits.maxBatchBytes, 'Chart mirror batch byte limit exceeded'); + assert(stats.pendingBytes + bytes <= limits.maxPendingBytes, 'Chart mirror pending byte limit exceeded'); + work.bytes = bytes; + stats.pendingBytes += bytes; + stats.peakPendingBytes = Math.max(stats.peakPendingBytes, stats.pendingBytes); + } catch { + return [nativeBeforeTransaction(work, nativeThunk)]; + } + return [write(target, copied, work, nativeThunk)]; + } + + return Object.freeze({ dispatch, getStats: () => ({ ...stats }), getPhase: () => phase, stop }); +} diff --git a/src/binance-orderbook-trade/chart-storage/runtime.js b/src/binance-orderbook-trade/chart-storage/runtime.js new file mode 100644 index 0000000..7e73ed9 --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/runtime.js @@ -0,0 +1,87 @@ +import { observeChartStorageBootstrap } from './bootstrap.js'; +import { replaceChartMirrorFactory } from './mirror-module.js'; +import { createChartMirrorWriter } from './mirror-writer.js'; +import { isChartStoragePage } from './scope.js'; + +const CAPTURE_DEADLINE_MS = 30_000; + +/** + * The sole interception point is the pinned mirror destination expression. + * Unsupported startup retires the hook; it never executes modules to find them. + * A retained callback drains accepted writes before resuming the native map. + */ +export function startChartStorageOptimizer() { + if (!isChartStoragePage()) throw new Error('Chart storage requires a top-level Binance trading page'); + const writer = createChartMirrorWriter(); + const state = { status: 'waiting', reason: null, attempts: 0, matches: 0, executions: 0 }; + let observer; + let timer; + let stopping; + + function snapshot() { + const stats = writer.getStats(); + return { ...state, status: stats.failedBatches > 0 ? 'native_after_failure' : state.status, + phase: writer.getPhase(), writer: stats }; + } + + function stop(reason = 'manual') { + if (stopping) return stopping; + state.status = 'stopping'; + state.reason = reason; + clearTimeout(timer); + self.removeEventListener('pagehide', onPageHide); + if (observer) observer.stop(); + stopping = writer.stop().then(() => { + state.status = 'native'; + return snapshot(); + }); + return stopping; + } + + function onPageHide() { void stop('pagehide'); } + + function dispatch(target, entries, nativeThunk) { + if (!isChartStoragePage()) void stop('scope_changed'); + return writer.dispatch(target, entries, nativeThunk); + } + + try { + observer = observeChartStorageBootstrap({ + replaceMirrorFactory(original) { + if (!isChartStoragePage()) { + void stop('scope_changed'); + return original; + } + state.attempts += 1; + let replacement; + try { + replacement = replaceChartMirrorFactory(original, dispatch); + } catch { + // Source drift is an expected release boundary; leave registration native. + void stop('source_mismatch'); + return original; + } + state.matches += 1; + return function (...args) { + state.executions += 1; + return Reflect.apply(replacement, this, args); + }; + }, + onCapture() { + clearTimeout(timer); + state.status = 'active'; + }, + }); + } catch { + // A late extension cannot safely replace the runtime's cached module. + void stop('bootstrap_unavailable'); + return Object.freeze({ snapshot, stop }); + } + + observer.captured.catch(() => { + if (!stopping) void stop('capture_failed'); + }); + self.addEventListener('pagehide', onPageHide, { once: true }); + timer = setTimeout(() => { void stop('capture_deadline'); }, CAPTURE_DEADLINE_MS); + return Object.freeze({ snapshot, stop }); +} diff --git a/src/binance-orderbook-trade/chart-storage/scope.js b/src/binance-orderbook-trade/chart-storage/scope.js new file mode 100644 index 0000000..6c507ea --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/scope.js @@ -0,0 +1,7 @@ +import { isFuturesTradingPathname } from '../../shared/binance-futures-route.js'; + +/** Storage admission follows the same concrete contract routes as the trading tools. */ +export function isChartStoragePage() { + return self === top && location.origin === 'https://www.binance.com' + && isFuturesTradingPathname(location.pathname); +} diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index a9751ea..74ca1ea 100644 --- a/src/binance-orderbook-trade/index.user.js +++ b/src/binance-orderbook-trade/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.217 +// @version 2.7.218 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -13,6 +13,7 @@ // @updateURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-orderbook-trade.user.js // @downloadURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-orderbook-trade.user.js // @run-at document-start +// @sandbox raw // @grant none // ==/UserScript== @@ -251,10 +252,24 @@ import { getBinanceChartOrdersTarget as getBinanceChartOrdersTargetDom, } from './dom/chart-orders.js'; import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js'; +import { installChartStorageOptimizer } from './chart-storage/install.js'; + +installChartStorageOptimizer(); (function initializeOrderbookTrade() { 'use strict'; + // Page-world injection may precede HTML creation; storage capture must still start immediately. + if (!document.documentElement) { + const rootObserver = new MutationObserver(() => { + if (!document.documentElement) return; + rootObserver.disconnect(); + initializeOrderbookTrade(); + }); + rootObserver.observe(document, { childList: true }); + return; + } + function isFuturesTradingPage() { return isFuturesTradingPathname(location.pathname); } diff --git a/test/fixtures/binance-chart-storage/README.md b/test/fixtures/binance-chart-storage/README.md new file mode 100644 index 0000000..5ab3556 --- /dev/null +++ b/test/fixtures/binance-chart-storage/README.md @@ -0,0 +1,77 @@ +# Binance chart-storage source fixtures + +These public source snapshots are test inputs for an isolated chart-storage +prototype. They are not userscripts, application startup scripts, or production +assets. Do not import them from `src/`, add them to userscript builds, or execute +them on a real Binance page. The repository build targets are explicit `src/` +entrypoints and do not include this fixture directory. + +## Files and provenance + +- `webpack-runtime.js` is the exact, unformatted response body from + . It identifies its + bundler as Rspack 1.7.11 and implements the webpack-compatible + `self.webpackChunkfutures_trade_ui` chunk interface. +- `localforage-module.js` contains the complete original module `43917` method + factory from . Its inline + localForage 1.10.0 / Mozilla / Apache License 2.0 notice is preserved verbatim. + The only surrounding code is a registration-only chunk with the string ID + `codex-chart-storage-fixture` and the original module ID `43917`. +- `manifest.json` records retrieval timestamps, source URLs, SHA-256 checksums, + byte counts, extraction offsets, dependency inspection, and verification. + The full main bundle was a temporary extraction input and is not retained. + +Retrieved on 2026-10-06 UTC. These snapshots establish that dated public source +contract; they do not establish which bundle a later live page has loaded. + +## Extraction contract + +Python's `urllib.request.urlopen` retrieved both public URLs without reading any +credential files. Acorn 8.18.0 parsed the main bundle as an ECMAScript script. The +extractor selected the unique numeric property `43917` in the first chunk push's +module map and copied its original source range. It did not match braces with a +regular expression or regenerate/minify the factory. + +The manifest ranges are zero-based UTF-16 string offsets with exclusive ends, +matching Acorn and JavaScript `String.prototype.slice`. `module_property_sha256` +checks the entire original `43917(_,y,e){...}` property. +`factory_sha256` checks Acorn's function value range, `(_,y,e){...}`, without the +numeric property key. Both hashes use the UTF-8 encoding of those exact source +slices. `fixture_sha256` includes the new registration wrapper. The runtime hash +checks downloaded bytes without transformation. + +The fixture payload has exactly two elements: chunk IDs and the module map. It +contains no third runtime callback and no module require. The string chunk ID was +absent from both downloaded source files, including the runtime's existing chunk +maps. + +## Runtime and dependency boundaries + +Load the runtime and then the module fixture only in a fresh, isolated test +context with an empty chunk queue. Registration adds the factory without +executing it. The real runtime can later supply its require function through a +separate test-owned chunk callback; explicitly requiring `43917` is a distinct +step owned by the test harness. + +The factory imports no external webpack modules. It reads the third parameter's +`g` property four times to obtain the global object for its bundled Browserify +code. Preserve that real runtime contract; do not substitute an empty object for +the webpack require argument. Browserify's modules are contained in the factory; +its loader also contains the original missing-module `require` fallback. + +On an empty queue, the runtime installs module/chunk registration, deferred +execution, and JS/CSS loading helpers. It does not start an application or load a +chunk. It does process any preexisting queue, including runtime callbacks, so +these files must not be injected into an existing page. The loader helpers retain +their upstream network capabilities; isolation and network blocking belong to +the test harness. + +Acorn scope inspection found exactly four references to the webpack parameter, +all reading `.g`, and no external webpack require calls. A bounded Node VM check +loaded the unmodified runtime, captured its real require in a test-owned empty +chunk, and registered the fixture. It observed only module ID `43917`, zero +factory invocations, and zero DOM, storage, network, or timer accesses. The check +used failing accessors for those boundaries and a counting module-registration +proxy. No localForage factory, browser, real page, or database was executed during +fixture acquisition. Browser behavior and storage migration must be verified by +the separate offline harness. diff --git a/test/fixtures/binance-chart-storage/host-persistence-extract.js b/test/fixtures/binance-chart-storage/host-persistence-extract.js new file mode 100644 index 0000000..bd6bfdb --- /dev/null +++ b/test/fixtures/binance-chart-storage/host-persistence-extract.js @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile, readdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { parse } from 'acorn'; +import { analyze } from 'eslint-scope'; + +const roots = process.argv.slice(2); +assert(roots.length > 0, 'Supply directories containing the public Binance source chunks'); +const modules = new Map(); +const sources = new Map(); +const sha = value => createHash('sha256').update(value).digest('hex'); + +function visit(node, action) { + if (!node || typeof node !== 'object') return; + action(node); + for (const value of Object.values(node)) { + if (Array.isArray(value)) value.forEach(child => visit(child, action)); + else if (value?.type) visit(value, action); + } +} + +for (const root of roots) { + for (const file of (await readdir(root)).filter(name => name.endsWith('.js')).sort()) { + const source = await readFile(path.join(root, file), 'utf8'); + const ast = parse(source, { ecmaVersion: 'latest', ranges: true }); + const record = { file, source, sha256: sha(source) }; + sources.set(file, record); + visit(ast, node => { + if (node.type !== 'CallExpression' || node.callee.type !== 'MemberExpression' + || node.callee.property.name !== 'push' + || !source.slice(node.callee.object.start, node.callee.object.end).includes('webpackChunkfutures_trade_ui') + || node.arguments[0]?.type !== 'ArrayExpression' + || node.arguments[0].elements[1]?.type !== 'ObjectExpression') return; + for (const property of node.arguments[0].elements[1].properties) { + if (property.type === 'Property' && typeof property.key.value === 'number') { + if (!modules.has(property.key.value)) modules.set(property.key.value, { ...record, property }); + } + } + }); + } +} + +function freeNames(source) { + const ast = parse(source, { ecmaVersion: 'latest', ranges: true }); + return new Set(analyze(ast, { ecmaVersion: 2024, sourceType: 'script' }) + .globalScope.through.map(reference => reference.identifier.name)); +} + +/** Select complete original declarations, recursively retaining their lexical dependencies. */ +function declarations(moduleId, names) { + const module = modules.get(moduleId); + assert(module, `Missing source module ${moduleId}`); + const available = new Map(); + for (const statement of module.property.value.body.body) { + if (statement.type === 'VariableDeclaration') { + for (const declaration of statement.declarations) { + if (declaration.id.type === 'Identifier') available.set(declaration.id.name, { node: declaration, kind: statement.kind }); + } + } else if (statement.type === 'FunctionDeclaration') { + available.set(statement.id.name, { node: statement, kind: null }); + } + } + const selected = new Map(); + function add(name) { + if (selected.has(name)) return; + const declaration = available.get(name); + assert(declaration, `Missing declaration ${moduleId}:${name}`); + selected.set(name, declaration); + const raw = module.source.slice(declaration.node.start, declaration.node.end); + const wrapped = declaration.kind ? `${declaration.kind} ${raw};` : raw; + for (const dependency of freeNames(wrapped)) if (available.has(dependency)) add(dependency); + } + names.forEach(add); + return [...selected].sort((first, second) => first[1].node.start - second[1].node.start) + .map(([name, declaration]) => ({ ...module, ...declaration, name, moduleId })); +} + +function nestedDeclarator(moduleId, name, predicate) { + const module = modules.get(moduleId); + const candidates = []; + visit(module.property.value.body, node => { + if (node.type === 'VariableDeclarator' && node.id.name === name && predicate(node)) candidates.push(node); + }); + assert.equal(candidates.length, 1, `Expected exactly one ${moduleId}:${name}`); + return { ...module, node: candidates[0], moduleId }; +} + +const tv = declarations(76535, ['kt', 'wt', 'xt', 'zt', 'Wt']); +const basic = declarations(15426, ['Wo', 'Dc']); +const saveCallback = nestedDeclarator(76535, 'Tt', node => node.init.arguments?.[0]?.type === 'ArrowFunctionExpression'); +saveCallback.node = saveCallback.node.init.arguments[0]; + +const mirrorModule = [...modules].find(([, module]) => module.file === 'TradingView.99bc5074.js' + && module.source.slice(module.property.start, module.property.end).includes('Tn=[Qe.ZE,Qe.F3]')); +assert(mirrorModule, 'Missing original TradingView mirror module'); +const mirrorCallback = nestedDeclarator(mirrorModule[0], 'z', node => { + const raw = mirrorModule[1].source.slice(node.start, node.end); + return raw.includes('De.keys()') && raw.includes('Sr.setItem(Ot,At)'); +}); +mirrorCallback.node = mirrorCallback.node.init.arguments[0]; +const mirrorNames = [...freeNames(`const callback=${mirrorCallback.source.slice(mirrorCallback.node.start, mirrorCallback.node.end)};`)] + .filter(name => !['a', 'Cr', '_r', 'Promise', 'console'].includes(name)); +const mirror = declarations(mirrorModule[0], mirrorNames); + +const dependencies = new Set(); +function requiredModules(node, parameter) { + const found = new Set(); + visit(node, child => { + if (child.type === 'CallExpression' && child.callee.type === 'Identifier' + && child.callee.name === parameter && child.arguments[0]?.type === 'Literal' + && typeof child.arguments[0].value === 'number') found.add(child.arguments[0].value); + }); + return found; +} +function addModule(id) { + if (id === 43917 || dependencies.has(id)) return; + const module = modules.get(id); + assert(module, `Missing original dependency module ${id}`); + dependencies.add(id); + for (const child of requiredModules(module.property.value.body, module.property.value.params[2]?.name)) addModule(child); +} +for (const declaration of [...tv, ...basic, ...mirror]) { + for (const id of requiredModules(declaration.node, declaration.property.value.params[2].name)) addModule(id); +} + +let fixture = '/* Exact public host persistence fragments; see host-persistence-manifest.json. */\n' + + '(self.webpackChunkfutures_trade_ui=self.webpackChunkfutures_trade_ui||[]).push([["codex-chart-host-persistence-fixture"],{\n'; +const fragments = []; +function appendOriginal(record, node, label) { + const raw = record.source.slice(node.start, node.end); + const start = fixture.length; + fixture += raw; + fragments.push({ + label, source_file: record.file, source_utf16_range: [node.start, node.end], + fixture_utf16_range: [start, fixture.length], sha256: sha(raw), + }); +} +for (const id of [...dependencies].sort((first, second) => first - second)) { + const module = modules.get(id); + appendOriginal(module, module.property, `module:${id}`); + fixture += ',\n'; +} +function appendDeclarations(items) { + for (const item of items) { + if (item.kind) fixture += `${item.kind} `; + appendOriginal(item, item.node, `declaration:${item.moduleId}:${item.name}`); + fixture += item.kind ? ';\n' : '\n'; + } +} +fixture += '"codex-chart-host-tv"(module,exports,r){\n'; +appendDeclarations(tv); +fixture += 'r.d(exports,{save:()=>xt,load:()=>zt,saveCustomSettings:()=>Wt,createSaveCallback:()=>createSaveCallback});\n' + + 'function createSaveCallback(storage,key,widget,onSave){const et={current:storage},st={current:key},p={current:widget},t={initialConfig:{onSave}};return '; +appendOriginal(saveCallback, saveCallback.node, 'callback:76535:Tt'); +fixture += ';}\n},\n"codex-chart-host-basic"(module,exports,a){\n'; +appendDeclarations(basic); +fixture += 'a.d(exports,{create:()=>Dc});\n},\n"codex-chart-host-mirror"(module,exports,e){\n'; +appendDeclarations(mirror); +fixture += 'e.d(exports,{create:()=>createMirror});\n' + + 'function createMirror(namespace){const a=namespace,Cr={current:false},_r={current:true};return '; +appendOriginal(mirrorCallback, mirrorCallback.node, `callback:${mirrorModule[0]}:z`); +fixture += ';}\n}}]);\n'; + +const usedFiles = [...new Set(fragments.map(fragment => fragment.source_file))].sort(); +const manifest = { + description: 'Exact source fragments from the public Binance chart persistence implementation, with original dependency factories.', + generated_at: new Date().toISOString(), + offset_contract: 'Zero-based UTF-16 String.slice offsets; exclusive ends; SHA-256 uses UTF-8 bytes.', + extraction: { parser: 'acorn', declarations: 'Lexical dependency closure; exact original declarator/function slices.', dependency_module_count: dependencies.size }, + wrapper_contract: { + runtime: 'Existing captured Rspack runtime; localForage module 43917 comes from the existing fixture.', + tv: 'Only original persistence helpers and save callback; storage/key/widget/onSave references are supplied by the test.', + basic: 'Original Wo/Dc and their helpers; React subscriptions, debounce and Ec JSON serialization are not executed.', + mirror: 'Original async callback and original SWC/lodash dependencies; namespace is supplied, Cr.current=false and _r.current=true select the native enabled branch.', + exclusions: 'No chart rendering, exchange requests, login, production records, or real-page adapter installation.', + }, + sources: usedFiles.map(file => ({ + file, source_url: `https://bin.bnbstatic.com/static/${file.startsWith('main.') ? '' : 'chunks/'}${file}`, + sha256: sources.get(file).sha256, bytes: Buffer.byteLength(sources.get(file).source), + })), + fixture: { file: 'host-persistence.js', sha256: sha(fixture), bytes: Buffer.byteLength(fixture) }, + fragments, +}; +await writeFile(new URL('host-persistence.js', import.meta.url), fixture); +await writeFile(new URL('host-persistence-manifest.json', import.meta.url), `${JSON.stringify(manifest, null, 2)}\n`); +console.log(JSON.stringify({ fixtureBytes: Buffer.byteLength(fixture), dependencyModules: dependencies.size, fragments: fragments.length, sourceFiles: usedFiles })); diff --git a/test/fixtures/binance-chart-storage/host-persistence-manifest.json b/test/fixtures/binance-chart-storage/host-persistence-manifest.json new file mode 100644 index 0000000..cba3385 --- /dev/null +++ b/test/fixtures/binance-chart-storage/host-persistence-manifest.json @@ -0,0 +1,3617 @@ +{ + "description": "Exact source fragments from the public Binance chart persistence implementation, with original dependency factories.", + "generated_at": "2026-10-06T13:39:08.155Z", + "offset_contract": "Zero-based UTF-16 String.slice offsets; exclusive ends; SHA-256 uses UTF-8 bytes.", + "extraction": { + "parser": "acorn", + "declarations": "Lexical dependency closure; exact original declarator/function slices.", + "dependency_module_count": 203 + }, + "wrapper_contract": { + "runtime": "Existing captured Rspack runtime; localForage module 43917 comes from the existing fixture.", + "tv": "Only original persistence helpers and save callback; storage/key/widget/onSave references are supplied by the test.", + "basic": "Original Wo/Dc and their helpers; React subscriptions, debounce and Ec JSON serialization are not executed.", + "mirror": "Original async callback and original SWC/lodash dependencies; namespace is supplied, Cr.current=false and _r.current=true select the native enabled branch.", + "exclusions": "No chart rendering, exchange requests, login, production records, or real-page adapter installation." + }, + "sources": [ + { + "file": "16873.442d9b04.js", + "source_url": "https://bin.bnbstatic.com/static/chunks/16873.442d9b04.js", + "sha256": "df41025e362f574e874621bd4cd03cae4fa0f21a6fd8634d2358c29cc045932d", + "bytes": 25068 + }, + { + "file": "28928.9aeec892.js", + "source_url": "https://bin.bnbstatic.com/static/chunks/28928.9aeec892.js", + "sha256": "0e8870ba5befe8aba1652a7a773264546fd82a5739f8a9da501680c75f6ada18", + "bytes": 614617 + }, + { + "file": "36648.dd8ae7dc.js", + "source_url": "https://bin.bnbstatic.com/static/chunks/36648.dd8ae7dc.js", + "sha256": "8191b50fb47212425d03146834207645c0e89b8ce623e23a71477194ee9a60ff", + "bytes": 24108 + }, + { + "file": "40784.08580559.js", + "source_url": "https://bin.bnbstatic.com/static/chunks/40784.08580559.js", + "sha256": "437eeea6e220f2b25abb32e7a2d3af589be5b95815d0ba5dbb7060e0a57cdfa8", + "bytes": 37118 + }, + { + "file": "56679.46a7195c.js", + "source_url": "https://bin.bnbstatic.com/static/chunks/56679.46a7195c.js", + "sha256": "9b7f8073ff13501279886af4fc929fd835e587146a51b03441851ed4b208209d", + "bytes": 312969 + }, + { + "file": "TradingView.99bc5074.js", + "source_url": "https://bin.bnbstatic.com/static/chunks/TradingView.99bc5074.js", + "sha256": "b6a2cddd017fb4517abb4ea458e19905c59f955a3f99efa18beb1e82f3938c1c", + "bytes": 52058 + }, + { + "file": "main.37a180b9.js", + "source_url": "https://bin.bnbstatic.com/static/main.37a180b9.js", + "sha256": "97bc05dcb6bf9e0803a00027e51e26a2d190c4c6d0a2b5151cfa44dfb846f451", + "bytes": 751358 + } + ], + "fixture": { + "file": "host-persistence.js", + "sha256": "abacc9588761a054e63b234bf8d3a591b9f1cd195461848811f83c072c010020", + "bytes": 50082 + }, + "fragments": [ + { + "label": "module:195", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 588121, + 588325 + ], + "fixture_utf16_range": [ + 206, + 410 + ], + "sha256": "076356d5ceb471a777fe40a21efb1d97cb83ca9a90c282480adb583dc8a798ba" + }, + { + "label": "module:1386", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 581333, + 581610 + ], + "fixture_utf16_range": [ + 412, + 689 + ], + "sha256": "044ac771b6a4cc51ccc79cef867ae9174b1f48477686eb246d704bf93a52d651" + }, + { + "label": "module:1877", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 602788, + 602891 + ], + "fixture_utf16_range": [ + 691, + 794 + ], + "sha256": "7a1d9f60d4ee88867155bf1e3ffd70946e06ca89cb61d15fd681c1e51ec70043" + }, + { + "label": "module:2306", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601866, + 602113 + ], + "fixture_utf16_range": [ + 796, + 1043 + ], + "sha256": "3e8dad8683615c659b227002344b3595c6b1a52aa9df81ca48d2e2960515286a" + }, + { + "label": "module:2478", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 152945, + 153055 + ], + "fixture_utf16_range": [ + 1045, + 1155 + ], + "sha256": "1b2e839dbc520323294a6f15c90ecfcd2c45fa9a13dd670cf375b77bfdb06e5b" + }, + { + "label": "module:3283", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 605925, + 606144 + ], + "fixture_utf16_range": [ + 1157, + 1376 + ], + "sha256": "e88a488c04e793705eb1107c28bc4036dde73bbc21134f437ce67538d768683c" + }, + { + "label": "module:3316", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 603116, + 603311 + ], + "fixture_utf16_range": [ + 1378, + 1573 + ], + "sha256": "39bc82d78923ca68ef8182399e8e511f825b647f10b8232c974e5c26907cf53b" + }, + { + "label": "module:3334", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 587936, + 588120 + ], + "fixture_utf16_range": [ + 1575, + 1759 + ], + "sha256": "ad54b48293376bdbfea2ec41c971f8e72cbcd34910b63e0462db9e028180de21" + }, + { + "label": "module:3422", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 583928, + 584103 + ], + "fixture_utf16_range": [ + 1761, + 1936 + ], + "sha256": "54698aaffe8e378dfd57928298979484f83244b6e3c7ec3184ef43a8a5bc63fe" + }, + { + "label": "module:3766", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 602203, + 602274 + ], + "fixture_utf16_range": [ + 1938, + 2009 + ], + "sha256": "6365268be7722795a85c6d18a02aadebfb99e771f38a0d0492edd19fbbea0bad" + }, + { + "label": "module:5681", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600889, + 600994 + ], + "fixture_utf16_range": [ + 2011, + 2116 + ], + "sha256": "0fa6390d2a0bd82281474aafa21be303f447e6a4ff7334b9626d42d90a8ddc24" + }, + { + "label": "module:7144", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600416, + 600524 + ], + "fixture_utf16_range": [ + 2118, + 2226 + ], + "sha256": "bc17621fe2ee7f1c85fb0ad66bcc2cb1b6ace99a18be6bf3094225944a2b23f3" + }, + { + "label": "module:7452", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600525, + 600607 + ], + "fixture_utf16_range": [ + 2228, + 2310 + ], + "sha256": "6c0069e1e25ed659eabee671b7976def0900fec31c13946f19c82dfb36518c78" + }, + { + "label": "module:7462", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 603557, + 603619 + ], + "fixture_utf16_range": [ + 2312, + 2374 + ], + "sha256": "973d646d7a2f9c1fdd5410608000a554bd2b35f89232501d102c742548e4a7c9" + }, + { + "label": "module:7980", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 51659, + 51716 + ], + "fixture_utf16_range": [ + 2376, + 2433 + ], + "sha256": "285e2cd28d53b4c0af6ea15808f4b7dd9647724ac3ed4c42a8b73ab4f9d2f908" + }, + { + "label": "module:8006", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 602892, + 602954 + ], + "fixture_utf16_range": [ + 2435, + 2497 + ], + "sha256": "d9b0457c8f0d4daf6c6f5ca498bda8559defaec5ed25c78b52764696053f8f92" + }, + { + "label": "module:8666", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 608024, + 608115 + ], + "fixture_utf16_range": [ + 2499, + 2590 + ], + "sha256": "02898fd5d4ba2b5c890e0ce990d965dfbc127f6316f709154bb809afe0e52f4d" + }, + { + "label": "module:8870", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 596653, + 596890 + ], + "fixture_utf16_range": [ + 2592, + 2829 + ], + "sha256": "5e2f97c5451b90e8679da15b25dcd897c6d13bbcce0ff2826fac725c2be453a7" + }, + { + "label": "module:10010", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 724875, + 725025 + ], + "fixture_utf16_range": [ + 2831, + 2981 + ], + "sha256": "a125b4a241318d59e17a0ed05f5c16d600d5cf8395cbaabae8ab28e8c992ad10" + }, + { + "label": "module:10358", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 583053, + 583470 + ], + "fixture_utf16_range": [ + 2983, + 3400 + ], + "sha256": "43c97e26758d54132c7afd6343e46d51f5783121ff9a5bffb9eacd0df3210607" + }, + { + "label": "module:11112", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 598187, + 598399 + ], + "fixture_utf16_range": [ + 3402, + 3614 + ], + "sha256": "ecc5189072302807e35f5303102c58426681cc86db50ec5b8e7d3a7f99465598" + }, + { + "label": "module:11129", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 583587, + 583687 + ], + "fixture_utf16_range": [ + 3616, + 3716 + ], + "sha256": "b0b1a0bdd1c551f7c1e49c13e4186d5de62115a7ad2f7d9d415330ffe587a194" + }, + { + "label": "module:12343", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 588870, + 589097 + ], + "fixture_utf16_range": [ + 3718, + 3945 + ], + "sha256": "b7a9f772aa3003f472337e5347a26c0906387017d4aab2b6e4a6785ca8bb4000" + }, + { + "label": "module:12393", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600157, + 600220 + ], + "fixture_utf16_range": [ + 3947, + 4010 + ], + "sha256": "2856af6a5b28b6722fd4ef2dde16d73d85a46470614218e0f5b690195b994fea" + }, + { + "label": "module:12762", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 590317, + 590386 + ], + "fixture_utf16_range": [ + 4012, + 4081 + ], + "sha256": "22f9a148b3db7d548577c772c572eecddfbfb2a3594983130e02090f8abf6f6c" + }, + { + "label": "module:13964", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600608, + 600747 + ], + "fixture_utf16_range": [ + 4083, + 4222 + ], + "sha256": "ae950db8670c1d8c1f7146271a12dd924357bbede67488090348fbd44ef64b00" + }, + { + "label": "module:15630", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 154859, + 155318 + ], + "fixture_utf16_range": [ + 4224, + 4683 + ], + "sha256": "cca361c7312b3cdcf5ed0a4fb2181e27d05efb6e63e4af5df9ce5a1d18bc3d99" + }, + { + "label": "module:17099", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 591798, + 592062 + ], + "fixture_utf16_range": [ + 4685, + 4949 + ], + "sha256": "ccd88beb86ffc5ba7c62ec61095c9736b439586a67cacff65d99708619bde6e9" + }, + { + "label": "module:18069", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 12802, + 12930 + ], + "fixture_utf16_range": [ + 4951, + 5079 + ], + "sha256": "679a51e58ebe23f63c53d2318e26068b2d2c3b98a551a578e3d014507702fb51" + }, + { + "label": "module:19270", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 582651, + 582719 + ], + "fixture_utf16_range": [ + 5081, + 5149 + ], + "sha256": "b7b3525dd7e04e3cb91053cad71c1abb6d638d1329175498f953e1163d73aa4e" + }, + { + "label": "module:19770", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 581611, + 581675 + ], + "fixture_utf16_range": [ + 5151, + 5215 + ], + "sha256": "62b118f55bc2fe8412e84f29ecced21b98b77280ca833835e84624e8825ca6a7" + }, + { + "label": "module:19953", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 160325, + 160430 + ], + "fixture_utf16_range": [ + 5217, + 5322 + ], + "sha256": "863b9a8662f226e7e712eb70dbc6b55dc7b8c03a0913fb66f22cb6d0a6794fdb" + }, + { + "label": "module:20155", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 597653, + 597704 + ], + "fixture_utf16_range": [ + 5324, + 5375 + ], + "sha256": "b0a1a6e840564e933218960780b8e1f46cb26618f0b69c6d7a1b7ae753d62aa3" + }, + { + "label": "module:20714", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 598852, + 598993 + ], + "fixture_utf16_range": [ + 5377, + 5518 + ], + "sha256": "f88aba7ba290fdf970df49f973430b4a62c04d737a8c61d08c0576fe95df88dc" + }, + { + "label": "module:21779", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 603397, + 603492 + ], + "fixture_utf16_range": [ + 5520, + 5615 + ], + "sha256": "4fd4f0043856c796af4a9a806420d02d116ee09561fadb00bd4d0ed2172104f4" + }, + { + "label": "module:22053", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 605880, + 605924 + ], + "fixture_utf16_range": [ + 5617, + 5661 + ], + "sha256": "09ace35dce7702409403ecabf4d55d59b2aa4420bfb9f75880b630682e17f7ae" + }, + { + "label": "module:23212", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 582090, + 582293 + ], + "fixture_utf16_range": [ + 5663, + 5866 + ], + "sha256": "5560039aabf6e603485b9a45561a0064980c85c0c2e4966dc1d5a848768eaf35" + }, + { + "label": "module:23305", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 597989, + 598081 + ], + "fixture_utf16_range": [ + 5868, + 5960 + ], + "sha256": "76b7d8b3dcea102a57e2442cf95c607c181241a1e29829cd7d81562e0ac432be" + }, + { + "label": "module:23904", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601392, + 601506 + ], + "fixture_utf16_range": [ + 5962, + 6076 + ], + "sha256": "0094d51330b28ac60bf2a8bdabbcb0cf0a7e6852c04613447936cf26e171d958" + }, + { + "label": "module:23945", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 590476, + 590578 + ], + "fixture_utf16_range": [ + 6078, + 6180 + ], + "sha256": "17db0c12313a7eb39d2e29f6c00586620723ec08eaed5ee1d4fdc7311b833fc2" + }, + { + "label": "module:24367", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 596336, + 596476 + ], + "fixture_utf16_range": [ + 6182, + 6322 + ], + "sha256": "e2499a5dc51b5669c5bd82d31f5b5e29bb8688c681c0414333ef756dea3d34aa" + }, + { + "label": "module:24733", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 13018, + 13104 + ], + "fixture_utf16_range": [ + 6324, + 6410 + ], + "sha256": "43431e20283e5305fd970706759a00edcc3f8b91f3bbd761198c9bc4a6031080" + }, + { + "label": "module:24810", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 583800, + 583927 + ], + "fixture_utf16_range": [ + 6412, + 6539 + ], + "sha256": "25eb9dd97577c853a4f82dcd567b942bbc3b1d0f507381db5016651232c7de55" + }, + { + "label": "module:25098", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 581052, + 581332 + ], + "fixture_utf16_range": [ + 6541, + 6821 + ], + "sha256": "c82e176a400a0cd8293a310171392887096d7eaa6e33f5fd2543cb2114c1ad80" + }, + { + "label": "module:25276", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 598400, + 598547 + ], + "fixture_utf16_range": [ + 6823, + 6970 + ], + "sha256": "35c96ee93562c76dca5c8c5c7c4fbe2bff5fe7f11f20e8ef8c9c2ffca31162ee" + }, + { + "label": "module:25288", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 608213, + 608314 + ], + "fixture_utf16_range": [ + 6972, + 7073 + ], + "sha256": "d1852dee788b486e5e7bc03154efe7b28799b90ce1e4897722c8957da577b9ec" + }, + { + "label": "module:26137", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 590838, + 590925 + ], + "fixture_utf16_range": [ + 7075, + 7162 + ], + "sha256": "a13e11fb6aac6b87eaee0a5124954da4c12b4aac006abd740e66b5e2d36a4969" + }, + { + "label": "module:26885", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 589365, + 589911 + ], + "fixture_utf16_range": [ + 7164, + 7710 + ], + "sha256": "3800f3c951c32987b6c859f67360fd3d3db923c9d86b591034cf7c6050969437" + }, + { + "label": "module:27276", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 605787, + 605879 + ], + "fixture_utf16_range": [ + 7712, + 7804 + ], + "sha256": "7eb44499ed19ee5204938b1e2da230197ebc37e37ec48b45210bef64fec6582f" + }, + { + "label": "module:28486", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 597109, + 597652 + ], + "fixture_utf16_range": [ + 7806, + 8349 + ], + "sha256": "7e12564ab2ddc53b343264b0169bce39dc4d39cfa6b51ac6536cd68f3f43f7fb" + }, + { + "label": "module:29005", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 602114, + 602202 + ], + "fixture_utf16_range": [ + 8351, + 8439 + ], + "sha256": "49e58fbd250cca015d07b74faeea15bb4cbf108dcff9a5277b4db3231b704493" + }, + { + "label": "module:30125", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 10532, + 10621 + ], + "fixture_utf16_range": [ + 8441, + 8530 + ], + "sha256": "5267718742dafe4ef8897ad7aefe55afa4fb5835e5f04e372e48387a54afe4c0" + }, + { + "label": "module:31182", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 595335, + 596028 + ], + "fixture_utf16_range": [ + 8532, + 9225 + ], + "sha256": "57ecfb09e70d275a697dbd72dbd030f2ecceaf7f1428a81f8a83e67f0f41d47a" + }, + { + "label": "module:31623", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 582595, + 582650 + ], + "fixture_utf16_range": [ + 9227, + 9282 + ], + "sha256": "d23626b921c3851b077fe782f133c2442e37e72490b4924b41f2d38e723a0eea" + }, + { + "label": "module:33103", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 580982, + 581051 + ], + "fixture_utf16_range": [ + 9284, + 9353 + ], + "sha256": "06e2420c76d1d48767dd3791966ba470ecf4ede3e30f39d65c62f93489d916b2" + }, + { + "label": "module:33934", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 593890, + 594435 + ], + "fixture_utf16_range": [ + 9355, + 9900 + ], + "sha256": "9e333b6302905e8d364b659a17f151fe28938ad6f0e334179bdcf10957eb33e0" + }, + { + "label": "module:34162", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 603493, + 603556 + ], + "fixture_utf16_range": [ + 9902, + 9965 + ], + "sha256": "5e3aab576a027f8149de89f5aa55c8f012a04073488b22a14d14ea0663035801" + }, + { + "label": "module:34772", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 609193, + 609236 + ], + "fixture_utf16_range": [ + 9967, + 10010 + ], + "sha256": "74a4a3a55a577ea63b0298f4cc68af56e523d4132bea354c8d5ae33123dc82ed" + }, + { + "label": "module:34789", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 14806, + 14888 + ], + "fixture_utf16_range": [ + 10012, + 10094 + ], + "sha256": "32c44883fc73a5acba8b7fe7fe0aec70983715e598048ea8d52d995c3c8ba9aa" + }, + { + "label": "module:35636", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 584819, + 584865 + ], + "fixture_utf16_range": [ + 10096, + 10142 + ], + "sha256": "8aa2bc7f50436919d1cd13fe4476dd408a8b99a14e99511b3155dd0759bceb66" + }, + { + "label": "module:36208", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 156551, + 156679 + ], + "fixture_utf16_range": [ + 10144, + 10272 + ], + "sha256": "9179a6b984728afb56607d5e91d1be3fc57e795b37a9e70cbd6a81e84b862ad9" + }, + { + "label": "module:36465", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 583688, + 583799 + ], + "fixture_utf16_range": [ + 10274, + 10385 + ], + "sha256": "649a6dae9650211c1b5fbb9bc74b984ad69c1591b5ad532898dfd5922c199976" + }, + { + "label": "module:36526", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 588699, + 588869 + ], + "fixture_utf16_range": [ + 10387, + 10557 + ], + "sha256": "6e802bcb7fdadb8f3572a196bc16d7d888138b9b736aef290a0e2f1de5b8cbe7" + }, + { + "label": "module:36529", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 606187, + 606287 + ], + "fixture_utf16_range": [ + 10559, + 10659 + ], + "sha256": "96d9de5d6c4e7384c4a2428b796588c1efa7a3218cc8efab21c2d6fb2e9bee56" + }, + { + "label": "module:37250", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 152550, + 152669 + ], + "fixture_utf16_range": [ + 10661, + 10780 + ], + "sha256": "bec925fb9c03e2835a9061d2335bea548bef7f36cf2565fa1650b47f4043f16c" + }, + { + "label": "module:37681", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 14634, + 14719 + ], + "fixture_utf16_range": [ + 10782, + 10867 + ], + "sha256": "be85b0228c0df4f4ae9298bbc02a4279b9952885ab369af2bed23a3eed59bd61" + }, + { + "label": "module:37930", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 10442, + 10531 + ], + "fixture_utf16_range": [ + 10869, + 10958 + ], + "sha256": "2505dc60f9f3c3ba44c92d0eedab1cd5504fef3dfac125a919f3546dc6976d0c" + }, + { + "label": "module:38330", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 586374, + 586764 + ], + "fixture_utf16_range": [ + 10960, + 11350 + ], + "sha256": "a4c9afe5b6627bd34bb1a67c5bd2683d7460fabbe94f74dc1051e28b4b7a9fa7" + }, + { + "label": "module:38546", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601768, + 601865 + ], + "fixture_utf16_range": [ + 11352, + 11449 + ], + "sha256": "b8f462f3e4b7ffa81cb1211712cfe5d89e0c3f0be6da881e6385e5169af0c12f" + }, + { + "label": "module:39032", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 592822, + 593034 + ], + "fixture_utf16_range": [ + 11451, + 11663 + ], + "sha256": "43d3875f94841a6f891b5dc7269a4f64bd6ebeceed54fea96b8140ea5ca5c85f" + }, + { + "label": "module:39361", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 598082, + 598186 + ], + "fixture_utf16_range": [ + 11665, + 11769 + ], + "sha256": "fa861a78279bfb59c00bff33d6d7d713d5581b3f5e2ec5d31b17cb5d3550a73d" + }, + { + "label": "module:39747", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 584362, + 584559 + ], + "fixture_utf16_range": [ + 11771, + 11968 + ], + "sha256": "fd9087e691aa5e7ca29fd2513d82aaabb1bc731ba0ca1333d7fe4b54bb927700" + }, + { + "label": "module:40091", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 589098, + 589364 + ], + "fixture_utf16_range": [ + 11970, + 12236 + ], + "sha256": "f4ddedfc03f88f1b5b506ce02eef194ae887bf9b1b36222092f2c0b730d9fddc" + }, + { + "label": "module:41580", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 607382, + 607477 + ], + "fixture_utf16_range": [ + 12238, + 12333 + ], + "sha256": "7a0f418052e24cf5980a502dbe6610625014a82a9a9d6ac0ab1c58faefbb2791" + }, + { + "label": "module:41950", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 593035, + 593101 + ], + "fixture_utf16_range": [ + 12335, + 12401 + ], + "sha256": "003a365d6f91f05476d6487de7a433cbfb89b47c8e6fb184f576e0437a43a5d1" + }, + { + "label": "module:42288", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 730272, + 731546 + ], + "fixture_utf16_range": [ + 12403, + 13677 + ], + "sha256": "246f01a4d0f336d062be0988ab5fac44bc1ef5010a2b4b838029ae865ad9bea8" + }, + { + "label": "module:43282", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 608825, + 608937 + ], + "fixture_utf16_range": [ + 13679, + 13791 + ], + "sha256": "676f59e8231caa9da455f32829d6c0f945baeb8ae99bc33bf46b2e43c8d6d049" + }, + { + "label": "module:43316", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 151666, + 151777 + ], + "fixture_utf16_range": [ + 13793, + 13904 + ], + "sha256": "789962ff39c302092f11d8f43ffa4d9ea966269dd1e3d2b20d1647efcfe6e18f" + }, + { + "label": "module:43636", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 585411, + 585479 + ], + "fixture_utf16_range": [ + 13906, + 13974 + ], + "sha256": "ca098e0e2a61bbebab107fd291dc4d152c2d4a1a2857016f0bd182999fc18b68" + }, + { + "label": "module:44700", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 596206, + 596335 + ], + "fixture_utf16_range": [ + 13976, + 14105 + ], + "sha256": "ad2582b788d5e0c2c9ac9a77344476516337db4e0374593d38c3016ff3b928fd" + }, + { + "label": "module:45770", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 593399, + 593603 + ], + "fixture_utf16_range": [ + 14107, + 14311 + ], + "sha256": "4f15d5e29cc67f1720b173dd27fb86fa6d0e4abc40baad6ed0e482bbbcfb10cf" + }, + { + "label": "module:46108", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 14555, + 14633 + ], + "fixture_utf16_range": [ + 14313, + 14391 + ], + "sha256": "169b7da271880c14187b5639a0b6b5d3c49c3987a93ba9cb5603a470e26e4604" + }, + { + "label": "module:46123", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 605678, + 605786 + ], + "fixture_utf16_range": [ + 14393, + 14501 + ], + "sha256": "b8d7c80369fad193a1ba03470405fe286ac7658c09a3b10ab8057b4a0a0c198d" + }, + { + "label": "module:46155", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 159145, + 159287 + ], + "fixture_utf16_range": [ + 14503, + 14645 + ], + "sha256": "04173860436f6b66674ec04cbe264cd1525017826e0eee271d85de2a65aa4e1c" + }, + { + "label": "module:46285", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 605613, + 605677 + ], + "fixture_utf16_range": [ + 14647, + 14711 + ], + "sha256": "33de54c2cb12a8fcdb4f47a9f97dced8414171fc37aa16bfc6507023738b7fa9" + }, + { + "label": "module:47170", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 584866, + 584952 + ], + "fixture_utf16_range": [ + 14713, + 14799 + ], + "sha256": "e121a8fd18aa8f287a4ad409e631375ac63ffc2723557795016081c3994a9e03" + }, + { + "label": "module:47403", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 590579, + 590740 + ], + "fixture_utf16_range": [ + 14801, + 14962 + ], + "sha256": "17bb980be3235b618fbb4db7b6fb6e2d98e978ee30226850481fe5403933c24e" + }, + { + "label": "module:48244", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 585106, + 585210 + ], + "fixture_utf16_range": [ + 14964, + 15068 + ], + "sha256": "74e0732678e94e0c9d501cac1e1596555f412d23eac2d2644dbafc5e9ef1c231" + }, + { + "label": "module:48360", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 607549, + 607896 + ], + "fixture_utf16_range": [ + 15070, + 15417 + ], + "sha256": "7372b285fa22ff4014492363a40a4384b02ad4573b1a4c812a5dd9d9af908fac" + }, + { + "label": "module:49253", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 725282, + 725635 + ], + "fixture_utf16_range": [ + 15419, + 15772 + ], + "sha256": "fd76469b1ff5b4b7efce3c6285c37484ac20f5d62fc56f7981ca89b08e1b7459" + }, + { + "label": "module:49460", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 591298, + 591365 + ], + "fixture_utf16_range": [ + 15774, + 15841 + ], + "sha256": "b8fa1b3c7d9927f70ed9ffa2691983387b3d4f6cd0f0a6e5fd7dcaafb79cec1a" + }, + { + "label": "module:49753", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600748, + 600888 + ], + "fixture_utf16_range": [ + 15843, + 15983 + ], + "sha256": "27f58c02fdbc86dfb43e698b56e7938f3360d9d6b305f328a97ba926453f4bd7" + }, + { + "label": "module:49828", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 602955, + 603057 + ], + "fixture_utf16_range": [ + 15985, + 16087 + ], + "sha256": "73953bbffb627e2fc92aacf145b810220cc770b48e97929544f7fe586815b2cc" + }, + { + "label": "module:50424", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 13728, + 14554 + ], + "fixture_utf16_range": [ + 16089, + 16915 + ], + "sha256": "f5885015cb3d0ca09cda448e53274aca7c07b9e7be8e5bdd1d05ad9b17e62a36" + }, + { + "label": "module:51187", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 607897, + 608023 + ], + "fixture_utf16_range": [ + 16917, + 17043 + ], + "sha256": "2947454024a20506c8f80b490fc48ba46fcf0ed2cf63ce14b22a48ac45977d97" + }, + { + "label": "module:51340", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 582294, + 582542 + ], + "fixture_utf16_range": [ + 17045, + 17293 + ], + "sha256": "54ec3daa5430d76865ae238a4a3dfce32d0b25b773648434452d9e40176cc472" + }, + { + "label": "module:52291", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 590926, + 591191 + ], + "fixture_utf16_range": [ + 17295, + 17560 + ], + "sha256": "bd89faf92252986369abbe8a469f435d5e5227f345e8e51fa1797a55fa098cc1" + }, + { + "label": "module:53142", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 606145, + 606186 + ], + "fixture_utf16_range": [ + 17562, + 17603 + ], + "sha256": "95eb6beb510a123da017b6925f8cd0449c368670a1f39d305023cbc0d815da3b" + }, + { + "label": "module:54732", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601074, + 601152 + ], + "fixture_utf16_range": [ + 17605, + 17683 + ], + "sha256": "3c6d129039fc047ed6ecf8664953199a664a321d73d491ef8ba253b2fc3a9750" + }, + { + "label": "module:56027", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 585480, + 585589 + ], + "fixture_utf16_range": [ + 17685, + 17794 + ], + "sha256": "176d5d500963e2961f23b6074bca044f9075a73c31e0a37b19e604d5b2c8a8ec" + }, + { + "label": "module:57073", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 584219, + 584361 + ], + "fixture_utf16_range": [ + 17796, + 17938 + ], + "sha256": "b265b6f5dfe11ae6fde73ab8fc2d7bf89b0ac5557ebc925eba5421a8eaba210e" + }, + { + "label": "module:57200", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 588326, + 588548 + ], + "fixture_utf16_range": [ + 17940, + 18162 + ], + "sha256": "17b1d87c35dc3194ff68148fac027fc06f889af08190ec5477915685749b6376" + }, + { + "label": "module:57452", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 598548, + 598716 + ], + "fixture_utf16_range": [ + 18164, + 18332 + ], + "sha256": "635d4be50d563a73ad7bd2055fe63b23a7efcb7c9473dde4d0cd45ad5c732b33" + }, + { + "label": "module:57923", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 584953, + 585105 + ], + "fixture_utf16_range": [ + 18334, + 18486 + ], + "sha256": "23137ccd8629224e67761e1a43d9cf85eecf6dba47bc68c2abb80c7168676e1f" + }, + { + "label": "module:58898", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 591673, + 591797 + ], + "fixture_utf16_range": [ + 18488, + 18612 + ], + "sha256": "d35825328e8e63626bd819ed59c0f4b51c2b09ed5bbc9858ba5532a2bb70610d" + }, + { + "label": "module:59068", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601153, + 601282 + ], + "fixture_utf16_range": [ + 18614, + 18743 + ], + "sha256": "42cdd5284ef2804921beac73147401586066a9eff4e401515d1e2746504ccb72" + }, + { + "label": "module:59756", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 151562, + 151665 + ], + "fixture_utf16_range": [ + 18745, + 18848 + ], + "sha256": "46424910254d5e60a0f445187f549c6e0b098cc24582b058045cf2cc91051c70" + }, + { + "label": "module:61875", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 731700, + 732062 + ], + "fixture_utf16_range": [ + 18850, + 19212 + ], + "sha256": "6ad6943dfa4be8af902e88cf95a631115957bc1c273a317ef2ef533f3e3a1ef9" + }, + { + "label": "module:62049", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600221, + 600415 + ], + "fixture_utf16_range": [ + 19214, + 19408 + ], + "sha256": "e5a8d86214c5340c1ad7539643a2082aef4f7cbef7c1f9af72962fdd7d505ce2" + }, + { + "label": "module:64066", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 604318, + 604464 + ], + "fixture_utf16_range": [ + 19410, + 19556 + ], + "sha256": "908917e3e7b6668eeb890c321d849dafbc854c08f350594f56c4b4ea7c038898" + }, + { + "label": "module:64103", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 603312, + 603396 + ], + "fixture_utf16_range": [ + 19558, + 19642 + ], + "sha256": "cd21c2ea1fcf1ebb8b4df1742c91b1fba44670b3454275f2e90d453be05c577a" + }, + { + "label": "module:64207", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 609894, + 609976 + ], + "fixture_utf16_range": [ + 19644, + 19726 + ], + "sha256": "772e31a097ab554bb19d6ef789c67168357c9bdf75a4cb210a32406c8c463625" + }, + { + "label": "module:64512", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 582025, + 582089 + ], + "fixture_utf16_range": [ + 19728, + 19792 + ], + "sha256": "2e25669599e234cc00b72ac92952ac196f56720662e2f76f6ce49a6512bc51d1" + }, + { + "label": "module:64759", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 599774, + 599947 + ], + "fixture_utf16_range": [ + 19794, + 19967 + ], + "sha256": "dd1c0e19dce4179e4cbf33ea4d01d92314879d311879180b2ed997519fcfd8b3" + }, + { + "label": "module:65111", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 583471, + 583586 + ], + "fixture_utf16_range": [ + 19969, + 20084 + ], + "sha256": "428b1db64df12ccb8a7186a5974937561307238c6f20d6c56e033fdff715669e" + }, + { + "label": "module:65187", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 599365, + 599638 + ], + "fixture_utf16_range": [ + 20086, + 20359 + ], + "sha256": "e7069f001ffcbdfd163b88639e7a427dfa9a14b5da6d8e986a10b4bd27379b04" + }, + { + "label": "module:65387", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 607277, + 607381 + ], + "fixture_utf16_range": [ + 20361, + 20465 + ], + "sha256": "99cb92cdf8bc751024275ebde5c15298df013c69f84823ac0cf2dbe6238163bc" + }, + { + "label": "module:65406", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 606288, + 606371 + ], + "fixture_utf16_range": [ + 20467, + 20550 + ], + "sha256": "1759deaef08ef51d497f2e3b25701fefafee2c0d1bc94798e111be3d4a65475e" + }, + { + "label": "module:65568", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 591496, + 591549 + ], + "fixture_utf16_range": [ + 20552, + 20605 + ], + "sha256": "1caedce163f61cddca5f80d1584618085a49f4a8e93e2614402a75f15c650ba0" + }, + { + "label": "module:65650", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 582543, + 582594 + ], + "fixture_utf16_range": [ + 20607, + 20658 + ], + "sha256": "a14be5da5f66bccfc0e2d87991848cb78958e7fd3a7389134e2540abb71a6320" + }, + { + "label": "module:66040", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 604179, + 604317 + ], + "fixture_utf16_range": [ + 20660, + 20798 + ], + "sha256": "6e91ef6afd3020de5f99249609ba71e42ecb95b82c63096b547054b26649ee86" + }, + { + "label": "module:67267", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600082, + 600156 + ], + "fixture_utf16_range": [ + 20800, + 20874 + ], + "sha256": "e4dd6681dd4219a21630e50ac31912643b072ffde0efd07a281080694202ab91" + }, + { + "label": "module:68250", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 581676, + 581955 + ], + "fixture_utf16_range": [ + 20876, + 21155 + ], + "sha256": "18e4b97e9fadb93c8a1bc97cd3bb8ac990fdc73a9cc1fb4080e11d34e27c4fba" + }, + { + "label": "module:69011", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 608315, + 608666 + ], + "fixture_utf16_range": [ + 21157, + 21508 + ], + "sha256": "3c953c91d34722c72bfa35afbbaa39d9ef483bf9369b44a982a882634896d2bc" + }, + { + "label": "module:69334", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 10622, + 12327 + ], + "fixture_utf16_range": [ + 21510, + 23215 + ], + "sha256": "f20e3b0f0d1ab76fb7b8990542d554e8400a38932f5048887d802e0335968472" + }, + { + "label": "module:69454", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 153597, + 153645 + ], + "fixture_utf16_range": [ + 23217, + 23265 + ], + "sha256": "d360bddb8b451e324d33597e6746c625a82df2e1657563f3bd0d05eee3cf93c4" + }, + { + "label": "module:69632", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 598994, + 599166 + ], + "fixture_utf16_range": [ + 23267, + 23439 + ], + "sha256": "e71daee0073324f300e041af2a9162fa0dee0ccc6b2688cccd1c4bdd59713a1a" + }, + { + "label": "module:70674", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 587160, + 587935 + ], + "fixture_utf16_range": [ + 23441, + 24216 + ], + "sha256": "4a741e61c15995b89c7e98b89131d698e977e449f7d943268323a403dd589db3" + }, + { + "label": "module:72195", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 13105, + 13264 + ], + "fixture_utf16_range": [ + 24218, + 24377 + ], + "sha256": "39dbda9cac00bd3e17ec37b831cf6d70d002442323c04bb408afc85f1b9bb330" + }, + { + "label": "module:72532", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 593765, + 593889 + ], + "fixture_utf16_range": [ + 24379, + 24503 + ], + "sha256": "9cdcf4ad679ae2c3c8ae33688ebeafccd634adc622d0cdbc83bbff67c3988596" + }, + { + "label": "module:72834", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 573724, + 577583 + ], + "fixture_utf16_range": [ + 24505, + 28364 + ], + "sha256": "3e1628b3ab6c18c009a39d2230334d59e326c16ec94900cc8986144d8078db9c" + }, + { + "label": "module:73788", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 10111, + 10228 + ], + "fixture_utf16_range": [ + 28366, + 28483 + ], + "sha256": "6ef54091bd86cd194fec09cdb8b7457c2fa7c382287d01f6ad7203d4853f020b" + }, + { + "label": "module:74535", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 599167, + 599364 + ], + "fixture_utf16_range": [ + 28485, + 28682 + ], + "sha256": "87f0ef7784d3023fc01302c8cb1160d5450567b70cc691c9286c93e9959a443a" + }, + { + "label": "module:74784", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 596579, + 596652 + ], + "fixture_utf16_range": [ + 28684, + 28757 + ], + "sha256": "98f21d559b8293f2a14a7a3912447f9e628810db4ef426add2af31d83c1c3497" + }, + { + "label": "module:74967", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 596029, + 596112 + ], + "fixture_utf16_range": [ + 28759, + 28842 + ], + "sha256": "06c4085571ee1829b8f9d6aa6cae3293200845465517ef4f1c7d3556c7d8d2be" + }, + { + "label": "module:75234", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 602275, + 602539 + ], + "fixture_utf16_range": [ + 28844, + 29108 + ], + "sha256": "601502a39a577ee921f430c11e53a8c4f42263f54e000af8a60318d00465a86f" + }, + { + "label": "module:75257", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 15010, + 15172 + ], + "fixture_utf16_range": [ + 29110, + 29272 + ], + "sha256": "547202c622c0678fe1996fe26a59112873e001ae2faf21cb096e2f53362010b0" + }, + { + "label": "module:75853", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 606372, + 606549 + ], + "fixture_utf16_range": [ + 29274, + 29451 + ], + "sha256": "8b0838231865ee1272a1851fa5119277507ff43ffd010268dce8da2aa8d9b065" + }, + { + "label": "module:75857", + "source_file": "16873.442d9b04.js", + "source_utf16_range": [ + 15615, + 15713 + ], + "fixture_utf16_range": [ + 29453, + 29551 + ], + "sha256": "a261bcfa630fd200eea8d0d40cbdf2e58d64a68a68d44553ab8ce322603face0" + }, + { + "label": "module:75964", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 598717, + 598851 + ], + "fixture_utf16_range": [ + 29553, + 29687 + ], + "sha256": "701fc82477ca6dd6551953904242c879f9f7aa6e904b8d7642e23250e271245c" + }, + { + "label": "module:76853", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601507, + 601641 + ], + "fixture_utf16_range": [ + 29689, + 29823 + ], + "sha256": "32bb4867042c0f3b7c11618f19f156407c3beb49698e989e59a97ad4f2bb5155" + }, + { + "label": "module:77135", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 729450, + 729907 + ], + "fixture_utf16_range": [ + 29825, + 30282 + ], + "sha256": "43d341be2b691de3e87c9b3cbf50553f3623bf97e18dd6d408feed0d3e769012" + }, + { + "label": "module:77848", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 593102, + 593398 + ], + "fixture_utf16_range": [ + 30284, + 30580 + ], + "sha256": "f0a411738d6079277fb997895094722b8afaf005697fa5fe8829fa18d4d8580a" + }, + { + "label": "module:77979", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 596891, + 597108 + ], + "fixture_utf16_range": [ + 30582, + 30799 + ], + "sha256": "1d5034a4d653356fd0b00057aa99c918fef06be13e5a3e2b76c0e3919ef98381" + }, + { + "label": "module:78340", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 12676, + 12801 + ], + "fixture_utf16_range": [ + 30801, + 30926 + ], + "sha256": "b4b31342f4ac7948890018433d25e0cd6b499cd56fe4676c49fd1713cd8cb6b7" + }, + { + "label": "module:78763", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 602673, + 602787 + ], + "fixture_utf16_range": [ + 30928, + 31042 + ], + "sha256": "2d935b02a8d78252f0f1016a3a23598d0696f9c26cdb6c9d118b77fc4fb1d7f6" + }, + { + "label": "module:78942", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 602540, + 602672 + ], + "fixture_utf16_range": [ + 31044, + 31176 + ], + "sha256": "b7c35e89c6a3f7d2441f208e37bfe23eaf047eb5bae09794a7ee7d93a48b6103" + }, + { + "label": "module:79067", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 599639, + 599773 + ], + "fixture_utf16_range": [ + 31178, + 31312 + ], + "sha256": "6ae9b7a65dff2a2e0da5c1d04311fe66170bc3793c319bf7b81f27319e916c67" + }, + { + "label": "module:79071", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 159958, + 159993 + ], + "fixture_utf16_range": [ + 31314, + 31349 + ], + "sha256": "3383fcf7b47c1c64ddba300230a7d294c0091fbc1d26cbd5c59e7049c43c8978" + }, + { + "label": "module:79847", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 582923, + 583052 + ], + "fixture_utf16_range": [ + 31351, + 31480 + ], + "sha256": "415d53d628ff287881c2d0d89f81a5ed1c0636b394308321d7d411ff1e0ae94a" + }, + { + "label": "module:79968", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 10229, + 10335 + ], + "fixture_utf16_range": [ + 31482, + 31588 + ], + "sha256": "8cdf78eb551dca5ed6baa55039163bdf4b0473f5af611e418d6d854fc4995706" + }, + { + "label": "module:80088", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 600995, + 601073 + ], + "fixture_utf16_range": [ + 31590, + 31668 + ], + "sha256": "87d74c6c8d1270ad9f16a5f22220f0b5111b71103ee800fce69acdb01bc6cd76" + }, + { + "label": "module:80393", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 596113, + 596205 + ], + "fixture_utf16_range": [ + 31670, + 31762 + ], + "sha256": "4561e9ccd10aa400d71cccdfae7d9b8578a6c05a07bd5dbd011cd29c9ea9ffac" + }, + { + "label": "module:80547", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 607478, + 607548 + ], + "fixture_utf16_range": [ + 31764, + 31834 + ], + "sha256": "a36ae886ff1e4a545d4fbecb9b6e26c33d553af60189f8bdd04ede674f580a8e" + }, + { + "label": "module:80686", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 727538, + 727755 + ], + "fixture_utf16_range": [ + 31836, + 32053 + ], + "sha256": "8e98a2ac4b4d17f0926c355e807a66f0b200cc67b3924d52bb7bda83051ca784" + }, + { + "label": "module:80777", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 12328, + 12431 + ], + "fixture_utf16_range": [ + 32055, + 32158 + ], + "sha256": "d5a3bd9ed95f328cdcadb2abbadb085a23ae0d892937d195d705fe3454d57628" + }, + { + "label": "module:80796", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 603058, + 603115 + ], + "fixture_utf16_range": [ + 32160, + 32217 + ], + "sha256": "44c89c326178075a48fddb166cee9ed2a7e031cce9cab3857960f3490212d11c" + }, + { + "label": "module:81211", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 608116, + 608212 + ], + "fixture_utf16_range": [ + 32219, + 32315 + ], + "sha256": "cbd101b158d70aabecf87e86b3aa8fa0a7feb60fa019caa70dd55435efd8c269" + }, + { + "label": "module:81935", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 12432, + 12535 + ], + "fixture_utf16_range": [ + 32317, + 32420 + ], + "sha256": "8666ceaaf3946f2a9fde43b9d1a84d3968f6397c7772ff22a4655f6bdd160742" + }, + { + "label": "module:82226", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 10336, + 10441 + ], + "fixture_utf16_range": [ + 32422, + 32527 + ], + "sha256": "3b465f17792c72b047fe738eeb5d7771937e4f026b172d745139b482968c9729" + }, + { + "label": "module:82438", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 162032, + 162113 + ], + "fixture_utf16_range": [ + 32529, + 32610 + ], + "sha256": "210c0fa472c3c4ba6ecbd0a33951e1e79960edfa96ee7b162744b33ba5c1ede1" + }, + { + "label": "module:82451", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 608667, + 608749 + ], + "fixture_utf16_range": [ + 32612, + 32694 + ], + "sha256": "1e1a647582f679e13827e7440d3b8e0b8ce59a4d5c39a9c809ded364ea5bf6e0" + }, + { + "label": "module:82619", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 604567, + 604630 + ], + "fixture_utf16_range": [ + 32696, + 32759 + ], + "sha256": "d3925a2473dd47710bd679e3fb191545a00b9307ac903fd1ceb6290706f4d358" + }, + { + "label": "module:83546", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 12931, + 13017 + ], + "fixture_utf16_range": [ + 32761, + 32847 + ], + "sha256": "12770e13eed4a336689e5e43247acb37806734586e18bed27addf145aa238e43" + }, + { + "label": "module:83943", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 14720, + 14805 + ], + "fixture_utf16_range": [ + 32849, + 32934 + ], + "sha256": "fc7f6cc7819d8084c96f18bb458e41270e2d7dd93a5bce531723e53435b2b5e3" + }, + { + "label": "module:84354", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 592718, + 592821 + ], + "fixture_utf16_range": [ + 32936, + 33039 + ], + "sha256": "15908c95388269ac42fe80ad5b4b98f5c59084022a43615e1b83d5cf4eb12367" + }, + { + "label": "module:84570", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 584560, + 584618 + ], + "fixture_utf16_range": [ + 33041, + 33099 + ], + "sha256": "a1e6b1691e64d253b6d095af66b0ae363bbac4f55ea23368157c82f664de67b5" + }, + { + "label": "module:84882", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 599948, + 600081 + ], + "fixture_utf16_range": [ + 33101, + 33234 + ], + "sha256": "1e2769c3188a2311df87097d0011a17789802b030c1980ba0b050d18f9a72a49" + }, + { + "label": "module:85832", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 13358, + 13523 + ], + "fixture_utf16_range": [ + 33236, + 33401 + ], + "sha256": "9f520d64444c005acfb112108f5b25492e1b91b39cb5a2afb79e017f599bad59" + }, + { + "label": "module:85899", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 597705, + 597988 + ], + "fixture_utf16_range": [ + 33403, + 33686 + ], + "sha256": "d0ef9c68a37fde49f64a26b91422e4bdc4b373fae23cc669b6b5dd420bf26944" + }, + { + "label": "module:86565", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 51418, + 51658 + ], + "fixture_utf16_range": [ + 33688, + 33928 + ], + "sha256": "0f6f721c882062851680b83b1d5773534b60b8a37cc673af0489f1bacee14def" + }, + { + "label": "module:86596", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 593604, + 593764 + ], + "fixture_utf16_range": [ + 33930, + 34090 + ], + "sha256": "69ad38ed878b07d5516be517db9137d49ce5c668c50fa32cc7b7285dc02609d1" + }, + { + "label": "module:86912", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 582720, + 582922 + ], + "fixture_utf16_range": [ + 34092, + 34294 + ], + "sha256": "e924a40ffc16cd961d564fc7c5d69f43acab910d68a2a6f7edc25b3ba2251e06" + }, + { + "label": "module:87379", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 585211, + 585410 + ], + "fixture_utf16_range": [ + 34296, + 34495 + ], + "sha256": "c8f0743a2c3a4f34a4a1bd153ed5855ec56b06c535ced7fa4da06e4bc9986f0c" + }, + { + "label": "module:87809", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 158391, + 158538 + ], + "fixture_utf16_range": [ + 34497, + 34644 + ], + "sha256": "f4ee5c3f358f7b22762835114176048afa29c626c59690e2aa6bfa073cdca8df" + }, + { + "label": "module:87835", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 162114, + 162214 + ], + "fixture_utf16_range": [ + 34646, + 34746 + ], + "sha256": "495bbb500b672d87a46ac5a345600389d6a200fe88591445873f66a5911c0066" + }, + { + "label": "module:88861", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 594436, + 595334 + ], + "fixture_utf16_range": [ + 34748, + 35646 + ], + "sha256": "c78272e4aae60cf26a474e107662fa8e055ef97a279700927c45e0fd78f1ebeb" + }, + { + "label": "module:89220", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 247069, + 247619 + ], + "fixture_utf16_range": [ + 35648, + 36198 + ], + "sha256": "c2fee77902187f680fa5d4e591720e61726edf085e3bcf78fb3b6a134b1252df" + }, + { + "label": "module:89413", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 581956, + 582024 + ], + "fixture_utf16_range": [ + 36200, + 36268 + ], + "sha256": "3b52d0b698640c31090b103a3a6226bd8f24fa684a834d0fce5fa3c3932fe429" + }, + { + "label": "module:89624", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 586765, + 587159 + ], + "fixture_utf16_range": [ + 36270, + 36664 + ], + "sha256": "5cecc166757bee669c09d915cb56038ba503c2ec6cd5c87256b9312d2fffdcbb" + }, + { + "label": "module:90313", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 584619, + 584818 + ], + "fixture_utf16_range": [ + 36666, + 36865 + ], + "sha256": "1c3e18ac5d5aa1038eeb1db06ed93461766dc24e5b7c5c76b234342a00e67ca3" + }, + { + "label": "module:90353", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 585739, + 586373 + ], + "fixture_utf16_range": [ + 36867, + 37501 + ], + "sha256": "ab76b89c77f301a1a6835ac45a5f1e8fcc9025c32b6232af1fa6eb6fe0bc1ecc" + }, + { + "label": "module:92264", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 592063, + 592198 + ], + "fixture_utf16_range": [ + 37503, + 37638 + ], + "sha256": "adc6d910a4945b8b216fcd91a1715484f1d847703e005ec8665e0f438352e143" + }, + { + "label": "module:92480", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 12536, + 12675 + ], + "fixture_utf16_range": [ + 37640, + 37779 + ], + "sha256": "54313abc78ce9ce3a70460c3c9d88d787dd08e5edbd538856f6f54805bc7e774" + }, + { + "label": "module:93526", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 591550, + 591672 + ], + "fixture_utf16_range": [ + 37781, + 37903 + ], + "sha256": "1db2e572b412f3fa12052deb00a6e699b0e0186b4fd163ab6dd423dff74da06d" + }, + { + "label": "module:93650", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 13265, + 13357 + ], + "fixture_utf16_range": [ + 37905, + 37997 + ], + "sha256": "1d44dcb8b93cb2a36dc2bf2f3400e6d3cd2b65685993301185fa41b3126c4f20" + }, + { + "label": "module:93655", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 607053, + 607276 + ], + "fixture_utf16_range": [ + 37999, + 38222 + ], + "sha256": "ed8d08853c943b620d2fd9e69c28413a4621bc7095b437da0b50033816a5aa19" + }, + { + "label": "module:94497", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601642, + 601703 + ], + "fixture_utf16_range": [ + 38224, + 38285 + ], + "sha256": "4615c772475a5838c5bd4612439c16377942727c5abf54f8cffaa958573dc390" + }, + { + "label": "module:94652", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 255124, + 255362 + ], + "fixture_utf16_range": [ + 38287, + 38525 + ], + "sha256": "832bfdab389d857e595dcf1f4acc3f96cc33761df6c44b94c7d3a263f7306ce3" + }, + { + "label": "module:94687", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 585590, + 585738 + ], + "fixture_utf16_range": [ + 38527, + 38675 + ], + "sha256": "6cd51955bb8ba4f7370d16a33f2ba04a7d0fb022d85551a8d7aebd5602c70e38" + }, + { + "label": "module:94715", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 596477, + 596578 + ], + "fixture_utf16_range": [ + 38677, + 38778 + ], + "sha256": "bf7bce2cd271b629dbaa1351ad1d830a96715eb22341065f820ea2c799e7baf4" + }, + { + "label": "module:94889", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 725026, + 725113 + ], + "fixture_utf16_range": [ + 38780, + 38867 + ], + "sha256": "1b7728d1ffb25d7ab66bedaa2d69f0f869d40a9d7768068db841f074412e0577" + }, + { + "label": "module:95243", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 609977, + 610054 + ], + "fixture_utf16_range": [ + 38869, + 38946 + ], + "sha256": "a281508355dc2b31a0bed6628444c3835e69f9ed3ee868930827b133efa269ab" + }, + { + "label": "module:95894", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601283, + 601391 + ], + "fixture_utf16_range": [ + 38948, + 39056 + ], + "sha256": "73dd83732f567e1d98bfa3e81016447c1dbd82b5375690b6c4659c027359eb70" + }, + { + "label": "module:96493", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 603890, + 604178 + ], + "fixture_utf16_range": [ + 39058, + 39346 + ], + "sha256": "6022a16a994e7f2e22fb49ccf64fff4541f75aba3024a79aeb0782f1f11627bf" + }, + { + "label": "module:96638", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 603620, + 603889 + ], + "fixture_utf16_range": [ + 39348, + 39617 + ], + "sha256": "97386fc0ac3b4a1950d9546d477cefb9db7aefd5a3cd7fa4be9b511f526c0f22" + }, + { + "label": "module:96830", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 271404, + 271588 + ], + "fixture_utf16_range": [ + 39619, + 39803 + ], + "sha256": "142815a78de8e480b7f49b8142a736ab53d94b466a917f352c174fb576b53046" + }, + { + "label": "module:97034", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 584104, + 584218 + ], + "fixture_utf16_range": [ + 39805, + 39919 + ], + "sha256": "636a711cd1cd753c9e9632151e3b9d78c83f2efdee8c992c157e8f446632a0cf" + }, + { + "label": "module:97350", + "source_file": "40784.08580559.js", + "source_utf16_range": [ + 13524, + 13727 + ], + "fixture_utf16_range": [ + 39921, + 40124 + ], + "sha256": "26bdce0d2dd0bf2e4165e5f06a665735673a150b931f04cae48d06cda193c60e" + }, + { + "label": "module:97706", + "source_file": "56679.46a7195c.js", + "source_utf16_range": [ + 156976, + 157077 + ], + "fixture_utf16_range": [ + 40126, + 40227 + ], + "sha256": "8c5aa29d25e6bc73c1f9ba0e08faf5c817a6064da9b4b538a555f7c165d3525c" + }, + { + "label": "module:98121", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 601704, + 601767 + ], + "fixture_utf16_range": [ + 40229, + 40292 + ], + "sha256": "135ba2b8442dde80118be4ab46c28ac0472930f78fcbbdf996b5a0bede5c86df" + }, + { + "label": "module:98880", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 590387, + 590475 + ], + "fixture_utf16_range": [ + 40294, + 40382 + ], + "sha256": "23e079370e85de6a1f06b0a3147992d71778c62366bb348e45438f2b829adac7" + }, + { + "label": "module:99306", + "source_file": "main.37a180b9.js", + "source_utf16_range": [ + 609149, + 609192 + ], + "fixture_utf16_range": [ + 40384, + 40427 + ], + "sha256": "7d16ff5ad030e85fb71cdd8c2c331f559e3111f9fde1844780b26953bdc7d618" + }, + { + "label": "declaration:76535:x", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 842, + 852 + ], + "fixture_utf16_range": [ + 40474, + 40484 + ], + "sha256": "9ad185a7078d40d73a11dab2f178919067a27cb7b7dea2d47f257e1738025c18" + }, + { + "label": "declaration:76535:ot", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 853, + 862 + ], + "fixture_utf16_range": [ + 40490, + 40499 + ], + "sha256": "f8b3edc1931d3dc8d7dee6583d346ec88090e793b2da6ede4d9e20f1882a8bae" + }, + { + "label": "declaration:76535:Z", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 863, + 873 + ], + "fixture_utf16_range": [ + 40505, + 40515 + ], + "sha256": "3674766b2cf72925c19de372f9dad1f3280ddbc4172f292407b078feb4cebe8b" + }, + { + "label": "declaration:76535:rt", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 874, + 883 + ], + "fixture_utf16_range": [ + 40521, + 40530 + ], + "sha256": "d204358ced7299ffe156e8cb511a1065303ff22970b3541141e92e065434948c" + }, + { + "label": "declaration:76535:at", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 884, + 895 + ], + "fixture_utf16_range": [ + 40536, + 40547 + ], + "sha256": "7ef5bac3d56cce9f4a4ccb488624d9ca7f67ae4ccd410776535a97f78edc5a7f" + }, + { + "label": "declaration:76535:j", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 896, + 905 + ], + "fixture_utf16_range": [ + 40553, + 40562 + ], + "sha256": "4f26093b37c5ac6ab0b0e0e90cdc1f799e3181aff7588816f86abb5d3346884a" + }, + { + "label": "declaration:76535:X", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 906, + 916 + ], + "fixture_utf16_range": [ + 40568, + 40578 + ], + "sha256": "9b58b0dc7769ff55974e8d82666f3999e115af7a03d0758e0e9ea95e89e3555e" + }, + { + "label": "declaration:76535:Q", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 917, + 925 + ], + "fixture_utf16_range": [ + 40584, + 40592 + ], + "sha256": "65356d2234bf73e526637b595f087be0178713bb05924e53046a9e918db301ce" + }, + { + "label": "declaration:76535:ct", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 926, + 937 + ], + "fixture_utf16_range": [ + 40598, + 40609 + ], + "sha256": "a220f1fde6fd9f87a1ce53511f6d0a4ff687ef3be7e3706cf141823be6b9c8e9" + }, + { + "label": "declaration:76535:it", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 938, + 948 + ], + "fixture_utf16_range": [ + 40615, + 40625 + ], + "sha256": "85c46200880294356763a6943229f5377b0ed607802a14acc6de51284720f709" + }, + { + "label": "declaration:76535:lt", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 949, + 960 + ], + "fixture_utf16_range": [ + 40631, + 40642 + ], + "sha256": "0bd854ed6256e923c3d4a28966e2f97f1a6076c01d2f648efdf45c12abe32982" + }, + { + "label": "declaration:76535:ut", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 961, + 971 + ], + "fixture_utf16_range": [ + 40648, + 40658 + ], + "sha256": "44e5ccda01242c8dd61697a99798630beb585ed6fdd76e2962edb237f8fddea7" + }, + { + "label": "declaration:76535:a", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 972, + 982 + ], + "fixture_utf16_range": [ + 40664, + 40674 + ], + "sha256": "b2b87dae2238d22539bac309bd4dc93363985f0d72f0d46f474c3327192a1f45" + }, + { + "label": "declaration:76535:E", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 983, + 991 + ], + "fixture_utf16_range": [ + 40680, + 40688 + ], + "sha256": "f5845df83303b3fd14d567796ddda2fbe7f0db84dde36a83d05e94fa027e5c50" + }, + { + "label": "declaration:76535:O", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 992, + 1002 + ], + "fixture_utf16_range": [ + 40694, + 40704 + ], + "sha256": "165f104b7a9466aa11642778013c3bc4a4ac6fbb5ce4d82b0b2f7b598325a465" + }, + { + "label": "declaration:76535:W", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 1009, + 1036 + ], + "fixture_utf16_range": [ + 40712, + 40739 + ], + "sha256": "1b830418cd017fa6780aeabdc522fda254d402fcf7a2dea8af7dac025cc72273" + }, + { + "label": "declaration:76535:_t", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 1037, + 1064 + ], + "fixture_utf16_range": [ + 40747, + 40774 + ], + "sha256": "98888ddda77291ec1b0a2fbcf0e655dc12270948e41b8d8668c8d0527256c505" + }, + { + "label": "declaration:76535:ft", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 1065, + 1084 + ], + "fixture_utf16_range": [ + 40782, + 40801 + ], + "sha256": "3948796a7e26ef18c2fb975473a8880df62b2a1db012804ce4a15430fd3ebbf5" + }, + { + "label": "declaration:76535:J", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 1085, + 1099 + ], + "fixture_utf16_range": [ + 40809, + 40823 + ], + "sha256": "09fdbf3549b0ad2d869914e4ba6d0a020a47796dfd7ecdb022a47e36b3ec9baf" + }, + { + "label": "declaration:76535:kt", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 1100, + 1508 + ], + "fixture_utf16_range": [ + 40831, + 41239 + ], + "sha256": "bdfa48ab3ac321be02acf35dfb1b2533c6b555d31d44168186be498b3be0057e" + }, + { + "label": "declaration:76535:wt", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 1509, + 1735 + ], + "fixture_utf16_range": [ + 41247, + 41473 + ], + "sha256": "87a5d085750406888fee56c39f3692b2dee28fa9b72c268f4787f009db536962" + }, + { + "label": "declaration:76535:xt", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 1736, + 2052 + ], + "fixture_utf16_range": [ + 41481, + 41797 + ], + "sha256": "3b9d7cfb8fa7a0b37cdd3dfb0a72c41b74ea0a96cad984bb4f21bc1afa82507e" + }, + { + "label": "declaration:76535:Wt", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 2053, + 2233 + ], + "fixture_utf16_range": [ + 41805, + 41985 + ], + "sha256": "7066ec195732c44d0035f237f6b975979223f7f439f54c519f1cafa04d61ec97" + }, + { + "label": "declaration:76535:zt", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 2234, + 2801 + ], + "fixture_utf16_range": [ + 41993, + 42560 + ], + "sha256": "1cd5679e7d173811a05fb914dded3fac2c8ebebebfdf2bfc2603afcd6165d417" + }, + { + "label": "callback:76535:Tt", + "source_file": "36648.dd8ae7dc.js", + "source_utf16_range": [ + 10961, + 11094 + ], + "fixture_utf16_range": [ + 42822, + 42955 + ], + "sha256": "3bdedf3c7a6695eed0cea796a03102637b689ec34edfbd86a5d4d59eed3022a2" + }, + { + "label": "declaration:15426:ot", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 353192, + 353203 + ], + "fixture_utf16_range": [ + 43009, + 43020 + ], + "sha256": "56f8d0ee86d72f3cc375925d68b9f7d4a8897a8e8d4eabef6db73a7c1b63de21" + }, + { + "label": "declaration:15426:d", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 353204, + 353213 + ], + "fixture_utf16_range": [ + 43026, + 43035 + ], + "sha256": "a8011da3bba4cb745ab319ef046e465913f328b8f7c8764f69f4588d6d5576c6" + }, + { + "label": "declaration:15426:A", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 353318, + 353328 + ], + "fixture_utf16_range": [ + 43041, + 43051 + ], + "sha256": "5f94721ea577197259ede09d3d9ecd5f3494f2771632c5fb6b191da3389a325a" + }, + { + "label": "declaration:15426:R", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 353329, + 353337 + ], + "fixture_utf16_range": [ + 43057, + 43065 + ], + "sha256": "c734a535a960f80e1be16a1c84c7fcbfdd94a3462d7a763063fed24128b0b590" + }, + { + "label": "declaration:15426:Tt", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 359034, + 359045 + ], + "fixture_utf16_range": [ + 43071, + 43082 + ], + "sha256": "ba947090750831ef3335c7b90de6adfb0dcc4bb571747cb8dbac0286dff65947" + }, + { + "label": "declaration:15426:vt", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 359046, + 359057 + ], + "fixture_utf16_range": [ + 43088, + 43099 + ], + "sha256": "5d279be4f9f699d9d62919bc11ed5b2e703b560ffe91172e0bed0b2a582746dd" + }, + { + "label": "declaration:15426:lo", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 452441, + 452549 + ], + "fixture_utf16_range": [ + 43107, + 43215 + ], + "sha256": "b15226a664d176b895c57ec87d0a854a77a17b57296a8340e2f40349ca781b62" + }, + { + "label": "declaration:15426:Sa", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 499737, + 499748 + ], + "fixture_utf16_range": [ + 43221, + 43232 + ], + "sha256": "36bcf27970ff0d306794def572e5e5c87d331dc06b2f9d979db29b2d1862def3" + }, + { + "label": "declaration:15426:Dn", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 499749, + 499759 + ], + "fixture_utf16_range": [ + 43238, + 43248 + ], + "sha256": "c54a7313d231a4652f386d8a70b0a77d84065701dfa0f677d96f0f8c7c7c343e" + }, + { + "label": "declaration:15426:xc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537530, + 537541 + ], + "fixture_utf16_range": [ + 43254, + 43265 + ], + "sha256": "a8efd2fc67b16aee2a87a349e11c82297b4f5cf254b78e418a0458b986611dde" + }, + { + "label": "declaration:15426:bc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537542, + 537552 + ], + "fixture_utf16_range": [ + 43271, + 43281 + ], + "sha256": "67be6013f2a9d488e6f37af0be5e9526b68b5552df143bec72f9b9ace2735c29" + }, + { + "label": "declaration:15426:Sc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537553, + 537564 + ], + "fixture_utf16_range": [ + 43287, + 43298 + ], + "sha256": "0d73989504bd1f5f943bc7dbfaa17a33547d8026b97be19f30321ebe78941076" + }, + { + "label": "declaration:15426:Do", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537565, + 537575 + ], + "fixture_utf16_range": [ + 43304, + 43314 + ], + "sha256": "6a6f9d98c4d4d341ed135a4ed092f5c28334add0bb65c190313ad7431b9ecd19" + }, + { + "label": "declaration:15426:Ac", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537576, + 537587 + ], + "fixture_utf16_range": [ + 43320, + 43331 + ], + "sha256": "435fd7bd280268f4c3eaed6d497568f7d14764ad022138026846e2790aa479ca" + }, + { + "label": "declaration:15426:vc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537588, + 537598 + ], + "fixture_utf16_range": [ + 43337, + 43347 + ], + "sha256": "5b120e90ec8b92e8c60a804421dffdef8fd1c2aa58f34a6f4fd07aa61297fa5f" + }, + { + "label": "declaration:15426:Cc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537599, + 537610 + ], + "fixture_utf16_range": [ + 43353, + 43364 + ], + "sha256": "5dc0da8abeee3aaea696a97e069841c858b04950cc21a91c81e12ad241d56ed9" + }, + { + "label": "declaration:15426:Qi", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537611, + 537621 + ], + "fixture_utf16_range": [ + 43370, + 43380 + ], + "sha256": "a435e6b65a5d242c9da21386a1ff9ce32e35f65a2b90671b2bc15d16b70bff04" + }, + { + "label": "declaration:15426:Vo", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537628, + 537752 + ], + "fixture_utf16_range": [ + 43388, + 43512 + ], + "sha256": "147376d47e9bac1b304d64f513d3ebfc5f3a3ec0b4587b9f7a31683c9b414a6f" + }, + { + "label": "declaration:15426:Mo", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537753, + 537791 + ], + "fixture_utf16_range": [ + 43520, + 43558 + ], + "sha256": "cad91c49bceb8905c1235d610a2e72d0f98cee5b2001f6a88ebb2e3af56e2990" + }, + { + "label": "declaration:15426:wc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537792, + 537820 + ], + "fixture_utf16_range": [ + 43566, + 43594 + ], + "sha256": "388d4c42ffb387d00039fd43d15a8e98c57862fd87311617226772c6ae90b3c5" + }, + { + "label": "declaration:15426:No", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537821, + 537851 + ], + "fixture_utf16_range": [ + 43602, + 43632 + ], + "sha256": "391ba5f54d116410f0580cff315196eea825b78e1642597a68b560830abd62e8" + }, + { + "label": "declaration:15426:Eo", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537852, + 537896 + ], + "fixture_utf16_range": [ + 43640, + 43684 + ], + "sha256": "0d642a1b392a1e1af1da5fcf1e76cc86a7288f34886ebac93b2b80393da39879" + }, + { + "label": "declaration:15426:qi", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537897, + 537914 + ], + "fixture_utf16_range": [ + 43692, + 43709 + ], + "sha256": "16909925e9f5a091cf0d718f10b69559d8d7e6615be2a36579e1687f9e29e1aa" + }, + { + "label": "declaration:15426:Rn", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537915, + 537932 + ], + "fixture_utf16_range": [ + 43717, + 43734 + ], + "sha256": "4b6ffa9d48051416ea6a9de96502284c8c2eac7aa357e43438859d8d361df37e" + }, + { + "label": "declaration:15426:Tc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537933, + 537962 + ], + "fixture_utf16_range": [ + 43742, + 43771 + ], + "sha256": "1df691ee775abfb89520e8fe5549435c7d09699c687a80680f29300ec6a1fd85" + }, + { + "label": "declaration:15426:Bo", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 537963, + 538028 + ], + "fixture_utf16_range": [ + 43779, + 43844 + ], + "sha256": "a943fcd7c936fe7932a59007ec54496d289eb5bb8ed4411d087178f9195d2070" + }, + { + "label": "declaration:15426:Ro", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 538029, + 538370 + ], + "fixture_utf16_range": [ + 43846, + 44187 + ], + "sha256": "237c259a915dca6aeb80153236d72130e7449aa81fefb1e4ac10f400cefd405f" + }, + { + "label": "declaration:15426:Lc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 538370, + 538568 + ], + "fixture_utf16_range": [ + 44188, + 44386 + ], + "sha256": "403d0bf86e9d5c72c2dae7bc13ea0847a855f51cedf5c93a92ba0a2dffae7718" + }, + { + "label": "declaration:15426:Ic", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 538574, + 538745 + ], + "fixture_utf16_range": [ + 44393, + 44564 + ], + "sha256": "1d2aee0d64ecd4167bdd3cc54f005953f29572690a4b06b5abc7a91ef3105f24" + }, + { + "label": "declaration:15426:_o", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 538746, + 539001 + ], + "fixture_utf16_range": [ + 44572, + 44827 + ], + "sha256": "6fda5f78546408db3eedadbea31abebe0ca2413f112bb1fad8717074a5371253" + }, + { + "label": "declaration:15426:ko", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 539002, + 539034 + ], + "fixture_utf16_range": [ + 44835, + 44867 + ], + "sha256": "3fdb1f0e4a7c6e4da456f68139cfc397b1a3effa81c9075c179aa6ac6cda3fa0" + }, + { + "label": "declaration:15426:Pc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 539035, + 539172 + ], + "fixture_utf16_range": [ + 44875, + 45012 + ], + "sha256": "b20c55d629d6c5e0426b30aaaa9378e9114f6284ff2c10c7c75f2b06bed6492c" + }, + { + "label": "declaration:15426:Ho", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 539173, + 539266 + ], + "fixture_utf16_range": [ + 45020, + 45113 + ], + "sha256": "d3cc9d7026185bb2efd068eb1c5252c2004b1c73d5f48cf0c7312b711e54a7bc" + }, + { + "label": "declaration:15426:Wo", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 539267, + 541246 + ], + "fixture_utf16_range": [ + 45121, + 47100 + ], + "sha256": "54f9b5d4f41fc656aca9db9b591909d284ba24b53ef35d944e3537446dc7a81b" + }, + { + "label": "declaration:15426:Dc", + "source_file": "28928.9aeec892.js", + "source_utf16_range": [ + 541247, + 542526 + ], + "fixture_utf16_range": [ + 47108, + 48387 + ], + "sha256": "eaba7dfb579d8b085c5f890d78deb8d8b528f1939a0078df5384d7cfc1a18ea1" + }, + { + "label": "declaration:70940:o", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 6745, + 6755 + ], + "fixture_utf16_range": [ + 48471, + 48481 + ], + "sha256": "57f898825807bc474d4ed21a63a45176cf280cd7cd267bc45cf1f825d549dc40" + }, + { + "label": "declaration:70940:v", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 6756, + 6766 + ], + "fixture_utf16_range": [ + 48487, + 48497 + ], + "sha256": "99611dfae5183efc7bbecf3b7711008a90757c5f3d22ad36d281ec4e4aa0925e" + }, + { + "label": "declaration:70940:i", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 6778, + 6788 + ], + "fixture_utf16_range": [ + 48503, + 48513 + ], + "sha256": "58a6ec8c539e8b2818a3a9e166dc301541aa8019e3cb99f13f4c7d13c961d138" + }, + { + "label": "declaration:70940:M", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 6811, + 6821 + ], + "fixture_utf16_range": [ + 48519, + 48529 + ], + "sha256": "4ba26af3077c24df79061524d201d923b2c29dbff3a4f5c8eba0788d920ed02c" + }, + { + "label": "declaration:70940:h", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 6822, + 6830 + ], + "fixture_utf16_range": [ + 48535, + 48543 + ], + "sha256": "146f2b139dd62452d5af6fd728ce7469debff80b74102cf2dd04e56a98bbaed5" + }, + { + "label": "declaration:70940:V", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 6851, + 6860 + ], + "fixture_utf16_range": [ + 48549, + 48558 + ], + "sha256": "0b2f315d50c17637ac2e831984417a0476826c8e6d58c99a793c4ad2d93942da" + }, + { + "label": "declaration:70940:U", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 6861, + 6869 + ], + "fixture_utf16_range": [ + 48564, + 48572 + ], + "sha256": "426e5ec0784fc49b6f26903423cad35d27d1bfe4a76631217477bf682528f94d" + }, + { + "label": "declaration:70940:Qe", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 10129, + 10140 + ], + "fixture_utf16_range": [ + 48578, + 48589 + ], + "sha256": "d47e0d632cd4028771ceca7c6412f0295bc8c7da270d3ba2793f58e3c6eb1092" + }, + { + "label": "declaration:70940:Tn", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 31600, + 31616 + ], + "fixture_utf16_range": [ + 48595, + 48611 + ], + "sha256": "e977d40a86f3906c839fc1579d1c14b4b5d60c087d3a3a0a90036bce51d07841" + }, + { + "label": "callback:70940:z", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 32997, + 34322 + ], + "fixture_utf16_range": [ + 48748, + 50073 + ], + "sha256": "79aa5f884c6cc6c32815b756a2a406b73231c0c97d058c781a45d0413fe6b6ee" + } + ] +} diff --git a/test/fixtures/binance-chart-storage/host-persistence.js b/test/fixtures/binance-chart-storage/host-persistence.js new file mode 100644 index 0000000..4c04b61 --- /dev/null +++ b/test/fixtures/binance-chart-storage/host-persistence.js @@ -0,0 +1,285 @@ +/* Exact public host persistence fragments; see host-persistence-manifest.json. */ +(self.webpackChunkfutures_trade_ui=self.webpackChunkfutures_trade_ui||[]).push([["codex-chart-host-persistence-fixture"],{ +195(_,y,e){var t=e(84882),i=e(98121),r=Object.prototype,a=r.hasOwnProperty;function s(n){if(!t(n))return i(n);var o=[];for(var c in Object(n))a.call(n,c)&&c!="constructor"&&o.push(c);return o}_.exports=s}, +1386(_,y,e){var t=e(12393),i=e(62049),r=e(7144),a=e(7452),s=e(13964);function n(o){var c=-1,v=o==null?0:o.length;for(this.clear();++c0){if(++a>=y)return arguments[0]}else a=0;return r.apply(void 0,arguments)}}_.exports=i}, +3334(_,y,e){var t=e(36526),i=e(12343),r=e(22053),a=e(53142),s=e(43282);function n(o){return typeof o=="function"?o:o==null?r:typeof o=="object"?a(o)?i(o[0],o[1]):t(o):s(o)}_.exports=n}, +3422(_,y,e){var t=e(57073),i=e(46285),r=Object.prototype,a=r.hasOwnProperty;function s(n,o,c){var v=n[o];(!(a.call(n,o)&&i(v,c))||c===void 0&&!(o in n))&&t(n,o,c)}_.exports=s}, +3766(_){function y(e,t){return function(i){return e(t(i))}}_.exports=y}, +5681(_,y,e){var t=e(44700);function i(r){var a=t(this,r).delete(r);return this.size-=a?1:0,a}_.exports=i}, +7144(_,y,e){var t=e(97034);function i(r){var a=this.__data__,s=t(a,r);return s<0?void 0:a[s][1]}_.exports=i}, +7452(_,y,e){var t=e(97034);function i(r){return t(this.__data__,r)>-1}_.exports=i}, +7462(_){function y(e){return this.__data__.has(e)}_.exports=y}, +7980(Z,x,e){var o=e(23945),v=e(86565),d=o(v);Z.exports=d}, +8006(_){function y(e){return this.__data__.has(e)}_.exports=y}, +8666(_,y,e){var t=e(70674),i=e(49460),r=e(2306),a=r&&r.isTypedArray,s=a?i(a):t;_.exports=s}, +8870(_,y,e){var t=e(65650),i=Object.prototype,r=i.hasOwnProperty,a=i.toString,s=t?t.toStringTag:void 0;function n(o){var c=r.call(o,s),v=o[s];try{o[s]=void 0;var g=!0}catch{}var S=a.call(o);return g&&(c?o[s]=v:delete o[s]),S}_.exports=n}, +10010(_,y,e){"use strict";e.d(y,{_:()=>t});function t(i,r){(r==null||r>i.length)&&(r=i.length);for(var a=0,s=new Array(r);a=u){var he=w?null:a(_);if(he)return c(he);W=!1,$=n,ne=new t}else ne=w?[]:R;e:for(;++OC))return!1;var b=d.get(o),f=d.get(c);if(b&&f)return b==c&&f==o;var p=-1,u=!0,h=v&s?new t:void 0;for(d.set(o,c),d.set(c,o);++pt});function t(i,r){var a,s,n,o={label:0,sent:function(){if(n[0]&1)throw n[1];return n[1]},trys:[],ops:[]},c=Object.create((typeof Iterator=="function"?Iterator:Object).prototype),v=Object.defineProperty;return v(c,"next",{value:g(0)}),v(c,"throw",{value:g(1)}),v(c,"return",{value:g(2)}),typeof Symbol=="function"&&v(c,Symbol.iterator,{value:function(){return this}}),c;function g(d){return function(A){return S([d,A])}}function S(d){if(a)throw new TypeError("Generator is already executing.");for(;c&&(c=0,d[0]&&(o=0)),o;)try{if(a=1,s&&(n=d[0]&2?s.return:d[0]?s.throw||((n=s.return)&&n.call(s),0):s.next)&&!(n=n.call(s,d[1])).done)return n;switch(s=0,n&&(d=[d[0]&2,n.value]),d[0]){case 0:case 1:n=d;break;case 4:return o.label++,{value:d[1],done:!1};case 5:o.label++,s=d[1],d=[0];continue;case 7:d=o.ops.pop(),o.trys.pop();continue;default:if(n=o.trys,!(n=n.length>0&&n[n.length-1])&&(d[0]===6||d[0]===2)){o=0;continue}if(d[0]===3&&(!n||d[1]>n[0]&&d[1]i});function t(r,a,s,n,o,c,v){try{var g=r[c](v),S=g.value}catch(d){s(d);return}g.done?a(S):Promise.resolve(S).then(n,o)}function i(r){return function(){var a=this,s=arguments;return new Promise(function(n,o){var c=r.apply(a,s);function v(S){t(c,n,o,v,g,"next",S)}function g(S){t(c,n,o,v,g,"throw",S)}v(void 0)})}}}, +49460(_){function y(e){return function(t){return e(t)}}_.exports=y}, +49753(_,y,e){var t=e(25098),i=e(1386),r=e(19770);function a(){this.size=0,this.__data__={hash:new t,map:new(r||i),string:new t}}_.exports=a}, +49828(_){function y(e){var t=-1,i=Array(e.size);return e.forEach(function(r){i[++t]=r}),i}_.exports=y}, +50424(O,p,i){var b=i(58898),u=i(92480),g=i(78340),a=i(18069),f=i(92264),_="[object Boolean]",h="[object Date]",T="[object Map]",d="[object Number]",L="[object RegExp]",I="[object Set]",R="[object String]",S="[object Symbol]",A="[object ArrayBuffer]",Z="[object DataView]",D="[object Float32Array]",K="[object Float64Array]",ae="[object Int8Array]",q="[object Int16Array]",ne="[object Int32Array]",W="[object Uint8Array]",s="[object Uint8ClampedArray]",m="[object Uint16Array]",z="[object Uint32Array]";function U(H,X,M){var ie=H.constructor;switch(X){case A:return b(H);case _:case h:return new ie(+H);case Z:return u(H,M);case D:case K:case ae:case q:case ne:case W:case s:case m:case z:return f(H,M);case T:return new ie;case d:case R:return new ie(H);case L:return g(H);case I:return new ie;case S:return a(H)}}O.exports=U}, +51187(_,y,e){var t=e(87379),i=e(80547),r="[object Symbol]";function a(s){return typeof s=="symbol"||i(s)&&t(s)==r}_.exports=a}, +51340(_,y,e){var t=e(1386),i=e(64103),r=e(21779),a=e(34162),s=e(7462),n=e(96638);function o(c){var v=this.__data__=new t(c);this.size=v.size}o.prototype.clear=i,o.prototype.delete=r,o.prototype.get=a,o.prototype.has=s,o.prototype.set=n,_.exports=o}, +52291(_,y,e){var t=e(65650),i=e(65111),r=e(53142),a=e(51187),s=1/0,n=t?t.prototype:void 0,o=n?n.toString:void 0;function c(v){if(typeof v=="string")return v;if(r(v))return i(v,c)+"";if(a(v))return o?o.call(v):"";var g=v+"";return g=="0"&&1/v==-s?"-0":g}_.exports=c}, +53142(_){var y=Array.isArray;_.exports=y}, +54732(_,y,e){var t=e(44700);function i(r){return t(this,r).has(r)}_.exports=i}, +56027(_,y,e){var t=e(87379),i=e(80547),r="[object Arguments]";function a(s){return i(s)&&t(s)==r}_.exports=a}, +57073(_,y,e){var t=e(72532);function i(r,a,s){a=="__proto__"&&t?t(r,a,{configurable:!0,enumerable:!0,value:s,writable:!0}):r[a]=s}_.exports=i}, +57200(_,y,e){var t=e(41580),i=e(84882),r=e(38546),a=Object.prototype,s=a.hasOwnProperty;function n(o){if(!t(o))return r(o);var c=i(o),v=[];for(var g in o)g=="constructor"&&(c||!s.call(o,g))||v.push(g);return v}_.exports=n}, +57452(_,y,e){var t=e(94497),i="__lodash_hash_undefined__";function r(a,s){var n=this.__data__;return this.size+=this.has(a)?0:1,n[a]=t&&s===void 0?i:s,this}_.exports=r}, +57923(_,y,e){var t=e(93526),i=e(66040);function r(a,s){s=t(s,a);for(var n=0,o=s.length;a!=null&&n-1}v.exports=o}, +61875(_,y,e){"use strict";e.d(y,{_:()=>i});var t=e(10010);function i(r,a){if(r){if(typeof r=="string")return(0,t._)(r,a);var s=Object.prototype.toString.call(r).slice(8,-1);if(s==="Object"&&r.constructor&&(s=r.constructor.name),s==="Map"||s==="Set")return Array.from(s);if(s==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(s))return(0,t._)(r,a)}}}, +62049(_,y,e){var t=e(97034),i=Array.prototype,r=i.splice;function a(s){var n=this.__data__,o=t(n,s);if(o<0)return!1;var c=n.length-1;return o==c?n.pop():r.call(n,o,1),--this.size,!0}_.exports=a}, +64066(_){var y=Function.prototype,e=y.toString;function t(i){if(i!=null){try{return e.call(i)}catch{}try{return i+""}catch{}}return""}_.exports=t}, +64103(_,y,e){var t=e(1386);function i(){this.__data__=new t,this.size=0}_.exports=i}, +64207(_,y,e){var t=e(39032),i=e(25288);function r(a){return t(a,i(a))}_.exports=r}, +64512(_,y,e){var t=e(94715),i=e(78942),r=t(i,"Set");_.exports=r}, +64759(_,y,e){var t=e(41950),i=(function(){var a=/[^.]+$/.exec(t&&t.keys&&t.keys.IE_PROTO||"");return a?"Symbol(src)_1."+a:""})();function r(a){return!!i&&i in a}_.exports=r}, +65111(_){function y(e,t){for(var i=-1,r=e==null?0:e.length,a=Array(r);++i-1&&t%1==0&&t<=y}_.exports=e}, +65406(_,y,e){var t=e(36529),i=e(80547);function r(a){return i(a)&&t(a)}_.exports=r}, +65568(_){function y(e,t){return e.has(t)}_.exports=y}, +65650(_,y,e){var t=e(78942),i=t.Symbol;_.exports=i}, +66040(_,y,e){var t=e(51187),i=1/0;function r(a){if(typeof a=="string"||t(a))return a;var s=a+"";return s=="0"&&1/a==-i?"-0":s}_.exports=r}, +67267(_,y,e){var t=e(41580);function i(r){return r===r&&!t(r)}_.exports=i}, +68250(_,y,e){var t=e(49753),i=e(5681),r=e(80088),a=e(54732),s=e(59068);function n(o){var c=-1,v=o==null?0:o.length;for(this.clear();++c-1&&i%1==0&&iQ,NB:()=>I,OP:()=>w,OQ:()=>M,Sc:()=>V,Tf:()=>y,Wx:()=>m,X2:()=>N,_j:()=>C,cs:()=>ot,dp:()=>W,lw:()=>r,nO:()=>D});var m=(function(d){return d.Year="Year",d.Month="Month",d.Week="Week",d.Day="Day",d.Hour="Hour",d.Minute="Minute",d.Second="Second",d})({}),y=(function(d){return d.Alphabetic="alphabetic",d.Top="top",d.Hanging="hanging",d.Middle="middle",d.Ideographic="ideographic",d.Bottom="bottom",d})({}),D=(function(d){return d.Start="start",d.End="end",d.Center="center",d.Left="left",d.Right="right",d})({}),M=(function(d){return d.All="All",d.Marks="Marks",d.Resize="Resize",d})({}),C=(function(d){return d.White="#ffffff",d.Black="#000000",d.Gray="#2d2d2d",d.CandleRed="#E0294A",d.CandleGreen="#2EBD85",d.CandleBlue="#0303F7",d.LightGray="#9a9b9a",d.GrayD="#D8D8D8",d.GrayF="#F2F2F2",d.Gray8="#848E9C",d.GrayE="#EAECEF",d.GrayC="#C1C6CD",d.YellowF="#F0B90B",d.Dark4="#48515D",d.Dark2="#212833",d.Dark2A="#2A2F37",d.Dark1F="#1F2124",d.Blue3="#328DFD",d.Gray0="#FFFFFF",d.Gray5="#FAFAFA",d.Gray10="#F5F5F5",d.Gray50="#E6E8EA",d.Gray100="#CACED3",d.Gray200="#AEB4BC",d.Gray300="#929AA5",d.Gray400="#76808F",d.Gray500="#5E6673",d.Gray600="#474D57",d.Gray700="#2B2F36",d.Gray800="#1E2026",d.Gray900="#14151A",d.Gray1000="#0B0E11",d.Red5="#FEF6F7",d.Red10="#FEF1F2",d.Red50="#FDDDDE",d.Red100="#FDBAB9",d.Red200="#FC9897",d.Red300="#FC6E75",d.Red400="#F84960",d.Red500="#D9304E",d.Red600="#982A42",d.Red700="#5C1929",d.Red800="#35141D",d.Red900="#221113",d.Green5="#F0FFFA",d.Green10="#E2FDF4",d.Green50="#CBF6E8",d.Green100="#9BE8CD",d.Green200="#54E8B4",d.Green300="#2ED191",d.Green400="#02C076",d.Green500="#0F8F62",d.Green600="#1E644E",d.Green700="#143D31",d.Green800="#102821",d.Green900="#0B1915",d.LineColor1="#ff00ff",d.LineColor2="#cc0066",d.LineColor3="#00cccc",d.LineColor4="#2ED191",d.LineColor5="#bd930b",d.LineColor6="#c305c6",d.AreaFillColor1="#e5e5f2",d.AreaFillColor2="#161833",d})({}),r=(function(d){return d.Normal="NORMAL",d.Percentage="PERCENTAGE",d})({}),w=(function(d){return d.Up="UP",d.Down="DOWN",d.Right="RIGHT",d.Left="LEFT",d})({}),I=(function(d){return d.Line="LINE",d.SegmentedDotLine="SEGMENTED_DOT_LINE",d.Bar="BAR",d.BarCandle="BAR_CANDLE",d.CandleBar="CANDLEBAR",d.Area="AREA",d.GradientArea="GRADIENTAREA",d.TimeGradientArea="TIMEGRADIENTAREA",d.Point="POINT",d.CrossLine="CROSSLINE",d.Transcation="TRANSCATION",d.HollowCandleBar="HOLLOW_CANDLEBAR",d.HollowBar="HOLLOW_BAR",d})({}),W=(function(d){return d.Width="WIDTH",d.Height="HIGHT",d.All="ALL",d})({}),N=(function(d){return d.DefaultCursor="DEFAULTCURSOR",d.CrossHairCursor="CROSSHAIRCURSOR",d.TrendLine="TRENDLINE",d.HorizontalExtendedLine="HROZONTAL_EXTENDED_LINE",d.ExtendTrendLine="EXTRENDLINE",d.FibonacciLine="FIBONACCILINE",d.Measurement="MEASUREMENT",d.InfoLine="INFOLINE",d.HorizontalLine="HORIZONTALLINE",d.VerticalLine="VERTICALLINE",d.CrossLine="CROSSLINE",d.ParallelLine="PARALLELLINE",d.UpArrow="UPARROW",d.DownArrow="DOWNARROW",d.LeftArrow="LEFTARROW",d.RightArrow="RIGHTARROW",d.Text="TEXT",d.Rectangle="RECTANGLE",d.RayLine="RAYLINE",d.PriceLine="PRICELINE",d.ArrowLine="ARROWLINE",d.OpenOrders="OPEN_ORDERS",d.LabelLines="LABELLINES",d.SingleLabelLines="SINGLE_LABEL_LINES",d.Alerts="ALERTS",d.Positions="POSITIONS",d.InformationMines="INFORMATION_MINES",d.ExecutionOrders="EXECUTION_ORDERS",d.SmartMoneySignals="SMART_MONEY_SIGNALS",d.SmartMoneyPositions="SMART_MONEY_POSITIONS",d.Delete="DELETE",d.KeepDrawing="KeepDrawing",d.LockDraw="LOCKDRAW",d.ToggleVisible="TOGGLEVISIBLE",d.Empty="Empty",d.Magnet="Magnet",d.LegacyArrow="ARROW",d.LegacyStraightLine="STRAIGHTLINE",d})({}),V=(function(d){return d.SOLID="solid",d.DASHED="dashed",d.DOTTED="dotted",d.DASHED_2="dashed_2",d})({}),Q=(function(d){return d.BOLD="bold",d.NORMAL="NORMAL",d})({}),ot=(function(d){return d.EDIT="EDIT",d.ADD="ADD",d.REMOVE="REMOVE",d})({})}, +73788(O){function p(i,b,u,g){for(var a=-1,f=i==null?0:i.length;++ae,b:()=>A});const A=f=>({...f}),e=({customSettings:f,widget:g})=>{f&&f.activeChartIndex!==void 0&&g.setActiveChart(f.activeChartIndex)}}, +75853(_,y,e){_=e.nmd(_);var t=e(78942),i=e(34772),r=y&&!y.nodeType&&y,a=r&&!0&&_&&!_.nodeType&&_,s=a&&a.exports===r,n=s?t.Buffer:void 0,o=n?n.isBuffer:void 0,c=o||i;_.exports=c}, +75857(d,g,a){var i=a(90313);function l(s){var u=s==null?0:s.length;return u?i(s,1):[]}d.exports=l}, +75964(_,y,e){var t=e(39747),i=e(74784),r=e(84882);function a(s){return typeof s.constructor=="function"&&!r(s)?t(i(s)):{}}_.exports=a}, +76853(_,y,e){var t=e(69011),i=500;function r(a){var s=t(a,function(o){return n.size===i&&n.clear(),o}),n=s.cache;return s}_.exports=r}, +77135(_,y,e){"use strict";e.d(y,{_:()=>s});var t=e(94889);function i(n,o){var c=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(c!=null){var v=[],g=!0,S=!1,d,A;try{for(c=c.call(n);!(g=(d=c.next()).done)&&(v.push(d.value),!(o&&v.length===o));g=!0);}catch(C){S=!0,A=C}finally{try{!g&&c.return!=null&&c.return()}finally{if(S)throw A}}return v}}var r=e(80686),a=e(61875);function s(n,o){return(0,t._)(n)||i(n,o)||(0,a._)(n,o)||(0,r._)()}}, +77848(_,y,e){var t=e(23945),i=e(74535);function r(a){return t(function(s,n){var o=-1,c=n.length,v=c>1?n[c-1]:void 0,g=c>2?n[2]:void 0;for(v=a.length>3&&typeof v=="function"?(c--,v):void 0,g&&i(n[0],n[1],g)&&(v=c<3?void 0:v,c=1),s=Object(s);++ot});function t(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}}, +80777(O,p,i){var b=i(28486),u=i(80547),g="[object Map]";function a(f){return u(f)&&b(f)==g}O.exports=a}, +80796(_,y,e){var t=e(47403),i=e(3316),r=i(t);_.exports=r}, +81211(_,y,e){var t=e(10358),i=e(195),r=e(36529);function a(s){return r(s)?t(s):i(s)}_.exports=a}, +81935(O,p,i){var b=i(28486),u=i(80547),g="[object Set]";function a(f){return u(f)&&b(f)==g}O.exports=a}, +82226(O,p,i){var b=i(84570);function u(g,a,f,_){return b(g,function(h,T,d){a(_,h,f(h),d)}),_}O.exports=u}, +82438(v,h,e){var t=e(15630);function o(r){return r&&r.length?t(r):[]}v.exports=o}, +82451(_,y,e){var t=e(40091),i=e(77848),r=i(function(a,s,n){t(a,s,n)});_.exports=r}, +82619(_){function y(e){return function(){return e}}_.exports=y}, +83546(O,p,i){var b=i(39032),u=i(77979);function g(a,f){return b(a,u(a),f)}O.exports=g}, +83943(O,p,i){var b=i(81935),u=i(49460),g=i(2306),a=g&&g.isSet,f=a?u(a):b;O.exports=f}, +84354(_){function y(e,t){var i=-1,r=e.length;for(t||(t=Array(r));++im});var m=(function(y){return y.MA="@MA",y.EMA="@EMA",y.WMA="@WMA",y.BOLL="@BOLL",y.VWAP="@VWAP",y.AVL="@AVL",y.TRIX="@TRIX",y.SAR="@SAR",y.SUPERTREND="@SUPERTREND",y.VOL="@VOL",y.MACD="@MACD",y.RSI="@RSI",y.KDJ="@KDJ",y.OBV="@OBV",y.CCI="@CCI",y.StochRSI="@Stoch RSI",y.WR="@Wm %R",y.DMI="@DMI",y.MTM="@MTM",y.EMV="@EMV",y.ATR="@ATR",y.OI_UM="@OI-UM",y.OI_CM="@OI-CM",y.LS_ACCO="@LS-Acco",y.LS_POSIT="@LS-Posit",y.LS_RATIO="@LS-Ratio",y.BS_VOL="@BSVol",y.BASIS="@Basis",y.MFI="@MFI",y.FundingRate="@FundingRate",y})({})}, +89413(_,y,e){var t=e(94715),i=e(78942),r=t(i,"Promise");_.exports=r}, +89624(_,y,e){var t=e(93655),i=e(64759),r=e(41580),a=e(64066),s=/[\\^$.*+?()[\]{}|]/g,n=/^\[object .+?Constructor\]$/,o=Function.prototype,c=Object.prototype,v=o.toString,g=c.hasOwnProperty,S=RegExp("^"+v.call(g).replace(s,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function d(A){if(!r(A)||i(A))return!1;var C=t(A)?S:n;return C.test(a(A))}_.exports=d}, +90313(_,y,e){var t=e(11129),i=e(20714);function r(a,s,n,o,c){var v=-1,g=a.length;for(n||(n=i),c||(c=[]);++v0&&n(S)?s>1?r(S,s-1,n,o,c):t(c,S):o||(c[c.length]=S)}return c}_.exports=r}, +90353(_,y,e){var t=e(51340),i=e(33934),r=e(88861),a=e(31182),s=e(28486),n=e(53142),o=e(75853),c=e(8666),v=1,g="[object Arguments]",S="[object Array]",d="[object Object]",A=Object.prototype,C=A.hasOwnProperty;function N(b,f,p,u,h,l){var E=n(b),m=n(f),D=E?S:s(b),O=m?S:s(f);D=D==g?d:D,O=O==g?d:O;var w=D==d,J=O==d,j=D==O;if(j&&o(b)){if(!o(f))return!1;E=!0,w=!1}if(j&&!w)return l||(l=new t),E||c(b)?i(b,f,p,u,h,l):r(b,f,D,p,u,h,l);if(!(p&v)){var W=w&&C.call(b,"__wrapped__"),V=J&&C.call(f,"__wrapped__");if(W||V){var B=W?b.value():b,H=V?f.value():f;return l||(l=new t),h(B,H,p,u,l)}}return j?(l||(l=new t),a(b,f,p,u,h,l)):!1}_.exports=N}, +92264(_,y,e){var t=e(58898);function i(r,a){var s=a?t(r.buffer):r.buffer;return new r.constructor(s,r.byteOffset,r.length)}_.exports=i}, +92480(O,p,i){var b=i(58898);function u(g,a){var f=a?b(g.buffer):g.buffer;return new g.constructor(f,g.byteOffset,g.byteLength)}O.exports=u}, +93526(_,y,e){var t=e(53142),i=e(65187),r=e(96493),a=e(95243);function s(n,o){return t(n)?n:i(n,o)?[n]:r(a(n))}_.exports=s}, +93650(O,p,i){var b=i(48244),u=i(85832),g=i(25288);function a(f){return b(f,g,u)}O.exports=a}, +93655(_,y,e){var t=e(87379),i=e(41580),r="[object AsyncFunction]",a="[object Function]",s="[object GeneratorFunction]",n="[object Proxy]";function o(c){if(!i(c))return!1;var v=t(c);return v==a||v==s||v==r||v==n}_.exports=o}, +94497(_,y,e){var t=e(94715),i=t(Object,"create");_.exports=i}, +94652(_,y,e){"use strict";e.d(y,{F3:()=>o,GX:()=>c,Lm:()=>s,ZE:()=>n});var t=e(96830),i="chart.spot",r="chart.futures",a="chart.delivery",s="chart.common",n=(0,t.W)()?"chart_eu_futures":"chart_futures",o="chart_delivery",c="chart_shared"}, +94687(_,y,e){var t=e(90353),i=e(80547);function r(a,s,n,o,c){return a===s?!0:a==null||s==null||!i(a)&&!i(s)?a!==a&&s!==s:t(a,s,n,o,r,c)}_.exports=r}, +94715(_,y,e){var t=e(89624),i=e(20155);function r(a,s){var n=i(a,s);return t(n)?n:void 0}_.exports=r}, +94889(_,y,e){"use strict";e.d(y,{_:()=>t});function t(i){if(Array.isArray(i))return i}}, +95243(_,y,e){var t=e(52291);function i(r){return r==null?"":t(r)}_.exports=i}, +95894(_){function y(e){var t=-1,i=Array(e.size);return e.forEach(function(r,a){i[++t]=[a,r]}),i}_.exports=y}, +96493(_,y,e){var t=e(76853),i=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,r=/\\(\\)?/g,a=t(function(s){var n=[];return s.charCodeAt(0)===46&&n.push(""),s.replace(i,function(o,c,v,g){n.push(v?g.replace(r,"$1"):c||o)}),n});_.exports=a}, +96638(_,y,e){var t=e(1386),i=e(19770),r=e(68250),a=200;function s(n,o){var c=this.__data__;if(c instanceof t){var v=c.__data__;if(!i||v.lengtht});var t=function(){var i=arguments.length>0&&arguments[0]!==void 0?arguments[0]:window.location.pathname,r=i.includes("/eu-futures");return r}}, +97034(_,y,e){var t=e(46285);function i(r,a){for(var s=r.length;s--;)if(t(r[s][0],a))return s;return-1}_.exports=i}, +97350(O){var p=Object.prototype,i=p.hasOwnProperty;function b(u){var g=u.length,a=new u.constructor(g);return g&&typeof u[0]=="string"&&i.call(u,"index")&&(a.index=u.index,a.input=u.input),a}O.exports=b}, +97706(v){function h(e,t,o){for(var r=o-1,n=e.length;++r/^(LineTool)/g.test(t); +const _t=t=>`${t}.customSettings`; +const ft=t=>`${t}.layout`; +const J=t=>t.chartId; +const kt=({save:t})=>{const n={},s=[],i=t.charts.reduce((I,N)=>{const _=N.panes.reduce((m,y)=>{const R=y.sources.find(h=>h.type==="MainSeries");R?.state.symbol&&(n[R?.state.symbol.toUpperCase()]=[]);const[C,Y]=it()(y.sources,h=>W(h.type));return s.push(...C),m.push({...y,sources:Y}),m},[]);return I.push({...N,panes:_}),I},[]),l=E()(s,I=>I.id),u=j()(l,I=>I.state.symbol),d={...n,...u};return{charts:i,drawings:d}}; +const wt=async({storage:t,key:n,charts:s})=>{const i=ft(n),l=await t.getItem(i)||[],u=Q()(l,J),d=Q()(s,J),I=l.map(J),N=s.map(J),y=ut()([...I,...N]).map(R=>d[R]||u[R]).sort((R,C)=>+R.chartId-+C.chartId);return await t.setItem(i,y),y}; +const xt=async({storage:t,save:n,key:s,widget:i})=>{if(!i)return{chartSave:n,drawingsSave:{}};const{drawings:l,charts:u}=kt({save:n}),d={...n,charts:u};return await t.setItem(s,d),await wt({storage:t,key:s,charts:u}),await Promise.all(Object.keys(l).map(I=>t.setItem(`#TV_SYMBOL-${I}`,l[I]))),{chartSave:d,drawingsSave:l}}; +const Wt=async({storage:t,key:n,widget:s})=>{if(!s)return(0,O.b)({activeChartIndex:0});const i=_t(n),l=s.activeChartIndex(),u=(0,O.b)({activeChartIndex:l});return await t.setItem(i,u),u}; +const zt=async({storage:t,key:n})=>{const l=(await t.keys()).filter(h=>/^(#TV_SYMBOL-)/g.test(h)).map(async h=>t.getItem(h)),u=await Promise.all(l),d=rt()(u).filter(h=>!!h),I=j()(d,h=>h.ownerSource),N=ft(n),_=_t(n),m=await t.getItem(n),y=await t.getItem(_);if(!m)return{chartSave:m,customSettings:y};const C=(await t.getItem(N)||m.charts).reduce((h,H)=>{const V=H.panes.reduce((D,M)=>{const z=ot()(M.sources),q=I[M.mainSourceId]||[];return z.push(...q),D.push({...M,sources:z}),D},[]);return h.push({...H,panes:V}),h},[]);return{chartSave:{...m,charts:C},customSettings:y}}; +r.d(exports,{save:()=>xt,load:()=>zt,saveCustomSettings:()=>Wt,createSaveCallback:()=>createSaveCallback}); +function createSaveCallback(storage,key,widget,onSave){const et={current:storage},st={current:key},p={current:widget},t={initialConfig:{onSave}};return async c=>{const{chartSave:L}=await xt({storage:et.current,save:c,key:st.current,widget:p.current});await t.initialConfig.onSave?.(L)};} +}, +"codex-chart-host-basic"(module,exports,a){ +var ot=a(46108); +var d=a.n(ot); +var A=a(22053); +var R=a.n(A); +var Tt=a(89220); +var vt=a(72834); +const lo=e=>e===null||e.length===0?null:e.length===1?e[0]:e.length===2?{p1:e[0],p2:e[1]}:{p1:e[0],p2:e[1],p3:e[2]}; +var Sa=a(79071); +var Dn=a.n(Sa); +var xc=a(43917); +var bc=a.n(xc); +var Sc=a(34789); +var Do=a.n(Sc); +var Ac=a(46155); +var vc=a.n(Ac); +var Cc=a(82451); +var Qi=a.n(Cc); +const Vo=[{id:"CANDLE",indicators:[{id:Tt.w.MA,categories:["main"]}]},{id:Tt.w.VOL,indicators:[{id:Tt.w.VOL,categories:["sub"]}]}]; +const Mo=()=>"candlestick-indicator-setting"; +const wc=()=>"candlestick-setting"; +const No=()=>"BASIC_KLINE_INDICATOR"; +const Eo=()=>"BASIC_KLINE_COMPARE_SYMBOL_SETTINGS"; +const qi="BASIC_KLINE_"; +const Rn=e=>`${qi}${e}`; +const Tc=()=>"original-candlestick"; +const Bo=e=>e.startsWith(qi)&&e.slice(qi.length).trim().startsWith("{"); +function Ro(e){if(!e.trim().startsWith("{"))return null;try{const t=JSON.parse(e);if(!t||typeof t!="object"||Array.isArray(t))return null;const s=t.pair,i=t.contractType;if(s!=null&&i!=null)return String(i).toUpperCase()==="PERPETUAL"?String(s):`${s}_${i}`;const o=t.base,l=t.quote;return o!=null&&l!=null?`${o}${l}`:null}catch{return null}} +async function Lc(e){const t=await e.keys();for(const s of t){if(!Bo(s))continue;const i=Ro(s.slice(qi.length));if(i==null)continue;const o=Rn(i),l=await e.getItem(s);l!=null&&await e.setItem(o,l)}} +const Ic=({type:e,lineType:t,dir:s})=>{switch(e){case vt.X2.LegacyArrow:return s||vt.X2.LeftArrow;case vt.X2.LegacyStraightLine:return t||vt.X2.HorizontalLine;default:return e}}; +const _o=e=>{const t=d()(e);for(const i of t??[])i.id=i.id??i.type;const s=t?.find(i=>(i.id??i.type)==="CANDLE");if(s){const i=s.annotations.map(({type:o=vt.X2.Empty,lineType:l,dir:u,...f})=>({type:Ic({type:o,lineType:l,dir:u}),...f}));s.annotations=i}return t}; +const ko={toStored:R(),fromStored:R()}; +const Pc=(e,t,s)=>{const i=o=>({...o,time:s(o.time,t)});return e==null?e:Array.isArray(e)?e.map(o=>o&&i(o)):"time"in e?i(e):vc()(e,o=>o&&i(o))}; +const Ho=(e,t,s)=>e.map(i=>({...i,annotations:i.annotations.map(o=>({...o,data:Pc(o.data,t,s)}))})); +const Wo=({type:e,namespace:t,symbol:s,saveIndicators:i,saveIndicatorSettings:o,saveAnnotations:l,saveSettings:u,saveAnnotationConfig:f,saveCompareSymbolSettings:S,load:x,clear:_})=>{const $={type:e,symbol:s};if(!t)return{info:$,saveAnnotations:()=>Promise.resolve(),saveIndicators:()=>Promise.resolve(),saveIndicatorSettings:()=>Promise.resolve(),saveSettings:()=>Promise.resolve(),saveAnnotationConfig:()=>Promise.resolve(),saveCompareSymbolSettings:()=>Promise.resolve(),load:()=>Promise.resolve({storeData:[],indicatorSetting:null,setting:null,annotationConfig:null,compareSymbolSettings:null}),clear:Dn()};const tt={indicatorStoreKey:No(),annotationsStoreKey:Rn(s),indicatorSettingStoreKey:Mo(),settingStoreKey:wc(),annotationConfigStoreKey:Tc(),compareSymbolSettingsStoreKey:Eo()},ht=bc().createInstance({name:t});return{info:$,saveAnnotations:async(gt,{interval:Mt,annotationTime:Ut=ko})=>{const _t=Ho(gt,Mt,Ut.toStored);await l({...tt,payload:_t,storage:ht,interval:Mt})},saveIndicators:async gt=>{await i({...tt,payload:gt,storage:ht})},saveIndicatorSettings:async gt=>{await o({...tt,payload:gt,storage:ht})},saveSettings:async gt=>{await u({...tt,payload:gt,storage:ht})},saveAnnotationConfig:async gt=>{await f({...tt,payload:gt,storage:ht})},saveCompareSymbolSettings:async gt=>{await S({...tt,payload:gt,storage:ht})},load:async({interval:gt,annotationTime:Mt=ko})=>{try{const Ut=await x({...tt,storage:ht,interval:gt}),{indicators:_t,annotations:zt,indicatorSetting:Gt,setting:ae,annotationConfig:ge,compareSymbolSettings:de}=Ut,Ee=zt&&Ho(zt,gt,Mt.fromStored),Ue=_t||Vo,Ne=Ee||[],ve=Do()(Ue,ts=>ts.id||ts.type),Ie=Do()(Ne,ts=>ts.id||ts.type),je=Qi()({},ve,Ie);return{storeData:Object.values(je),indicatorSetting:Gt,setting:ae,annotationConfig:ge,compareSymbolSettings:de}}catch(Ut){return console.warn("[candlestick] load saving error: ",Ut),{storeData:[],indicatorSetting:null,setting:null,annotationConfig:null,compareSymbolSettings:null}}},clear:()=>_({storage:ht})}}; +const Dc=({namespace:e,symbol:t})=>Wo({type:"local",namespace:e,symbol:t,saveAnnotations:async({payload:s,annotationsStoreKey:i,storage:o})=>{const l=s.map(u=>({...u,annotations:u.annotations.map(f=>({...f,data:lo(f.data)}))}));await o.setItem(i,l)},saveIndicators:async({payload:s,indicatorStoreKey:i,storage:o})=>{await o.setItem(i,s)},saveIndicatorSettings:async({payload:s,indicatorSettingStoreKey:i,storage:o})=>{await o.setItem(i,s)},saveSettings:async({payload:s,settingStoreKey:i,storage:o})=>{const{"@colorTable":l,"@watermark":u,...f}=s;await o.setItem(i,f)},saveAnnotationConfig:async({payload:s,annotationConfigStoreKey:i,storage:o})=>{await o.setItem(i,s)},saveCompareSymbolSettings:async({payload:s,compareSymbolSettingsStoreKey:i,storage:o})=>{await o.setItem(i,s)},load:async({storage:s,indicatorStoreKey:i,annotationsStoreKey:o,indicatorSettingStoreKey:l,settingStoreKey:u,annotationConfigStoreKey:f,compareSymbolSettingsStoreKey:S})=>{let x=await s.getItem(o);x==null&&(await Lc(s),x=await s.getItem(o));const[_,$,tt,ht,gt]=await Promise.all([s.getItem(i),s.getItem(l),s.getItem(u),s.getItem(f),s.getItem(S)]),Mt=_o(x||[]);return{indicators:_,annotations:Mt,indicatorSetting:$,setting:tt,annotationConfig:ht,compareSymbolSettings:gt}},clear:({storage:s})=>s.clear()}); +a.d(exports,{create:()=>Dc}); +}, +"codex-chart-host-mirror"(module,exports,e){ +var o=e(49253); +var v=e(77135); +var i=e(42288); +var M=e(43917); +var h=e.n(M); +var V=e(7980); +var U=e.n(V); +var Qe=e(94652); +var Tn=[Qe.ZE,Qe.F3]; +e.d(exports,{create:()=>createMirror}); +function createMirror(namespace){const a=namespace,Cr={current:false},_r={current:true};return (0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})()),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er=="string"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})});} +}}]); diff --git a/test/fixtures/binance-chart-storage/host-persistence.md b/test/fixtures/binance-chart-storage/host-persistence.md new file mode 100644 index 0000000..93a4ce5 --- /dev/null +++ b/test/fixtures/binance-chart-storage/host-persistence.md @@ -0,0 +1,70 @@ +# Public host persistence fixture + +`host-persistence.js` executes the original Binance storage business functions +through the existing captured Rspack runtime and localForage module. It does not +reimplement lodash, the chart save algorithm, Basic storage, or mirroring. + +`host-persistence-manifest.json` records each complete source file's public URL, +SHA-256 and byte count. Every copied declaration, callback and dependency factory +has its original UTF-16 range, the corresponding generated-fixture range and the +SHA-256 of the exact UTF-8-encoded slice. The generated registration wrapper and +each added `var`/`const` declaration delimiter sit outside those original ranges. +The browser helper checks the whole fixture and all fragment hashes before use. + +The source inputs were obtained on 2026-10-06. The original investigation supplied +`main.37a180b9.js`, `28928.9aeec892.js`, `36648.dd8ae7dc.js` and +`TradingView.99bc5074.js`. A separate anonymous Chrome visit to the public USUSDT +page identified the missing lodash factories in chunks `40784`, `45485`, `16873` +and `56679`. Only those public JS responses were downloaded; extraction retains +only the required factories. No login, existing browser profile, storage values +or trading actions were used. Both anonymous browsers were closed in `finally`. +The current page selected Basic by default; selecting Trading View was unnecessary +to obtain these dependencies. A successful module observation is not UI evidence. + +Regenerate from directories containing these public response bodies: + +```sh +node test/fixtures/binance-chart-storage/host-persistence-extract.js \ + /path/to/original-public-chunks /path/to/additional-public-chunks +``` + +Acorn selects the host functions and recursively collects their original lexical +declarations. The extractor then collects original webpack dependency factories. +Missing declarations or dependencies are errors; no replacement implementation is +inserted. It does not download sources or read a browser profile. + +## Executed contracts + +- TradingView module `76535`: `kt`, `wt`, `xt`, `zt`, `Wt`, key helpers and the + original `Tt` save callback. The test supplies storage, key, widget and onSave + references to that unchanged callback. `onSave` is the original mirror callback. +- Basic module `15426`: `Wo`, `Dc`, default indicator data, key constructors, + annotations conversion and the actual `Lc` old-key migration. Tests supply JSON + data at this storage boundary. The upstream `Ec` React subscriptions, debounce, + loading guard and JSON serialization are not executed by this fixture. +- TradingView mirror: the original compiled async callback, original SWC helpers, + namespace constants and original lodash dependencies. The wrapper supplies the + requested namespace and native enabled-state refs (`Cr.current=false`, + `_r.current=true`). It does not simulate React lifecycle changes to those refs. +- The widget boundary implements only the `activeChartIndex()` call used by these + storage functions. Chart rendering and the full TradingView widget are absent. + +All scenarios run independently against native localForage and the adapter, using +fresh Chromium contexts, the same original functions and real IndexedDB. They +cover staged saves, the cross-database mirror, layout retention, restored drawings, +removing a symbol's last drawing, all six Basic keys, old-key migration and shared +store clearing. The save/mirror measurement excludes setup and final verification. +It measures transaction/request counts, not wall-clock speed or blank-page recovery. + +The concurrent clear scenario records a real behavioral difference. In the +captured localForage IndexedDB driver, `setItem` creates its transaction in a +second `ready().then(...).then(...)` stage, while `clear` creates its transaction +in the first `ready().then(...)` stage. Simultaneously invoked sets can therefore +commit after a later-invoked clear. The adapter instead enforces issue order. +The shared-clear conformance scenario awaits existing saves before clearing; +the separate concurrency scenario explicitly asserts each implementation's +different result. Neither result establishes atomic behavior across databases. + +These offline tests do not establish production injection, existing-profile data +compatibility, UI rendering, safety of stopping mounted callers, fallback-driver +behavior, cross-tab lost-update protection or cross-database atomicity. diff --git a/test/fixtures/binance-chart-storage/localforage-module.js b/test/fixtures/binance-chart-storage/localforage-module.js new file mode 100644 index 0000000..15e179c --- /dev/null +++ b/test/fixtures/binance-chart-storage/localforage-module.js @@ -0,0 +1,10 @@ +/* Test-only extraction from Binance main.37a180b9.js; see manifest.json. */ +(self.webpackChunkfutures_trade_ui = self.webpackChunkfutures_trade_ui || []).push([ + ["codex-chart-storage-fixture"], + {43917(_,y,e){/*! + localForage -- Offline Storage, Improved + Version 1.10.0 + https://localforage.github.io/localForage + (c) 2013-2017 Mozilla, Apache License 2.0 +*/(function(t){if(1)_.exports=t();else var i})(function(){var t,i,r;return(function a(s,n,o){function c(S,d){if(!n[S]){if(!s[S]){var A=void 0;if(!d&&A)return require(S,!0);if(v)return v(S,!0);var C=new Error("Cannot find module '"+S+"'");throw C.code="MODULE_NOT_FOUND",C}var N=n[S]={exports:{}};s[S][0].call(N.exports,function(b){var f=s[S][1][b];return c(f||b)},N,N.exports,a,s,n,o)}return n[S].exports}for(var v=void 0,g=0;g"u"&&a(3);var A=Promise;function C(pe,He){He&&pe.then(function(Ae){He(null,Ae)},function(Ae){He(Ae)})}function N(pe,He,Ae){typeof He=="function"&&pe.then(He),typeof Ae=="function"&&pe.catch(Ae)}function b(pe){return typeof pe!="string"&&(console.warn(pe+" used as a key, but it is not a string."),pe=String(pe)),pe}function f(){if(arguments.length&&typeof arguments[arguments.length-1]=="function")return arguments[arguments.length-1]}var p="local-forage-detect-blob-support",u=void 0,h={},l=Object.prototype.toString,E="readonly",m="readwrite";function D(pe){for(var He=pe.length,Ae=new ArrayBuffer(He),Je=new Uint8Array(Ae),Ze=0;Ze=43)}}).catch(function(){return!1})}function w(pe){return typeof u=="boolean"?A.resolve(u):O(pe).then(function(He){return u=He,u})}function J(pe){var He=h[pe.name],Ae={};Ae.promise=new A(function(Je,Ze){Ae.resolve=Je,Ae.reject=Ze}),He.deferredOperations.push(Ae),He.dbReady?He.dbReady=He.dbReady.then(function(){return Ae.promise}):He.dbReady=Ae.promise}function j(pe){var He=h[pe.name],Ae=He.deferredOperations.pop();if(Ae)return Ae.resolve(),Ae.promise}function W(pe,He){var Ae=h[pe.name],Je=Ae.deferredOperations.pop();if(Je)return Je.reject(He),Je.promise}function V(pe,He){return new A(function(Ae,Je){if(h[pe.name]=h[pe.name]||z(),pe.db)if(He)J(pe),pe.db.close();else return Ae(pe.db);var Ze=[pe.name];He&&Ze.push(pe.version);var Ke=g.open.apply(g,Ze);He&&(Ke.onupgradeneeded=function(dt){var Rt=Ke.result;try{Rt.createObjectStore(pe.storeName),dt.oldVersion<=1&&Rt.createObjectStore(p)}catch(Lt){if(Lt.name==="ConstraintError")console.warn('The database "'+pe.name+'" has been upgraded from version '+dt.oldVersion+" to version "+dt.newVersion+', but the storage "'+pe.storeName+'" already exists.');else throw Lt}}),Ke.onerror=function(dt){dt.preventDefault(),Je(Ke.error)},Ke.onsuccess=function(){var dt=Ke.result;dt.onversionchange=function(Rt){Rt.target.close()},Ae(dt),j(pe)}})}function B(pe){return V(pe,!1)}function H(pe){return V(pe,!0)}function Z(pe,He){if(!pe.db)return!0;var Ae=!pe.db.objectStoreNames.contains(pe.storeName),Je=pe.versionpe.db.version;if(Je&&(pe.version!==He&&console.warn('The database "'+pe.name+`" can't be downgraded from version `+pe.db.version+" to version "+pe.version+"."),pe.version=pe.db.version),Ze||Ae){if(Ae){var Ke=pe.db.version+1;Ke>pe.version&&(pe.version=Ke)}return!0}return!1}function Te(pe){return new A(function(He,Ae){var Je=new FileReader;Je.onerror=Ae,Je.onloadend=function(Ze){var Ke=btoa(Ze.target.result||"");He({__local_forage_encoded_blob:!0,data:Ke,type:pe.type})},Je.readAsBinaryString(pe)})}function ve(pe){var He=D(atob(pe.data));return d([He],{type:pe.type})}function le(pe){return pe&&pe.__local_forage_encoded_blob}function Ce(pe){var He=this,Ae=He._initReady().then(function(){var Je=h[He._dbInfo.name];if(Je&&Je.dbReady)return Je.dbReady});return N(Ae,pe,pe),Ae}function se(pe){J(pe);for(var He=h[pe.name],Ae=He.forages,Je=0;Je0&&(!pe.db||Ke.name==="InvalidStateError"||Ke.name==="NotFoundError"))return A.resolve().then(function(){if(!pe.db||Ke.name==="NotFoundError"&&!pe.db.objectStoreNames.contains(pe.storeName)&&pe.version<=pe.db.version)return pe.db&&(pe.version=pe.db.version+1),H(pe)}).then(function(){return se(pe).then(function(){ge(pe,He,Ae,Je-1)})}).catch(Ae);Ae(Ke)}}function z(){return{forages:[],db:null,dbReady:null,deferredOperations:[]}}function Le(pe){var He=this,Ae={db:null};if(pe)for(var Je in pe)Ae[Je]=pe[Je];var Ze=h[Ae.name];Ze||(Ze=z(),h[Ae.name]=Ze),Ze.forages.push(He),He._initReady||(He._initReady=He.ready,He.ready=Ce);var Ke=[];function dt(){return A.resolve()}for(var Rt=0;Rt>4,wt[Ze++]=(dt&15)<<4|Rt>>2,wt[Ze++]=(Rt&3)<<6|Lt&63;return _t}function fe(pe){var He=new Uint8Array(pe),Ae="",Je;for(Je=0;Je>2],Ae+=G[(He[Je]&3)<<4|He[Je+1]>>4],Ae+=G[(He[Je+1]&15)<<2|He[Je+2]>>6],Ae+=G[He[Je+2]&63];return He.length%3===2?Ae=Ae.substring(0,Ae.length-1)+"=":He.length%3===1&&(Ae=Ae.substring(0,Ae.length-2)+"=="),Ae}function je(pe,He){var Ae="";if(pe&&(Ae=_e.call(pe)),pe&&(Ae==="[object ArrayBuffer]"||pe.buffer&&_e.call(pe.buffer)==="[object ArrayBuffer]")){var Je,Ze=x;pe instanceof ArrayBuffer?(Je=pe,Ze+=ce):(Je=pe.buffer,Ae==="[object Int8Array]"?Ze+=we:Ae==="[object Uint8Array]"?Ze+=Be:Ae==="[object Uint8ClampedArray]"?Ze+=qe:Ae==="[object Int16Array]"?Ze+=rt:Ae==="[object Uint16Array]"?Ze+=q:Ae==="[object Int32Array]"?Ze+=ze:Ae==="[object Uint32Array]"?Ze+=de:Ae==="[object Float32Array]"?Ze+=ae:Ae==="[object Float64Array]"?Ze+=ee:He(new Error("Failed to get type for BinaryArray"))),He(Ze+fe(Je))}else if(Ae==="[object Blob]"){var Ke=new FileReader;Ke.onload=function(){var dt=R+pe.type+"~"+fe(this.result);He(x+Ee+dt)},Ke.readAsArrayBuffer(pe)}else try{He(JSON.stringify(pe))}catch(dt){console.error("Couldn't convert value into a JSON string: ",pe),He(null,dt)}}function Ve(pe){if(pe.substring(0,X)!==x)return JSON.parse(pe);var He=pe.substring(ye),Ae=pe.substring(X,ye),Je;if(Ae===Ee&&U.test(He)){var Ze=He.match(U);Je=Ze[1],He=He.substring(Ze[0].length)}var Ke=Fe(He);switch(Ae){case ce:return Ke;case Ee:return d([Ke],{type:Je});case we:return new Int8Array(Ke);case Be:return new Uint8Array(Ke);case qe:return new Uint8ClampedArray(Ke);case rt:return new Int16Array(Ke);case q:return new Uint16Array(Ke);case ze:return new Int32Array(Ke);case de:return new Uint32Array(Ke);case ae:return new Float32Array(Ke);case ee:return new Float64Array(Ke);default:throw new Error("Unkown type: "+Ae)}}var $e={serialize:je,deserialize:Ve,stringToBuffer:Fe,bufferToString:fe};function nt(pe,He,Ae,Je){pe.executeSql("CREATE TABLE IF NOT EXISTS "+He.storeName+" (id INTEGER PRIMARY KEY, key unique, value)",[],Ae,Je)}function Et(pe){var He=this,Ae={db:null};if(pe)for(var Je in pe)Ae[Je]=typeof pe[Je]!="string"?pe[Je].toString():pe[Je];var Ze=new A(function(Ke,dt){try{Ae.db=openDatabase(Ae.name,String(Ae.version),Ae.description,Ae.size)}catch(Rt){return dt(Rt)}Ae.db.transaction(function(Rt){nt(Rt,Ae,function(){He._dbInfo=Ae,Ke()},function(Lt,_t){dt(_t)})},dt)});return Ae.serializer=$e,Ze}function ht(pe,He,Ae,Je,Ze,Ke){pe.executeSql(Ae,Je,Ze,function(dt,Rt){Rt.code===Rt.SYNTAX_ERR?dt.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name = ?",[He.storeName],function(Lt,_t){_t.rows.length?Ke(Lt,Rt):nt(Lt,He,function(){Lt.executeSql(Ae,Je,Ze,Ke)},Ke)},Ke):Ke(dt,Rt)},Ke)}function ke(pe,He){var Ae=this;pe=b(pe);var Je=new A(function(Ze,Ke){Ae.ready().then(function(){var dt=Ae._dbInfo;dt.db.transaction(function(Rt){ht(Rt,dt,"SELECT * FROM "+dt.storeName+" WHERE key = ? LIMIT 1",[pe],function(Lt,_t){var wt=_t.rows.length?_t.rows.item(0).value:null;wt&&(wt=dt.serializer.deserialize(wt)),Ze(wt)},function(Lt,_t){Ke(_t)})})}).catch(Ke)});return C(Je,He),Je}function et(pe,He){var Ae=this,Je=new A(function(Ze,Ke){Ae.ready().then(function(){var dt=Ae._dbInfo;dt.db.transaction(function(Rt){ht(Rt,dt,"SELECT * FROM "+dt.storeName,[],function(Lt,_t){for(var wt=_t.rows,zt=wt.length,fr=0;fr0){dt(at.apply(Ze,[pe,Lt,Ae,Je-1]));return}Rt(fr)}})})}).catch(Rt)});return C(Ke,Ae),Ke}function it(pe,He,Ae){return at.apply(this,[pe,He,Ae,1])}function Ot(pe,He){var Ae=this;pe=b(pe);var Je=new A(function(Ze,Ke){Ae.ready().then(function(){var dt=Ae._dbInfo;dt.db.transaction(function(Rt){ht(Rt,dt,"DELETE FROM "+dt.storeName+" WHERE key = ?",[pe],function(){Ze()},function(Lt,_t){Ke(_t)})})}).catch(Ke)});return C(Je,He),Je}function yt(pe){var He=this,Ae=new A(function(Je,Ze){He.ready().then(function(){var Ke=He._dbInfo;Ke.db.transaction(function(dt){ht(dt,Ke,"DELETE FROM "+Ke.storeName,[],function(){Je()},function(Rt,Lt){Ze(Lt)})})}).catch(Ze)});return C(Ae,pe),Ae}function gt(pe){var He=this,Ae=new A(function(Je,Ze){He.ready().then(function(){var Ke=He._dbInfo;Ke.db.transaction(function(dt){ht(dt,Ke,"SELECT COUNT(key) as c FROM "+Ke.storeName,[],function(Rt,Lt){var _t=Lt.rows.item(0).c;Je(_t)},function(Rt,Lt){Ze(Lt)})})}).catch(Ze)});return C(Ae,pe),Ae}function Tt(pe,He){var Ae=this,Je=new A(function(Ze,Ke){Ae.ready().then(function(){var dt=Ae._dbInfo;dt.db.transaction(function(Rt){ht(Rt,dt,"SELECT key FROM "+dt.storeName+" WHERE id = ? LIMIT 1",[pe+1],function(Lt,_t){var wt=_t.rows.length?_t.rows.item(0).key:null;Ze(wt)},function(Lt,_t){Ke(_t)})})}).catch(Ke)});return C(Je,He),Je}function ct(pe){var He=this,Ae=new A(function(Je,Ze){He.ready().then(function(){var Ke=He._dbInfo;Ke.db.transaction(function(dt){ht(dt,Ke,"SELECT key FROM "+Ke.storeName,[],function(Rt,Lt){for(var _t=[],wt=0;wt '__WebKitDatabaseInfoTable__'",[],function(Ze,Ke){for(var dt=[],Rt=0;Rt0}function gr(pe){var He=this,Ae={};if(pe)for(var Je in pe)Ae[Je]=pe[Je];return Ae.keyPrefix=Gt(pe,He._defaultConfig),cr()?(He._dbInfo=Ae,Ae.serializer=$e,A.resolve()):A.reject()}function sr(pe){var He=this,Ae=He.ready().then(function(){for(var Je=He._dbInfo.keyPrefix,Ze=localStorage.length-1;Ze>=0;Ze--){var Ke=localStorage.key(Ze);Ke.indexOf(Je)===0&&localStorage.removeItem(Ke)}});return C(Ae,pe),Ae}function lr(pe,He){var Ae=this;pe=b(pe);var Je=Ae.ready().then(function(){var Ze=Ae._dbInfo,Ke=localStorage.getItem(Ze.keyPrefix+pe);return Ke&&(Ke=Ze.serializer.deserialize(Ke)),Ke});return C(Je,He),Je}function hr(pe,He){var Ae=this,Je=Ae.ready().then(function(){for(var Ze=Ae._dbInfo,Ke=Ze.keyPrefix,dt=Ke.length,Rt=localStorage.length,Lt=1,_t=0;_t=0;dt--){var Rt=localStorage.key(dt);Rt.indexOf(Ke)===0&&localStorage.removeItem(Rt)}}):Ze=A.reject("Invalid arguments"),C(Ze,He),Ze}var Cr={_driver:"localStorageWrapper",_initStorage:gr,_support:Vt(),iterate:hr,getItem:lr,setItem:Yt,removeItem:br,clear:sr,length:Pr,key:nr,keys:pr,dropInstance:Nr},jr=function(He,Ae){return He===Ae||typeof He=="number"&&typeof Ae=="number"&&isNaN(He)&&isNaN(Ae)},xt=function(He,Ae){for(var Je=He.length,Ze=0;Ze"u"?"undefined":o(Ae))==="object"){if(this._ready)return new Error("Can't call config() after localforage has been used.");for(var Je in Ae){if(Je==="storeName"&&(Ae[Je]=Ae[Je].replace(/\W/g,"_")),Je==="version"&&typeof Ae[Je]!="number")return new Error("Database version must be a number.");this._config[Je]=Ae[Je]}return"driver"in Ae&&Ae.driver?this.setDriver(this._config.driver):!0}else return typeof Ae=="string"?this._config[Ae]:this._config},pe.prototype.defineDriver=function(Ae,Je,Ze){var Ke=new A(function(dt,Rt){try{var Lt=Ae._driver,_t=new Error("Custom driver not compliant; see https://mozilla.github.io/localForage/#definedriver");if(!Ae._driver){Rt(_t);return}for(var wt=Er.concat("_initStorage"),zt=0,fr=wt.length;ztDt,default:()=>Cn});var o=e(49253),v=e(77135),d=e(75510),i=e(42288),p=e(31085),r=e(41594),M=e(43917),h=e.n(M),P=e(81936),b=e.n(P),V=e(7980),U=e.n(V),D=e(76469),c=e(4260),k=e(53837),B=e(27571),C=e(45250),y=e(87646),A=e.n(y),q=e(80817);const E=(0,C.range)(1,1380),m=(0,C.range)(1,365).map(t=>`${t}D`),R=(0,C.range)(1,52).map(t=>`${t}W`),I=(0,C.range)(1,12).map(t=>`${t}M`),F=["1S",...E,...m,...R,...I],L=({symbol:t,params:n})=>{const a=new URLSearchParams(n);return`${t}@${a.toString()}`},X=t=>{const[n,a]=t.split("@"),s=new URLSearchParams(a).entries(),g=A()(Array.from(s));return{symbol:n,params:g}},ue=()=>{const t=new Map;let n={},a=[],u={onReady:()=>null,resolveSymbol:()=>null,searchSymbols:()=>null,getBars:()=>null,subscribeBars:()=>null,unsubscribeBars:()=>null};return({config:s,onSymbolResolving:g,onSymbolResolved:S,onFetchSymbolsInfo:K,onFetchBars:Y,onSubscribeBars:H,onUnsubscribeBars:j,onFetchServerTimeApi:$,onTransformSymbolInfo:f=({template:T})=>T,onSearchSymbols:l=()=>!0})=>(u={onReady:T=>{setTimeout(async()=>{const _=(await K()).map(O=>({...O,ticker:L({symbol:O.symbol,params:O.params||{}})}));a=_.map(({ticker:O,symbol:G,description:te="",exchange:J="",type:oe=""})=>({symbol:G,ticker:O,description:te,exchange:J,type:oe})),n=(0,C.keyBy)(_,"ticker"),T({exchanges:[],symbols_types:[],supported_resolutions:F,supports_marks:!1,supports_timescale_marks:!1,supports_time:!0,...s})})},resolveSymbol:async(T,W,_)=>{const O=n[T];if(!O){_(`cannot resolve symbol - ${T}`);return}const G=f({info:O,template:{description:O.description||"",fractional:!1,has_seconds:!0,seconds_multipliers:["1"],has_intraday:!0,intraday_multipliers:["1","3","5","15","30","60","120","240","360","480","720"],has_daily:!0,daily_multipliers:["1"],has_weekly_and_monthly:!0,weekly_multipliers:["1"],monthly_multipliers:["1"],minmov:1,minmove2:0,name:O.symbol,ticker:O.ticker,timezone:"Etc/UTC",pricescale:O.tickSize,session:"24x7",type:O.type||"",exchange:O.exchange||"",listed_exchange:O.exchange||"",format:"price",supported_resolutions:F,visible_plots_set:"ohlcv",volume_precision:3}});if(g){const te=X(G.ticker||"");if(!await g({symbol:T,info:G,tickerInfo:te})){_(`symbol resolving error - ${T}`);return}}setTimeout(()=>{W(G),S?.(G)})},searchSymbols:(T,W,_,O)=>{const G=new RegExp(T,"i"),te=new RegExp(W,"i"),J=new RegExp(_,"i"),oe=a.filter(Te=>{const{symbol:N,ticker:w,description:re="",exchange:ie="",type:le=""}=Te;return[N,w,re].some(de=>G.test(de||""))&&te.test(ie)&&J.test(le)&&l?.({item:Te})});O(oe)},getBars:async(T,W,_,O,G)=>{const{from:te,to:J,firstDataRequest:oe}=_,Te=(0,q.Cz)(W);try{if(J>=0){const N=X(T.ticker||""),w=await Y({symbolInfo:T,resolution:W,rangeStartDate:te,rangeEndDate:J,tickerInfo:N,binanceInterval:Te,firstDataRequest:oe});O(w,{noData:w.length===0})}else O([],{noData:!0})}catch(N){G(N.toString())}},subscribeBars:(T,W,_,O,G)=>{const te=X(T.ticker||""),J=(0,q.Cz)(W),oe=H({symbolInfo:T,resolution:W,callback:_,onResetCacheNeededCallback:G,tickerInfo:te,binanceInterval:J});t.set(O,oe)},unsubscribeBars:T=>{t?.get(T)?.(),t?.delete(T),j?.(T)},getServerTime:async T=>{try{const W=await $();T(W)}catch(W){console.warn(W),T(Date.now()/1e3)}}},{datafeed:u,observers:t,symbolsInfo:n})},Le=({namespace:t,type:n})=>{const a=(0,r.useMemo)(()=>h().createInstance({name:t}),[t]);return(0,r.useCallback)(()=>{n==="tv"&&a.removeItem("myTradingView")},[a,n])};var Ue=e(76535),pr=e(26531),Ye=e(17409),Qe=e(94652),Ae=e(76830),he=e(48651),Fe=e(79344),se=e(51029),Je=e(36307),or=e(25379),yr=e(44745),Ne=e(15727),rr=e(45358),sr=e(82071),Ve=e(85411),ge=e(94889),hr=e(94547),tr=e(80686),Ar=e(61875);function Fr(t){return(0,ge._)(t)||(0,hr._)(t)||(0,Ar._)(t)||(0,tr._)()}var Nr=e(92873),zr=e(35630),Vr=e(12055),Qr=e(79515),Br=e(80737),Jr=e(40545),Zr=e(98107),Kr=e(51846),Xr="Chart.Crosshair.PlusButton.DrawHorizontalLine",wr=function(t){var n=t.namespace,a=t.symbol,u=t.priceType,s=t.crosshairPrice,g=(0,Nr.o)("","kline-ui").getI18n,S=(0,Vr.r)().format,K=k.y$(n),Y=(0,Br.S)({namespace:n}),H=(0,Jr.hw)(),j=(0,Zr.c)(),$=(0,Qr.nH)(),f=(0,se.Ye)(),l=(0,Kr.E)({symbol:a}),T=(0,Fe.f0)(a),W=T.isCM,_=(0,r.useRef)(s);(0,r.useEffect)(function(){_.current=s},[s]);var O=(0,r.useRef)(H);(0,r.useEffect)(function(){O.current=H},[H]);var G=(0,r.useRef)(j);(0,r.useEffect)(function(){G.current=j},[j]);var te=(0,r.useRef)(u);(0,r.useEffect)(function(){te.current=u},[u]);var J=(0,r.useRef)($);(0,r.useEffect)(function(){J.current=$},[$]);var oe=(0,r.useRef)(f);(0,r.useEffect)(function(){oe.current=f},[f]);var Te=(0,r.useRef)(l);(0,r.useEffect)(function(){Te.current=l},[l]);var N=(0,r.useRef)(W);return(0,r.useEffect)(function(){N.current=W},[W]),{items_processor:(function(){var w=(0,o._)(function(re,ie,le){var de,Be,ve,Ce,ce,Ke,Ze,Ge,Xe,Ie,He,ur,Tr,_r,Cr;return(0,i._)(this,function(Er){if(Be=K.getState().tradingViewReference,!Be)return console.warn("[context-menu] trading view reference is not found"),[2,re];if(Be.chartsCount()>1)return[2,re];if(ve=Be.activeChart().symbolExt(),!ve)return[2,re];switch(Ce=ve.name,ce=_.current,Ke=le.menuName==="CrosshairMenuView"?"chart-trading":"context-menu",Ze={price:"".concat(ce.value),formattedPrice:ce.formatted,source:Ke,chartType:"tradingview"},Ge=(de=oe.current[Ce])===null||de===void 0?void 0:de.baseAsset,Xe=g("trade",{defaultValue:"Trade"}),Ie=S(ce.formatted),He=G.current&&te.current===c.SJ.Last&&J.current&&Ge?[ie.createAction({actionId:"Chart.PlaceOrder.BuyLimit",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("limit",{defaultValue:"Limit"})),onExecute:function(){return Te.current.onClickBuyLimit(Ze)}}),ie.createAction({actionId:"Chart.PlaceOrder.BuyStop",label:"".concat(Xe," ").concat(Ge," @ ").concat(Ie," ").concat(g("stop",{defaultValue:"Stop"})),onExecute:function(){return Te.current.onClickBuyStop(Ze)}})]:[],ur=O.current?ie.createAction({actionId:"Chart.PriceAlert.CreateAtCrosshair",label:g("create-alert-at",{defaultValue:"Create Alert at {{price}}",price:ce.formatted}),onExecute:function(){Y({symbol:Ce,price:ce.formatted,mode:zr.n.CREATE})}}):null,re.forEach(function(we){if(!(!("execute"in we)||!("getState"in we))){var Dr=we.getState();if(Dr.actionId===Xr){var z=we.execute.bind(we);we.execute=function(){(0,Kr.A)({elementId:"draw_horizontal_line",isDelivery:N.current,chartType:"tradingview"}),z()}}}}),le.menuName){case"CrosshairMenuView":return ur?[2,[ur].concat((0,d._)(He),(0,d._)(re.slice()))]:[2,(0,d._)(He).concat((0,d._)(re.slice()))];case"ChartContextMenu":return ur?(Tr=Fr(re),_r=Tr[0],Cr=Tr.slice(1),[2,[_r,ur].concat((0,d._)(He),(0,d._)(Cr))]):[2,(0,d._)(He).concat((0,d._)(re.slice()))];default:return[2,re]}return[2]})});return function(re,ie,le){return w.apply(this,arguments)}})()}},qr=function(){var t=(0,r.useState)({value:NaN,formatted:""}),n=t[0],a=t[1],u=(0,r.useCallback)(function(s){var g=s.crosshairData,S=s.activeChart;S&&a({value:g.price,formatted:S.priceFormatter().format(g.price)})},[]);return{onCrosshairMoved:u,crosshairPrice:n}};const Hr=t=>Math.floor(Math.log10(t)),et=t=>{const[n="1"]=t.split(",");return Hr(+n)},pe=({symbolInfo:t,minTick:n})=>{const{pricescale:a=2}=t||{};return n==="default"||n===void 0?Hr(a):et(n)},br=t=>(n,a)=>{try{return n?{format:u=>{const s=pe({symbolInfo:n,minTick:a});return t.format({symbolInfo:n,minTick:a,value:u,precision:s})}}:null}catch{return null}},Mr=10**12,rt=10**9,tt=10**6,jr=10**3,gr=t=>{const n=t.toString(),[,a=""]=n.split(".");return Math.min(a.length,3)},nt=({value:t})=>{const n=Math.abs(t);if(n>=Mr){const a=t/Mr;return{value:a,unit:"T",precision:gr(a)}}if(n>=rt){const a=t/rt;return{value:a,unit:"B",precision:gr(a)}}if(n>=tt){const a=t/tt;return{value:a,unit:"M",precision:gr(a)}}if(n>=jr){const a=t/jr;return{value:a,unit:"K",precision:gr(a)}}return{value:t,unit:"",precision:gr(t)}},ut=t=>(n,a,u)=>{try{return a&&n.type==="volume"?{format:s=>s===void 0?"":t.formatVolume({format:n,symbolInfo:a,precision:u,value:s,volumeDetail:nt({value:s})})}:null}catch{return null}};var lt=function(){var t=(0,Vr.r)(),n=t.format,a=(0,r.useMemo)(function(){return br({format:function(s){var g=s.value,S=s.precision;return n(g,{precision:S})}})},[n]),u=(0,r.useMemo)(function(){return ut({formatVolume:function(s){var g=s.value,S=s.volumeDetail;if(g===void 0||!S)return"";var K=n(S.value,{precision:S.precision});return"".concat(K).concat(S.unit)}})},[n]);return{priceFormatterFactory:a,studyFormatterFactory:u}},dt=e(74069),ir=function(t){var n=(0,dt.K)(t),a=(0,r.useMemo)(function(){return n.map(function(u){var s=u.id,g=u.time,S=u.isBuy,K=u.price,Y=u.tooltip;return{id:s,text:"",price:K,quantity:"",time:Math.round(g/1e3),isBuy:S,tooltip:Y}})},[n]);return{executionOrders:a}},at=function(t){var n=t.namespace,a=t.symbol,u=(0,Br.j)({namespace:n,symbol:a});return(0,r.useMemo)(function(){return u.map(function(s){var g=s.id,S=s.price,K=s.formattedPrice,Y=s.sideText,H=s.showCloseButton,j=s.isPriceChangeable,$=s.cancelTooltipText,f=s.onClick,l=s.onCancel,T=s.onPriceChanged;return{id:g,price:+S,text:"".concat(Y," ").concat(K),editable:j,cancellable:H,cancelTooltip:$,type:c.ND.PriceAlert,onCancel:H?l:void 0,onMove:j?function(W){var _=W.price;T({currentPrice:_})}:void 0,onClick:f}})},[u])},vt=e(90664),ot=function(t){var n=t.symbol,a=(0,vt.w)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){var s=u.id,g=u.text,S=u.price;return{id:s,text:g,price:+S,type:c.ND.BreakEvenPrice}})},[a])},cr=e(82508),ft=e(91025),mt=e(36077),Rr=function(t){var n=t.symbol,a=(0,mt.c)({symbol:n});return(0,r.useMemo)(function(){return a.map(function(u){return(0,ft._)((0,cr._)({},u),{type:c.ND.LiquidationPrice})})},[a])},pt=function(t){var n=t.namespace,a=t.symbol,u=at({namespace:n,symbol:a}),s=ot({symbol:a}),g=Rr({symbol:a});return(0,r.useMemo)(function(){return(0,d._)(u).concat((0,d._)(s),(0,d._)(g))},[u,s,g])},st=e(42902),Ir=e(90170),yt=e(63345),ht=e(48210),Pr=e(51289),xr=e(24326),$r=e(90922),kr=e(34235),it=e(63762),Ft=function(t){var n=(0,Fe.f0)(t),a=n.pricePrecision,u=n.isCM,s=(0,st.yD)(t)||{},g=s.lastPrice,S=g===void 0?"0":g,K=(0,kr.Bp)(u),Y=(0,it.HY)(t),H=(0,kr.gv)(u),j=(0,kr.WF)(u);return(0,r.useMemo)(function(){return(0,$r.qF)({orders:(0,d._)(K).concat((0,d._)(H),(0,d._)(j)),symbol:t}).map(function($){return Y({order:$,lastPrice:S,pricePrecision:a})})},[S,K,H,a,t,Y])},gt=e(31542),Tt=function(t){var n=t.type;(0,gt.u4)("webClick",{module:"chart",elementId:n})},Nt=function(t){var n=t.symbol,a=t.namespace,u=(0,r.useState)(0),s=u[0],g=u[1],S=(0,r.useCallback)(function(){return g(function(N){return N+1})},[]),K=Ft(n),Y=(0,xr.IT)({namespace:a}),H=(0,Fe.f0)(n),j=H.isDelivery,$=H.contractType,f=(0,st.yD)(n)||{},l=f.lastPrice,T=l===void 0?"0":l,W=(0,yt.s)(n),_=(0,it.YZ)({symbol:n,afterConfirmationModalIsShown:S}),O=(0,it.u8)(n),G=(0,it.jn)(n),te=(0,r.useCallback)(function(N){return(0,ht.D)({order:N,isDelivery:j,symbol:j?N.symbol:"".concat(N.symbol,"_").concat($),module:"tradingview_kline"})},[$,j]),J=(0,r.useMemo)(function(){return K.map(function(N){var w=N.order,re=N.price,ie=N.quantity,le=N.quantityText,de=N.isBuy,Be=N.text,ve=N.priceTooltipText,Ce=N.pnlText,ce=(0,Pr.$L)(w),Ke=(0,Pr.OX)(w),Ze=(0,Pr.xL)(w),Ge=ce==="LIMIT",Xe=(0,Ir.Y)(w);return{id:String(Ke),text:Be,price:+re,quantity:le,isBuy:de,editable:Xe&&G,tooltip:ve,modifyTooltip:w.closePosition||!Ge?"":void 0,onMove:G?function(Ie){var He=Ie.price;te(w),Tt({type:"TV_vertical"}),_({order:w,price:+He,quantity:+Ze,lastPrice:T})}:void 0,onModify:w.closePosition||!Ge||!Xe?void 0:function(){Tt({type:"TV_amountclick"}),O({order:w,price:re.toString(),quantity:ie,lastPrice:T})},onCancel:function(){Tt({type:"TV_cancel"}),W(w)}}})},[G,K,T,te,_,O,W,s]),oe=(0,r.useMemo)(function(){return Y.map(function(N){var w=N.id,re=N.value,ie=N.text,le=N.onChange,de=N.isBuy,Be=N.type;return{id:w,price:+re,quantity:"",text:ie,isBuy:de,editable:!0,limitOrderType:c.aA.OrderPreview,onMove:function(ve){var Ce=ve.price;(0,xr.eP)({namespace:a,orderType:Be}),le({id:w,newPrice:Ce})},onMoving:function(ve){var Ce=ve.price;return le({id:w,newPrice:Ce})}}})},[Y,a]),Te=(0,r.useMemo)(function(){return(0,d._)(J).concat((0,d._)(oe))},[J,oe]);return{orderLines:Te}},Vt=e(17855),Bt=function(t,n){var a=(0,r.useState)(t()),u=a[0],s=a[1];return(0,Vt.A)(function(){return s(t())},n),u},Kt=e(58656),Ht=function(t){var n=t.symbol,a=(0,Kt.Q)({symbol:n})||[],u=Bt(function(){return a.map(function(s){var g=s.position,S=s.pnl,K=s.pnlInNumber,Y=s.pnlText,H=s.price,j=s.quantity,$=s.reverseText,f=s.isBuy,l=s.displayReverseOrderModal,T=s.displayClosePositionModal;return{id:g.id||"".concat(g.symbol,"-").concat(g.positionSide),PNL:K,text:"".concat(Y," ").concat(S),price:H,quantity:j,tooltip:"",reverseTooltip:$,isBuy:f,onReverse:l,onClose:function(){T(),(0,gt.u4)("$WebClick",{item:1,module:"xxx",$element_id:"kline_close_position"})}}})},[a]);return{positionLines:u}},jt=e(47354),bt=e.n(jt),$t=e(87017),Ut=e(99168),Yt=e(3742),Wt=e(43335),Gt=e(15540),zt=e(20163),Qt=e(47255);const Jt=t=>({endTime:n})=>(n??0)===t;var Zt=e(36488),Ct=e(48018),Xt=e(82911),We=e(69578),wt=e(53208),Rt=e(19862),qt=e(8874),en=e(26679),rn=e(66547),It=(function(){var t=(0,o._)(function(n){var a,u,s,g,S,K,Y,H,j;return(0,i._)(this,function($){switch($.label){case 0:a=n.onFetchKlineData,u=n.startTime,s=n.endTime,g=n.interval,S=n.limit,K=S===void 0?1e3:S,Y=[],H=[],j=s,$.label=1;case 1:return[4,a((0,cr._)({interval:g,limit:K},j===0?{}:{endTime:j}))];case 2:if(H=$.sent(),H.length===0)return[3,4];H.forEach(function(f){var l=(0,v._)(f,8),T=l[0],W=l[1],_=l[2],O=l[3],G=l[4],te=l[5],J=l[7];Y.push({time:+T,open:+W,high:+_,low:+O,close:+G,volume:+te,quantity:+J})}),j=H[0][0]-1,$.label=3;case 3:if(j>u)return[3,1];$.label=4;case 4:return Y.sort(function(f,l){return f.time-l.time}),[2,Y]}})});return function(a){return t.apply(this,arguments)}})(),tn=function(t){var n=t.firstDataRequest,a=t.hasRequested,u=t.rangeEndDate;return n&&!a?0:u*1e3},nn=e(78142),an=e(16238),St=e.n(an),on=e(98087),xt=e.n(on),sn=e(55009),cn=e.n(sn),kt=e(56477);const Et=t=>cn()(t.chartsCount(),n=>t.chart(n)),un=async t=>{try{const{schema:n,data:a}=await t.exportData({includedStudies:[]}),u=xt()(a)?.[n.findIndex(s=>s.type==="time")];return u!==void 0&&Number.isFinite(u)?u*1e3:void 0}catch{return}},ln=({getWidget:t,...n})=>{const a=new Map,u=new Map,s=()=>{try{const f=t();return f?St()(Et(f).map(l=>l.symbolExt()?.ticker)):[]}catch{return[]}},g=f=>{const l=s();return!l.length||l.includes(f)},S=f=>{const l=u.get(f);if(l)return l;const T={controller:new AbortController,claimedAsMain:!1};return u.set(f,T),T},K=f=>{const l=u.get(f);l&&(l.controller.abort(),u.delete(f))},Y=()=>{const f=s();f.length&&Array.from(u.keys()).forEach(l=>{const T=u.get(l);if(T){if(f.includes(l)){T.claimedAsMain=!0;return}a.has(l)||T.claimedAsMain&&K(l)}})},H=(f,l)=>{f.closed||l.time{f.held=[];let T=l;try{const _=(await f.options.fetchSince(l)).filter(O=>O.time>=l&&!f.closed);_.forEach(O=>{f.highWater=Math.max(f.highWater,O.time),f.options.onTick(O)}),T=xt()(_)?.time??l}finally{const W=f.held??[];f.held=void 0,W.filter(_=>_.time>=T).forEach(_=>{H(f,_)})}},$=(0,kt.k)({...n,onCheck:Y,repair:async()=>{const f=t();if(f)try{const l=Et(f),T=St()(l.map(_=>{const O=a.get(_.symbolExt()?.ticker??"");return O&&{chart:_,feed:O}}));if(!T.length){f.resetCache(),l.forEach(_=>{_.resetData()});return}const W=St()(await Promise.all(T.map(async({chart:_,feed:O})=>{const G=await un(_);return G===void 0?void 0:{feed:O,fromMs:G}})));await Promise.allSettled(W.map(({feed:_,fromMs:O})=>j(_,O)))}catch{}}});return{openFeed:f=>{const l={options:f,held:void 0,highWater:0,closed:!1,detector:void 0};return l.detector=(0,kt.k)({...n,subscribeSignals:f.subscribeSignals,repair:async()=>{l.closed||!l.highWater||g(f.ticker)||await j(l,l.highWater)}}),a.set(f.ticker,l),g(f.ticker)&&$.resubscribeSignals(f.subscribeSignals),{push:T=>{l.held?l.held.push(T):H(l,T)},close:()=>{l.closed=!0,l.detector?.dispose(),a.get(f.ticker)===l&&a.delete(f.ticker)}}},subscribeWake:$.subscribeWake,getSignal:f=>{const l=S(f);return s().includes(f)&&(l.claimedAsMain=!0),l.controller.signal},dispose:()=>{u.forEach(({controller:f})=>{f.abort()}),u.clear(),a.forEach(f=>{f.detector?.dispose()}),a.clear(),$.dispose()}}};var dn=e(71730),vn=e(36620),fn=function(t){var n=t.namespace,a=k.y$(n),u=(0,vn.l)().getSocketSignals,s=(0,r.useRef)();(0,r.useEffect)(function(){return function(){var S;(S=s.current)===null||S===void 0||S.instance.dispose(),s.current=void 0}},[]);var g=(0,r.useCallback)(function(S){var K,Y,H=S;if(((K=s.current)===null||K===void 0?void 0:K.key)===H)return s.current.instance;(Y=s.current)===null||Y===void 0||Y.instance.dispose();var j=nn.wr(S),$=ln({getWidget:function(){return a.getState().tradingViewReference},checkPeriodMs:dn.D(j)});return s.current={key:H,instance:$},$},[a]);return(0,r.useMemo)(function(){return{ensureRecovery:g,getSocketSignals:u}},[g,u])},mn="tv_web_error",pn=[Ye.bt.TRADING,Ye.bt.DELIVERING],Re;(function(t){t.Futures="futures",t.FuturesMark="futures-mark",t.FuturesIndex="futures-index",t.Delivery="delivery",t.DeliveryMark="delivery-mark",t.DeliveryIndex="delivery-index"})(Re||(Re={}));var Mt=function(t){var n=t.symbol,a=t.description,u=t.productType,s=t.pricePrecision,g=t.params,S=t.supportedResolutions,K=S===void 0?F:S;return{symbol:n,description:a,tickSize:Math.pow(10,s),exchange:"Binance",supportedResolutions:K,type:u,params:g}},yn=function(t){var n=t.namespace,a=t.useMiniTicker,u=a===void 0?!1:a,s=t.isCM,g=c.y$(n),S=k.y$(n),K=(0,$t.useQueryClient)(),Y=fn({namespace:n}),H=Y.ensureRecovery,j=Y.getSocketSignals,$=(0,r.useRef)(0),f=(0,r.useRef)(!1),l=(0,r.useMemo)(function(){return bt()(function(){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsStart",catch:function(Q){var ee=Q.error,ae=Q.track;ae({GET_BAR_START_TYPE:ee})},log:function(Q){var ee=Q.track;ee({GET_BAR_START_TYPE:"SUCCESS"})}})})},[]),T=(0,r.useMemo)(function(){return bt()(function(z){return(0,We.HP)({eventId:"trd_chart_tv_load_timeout",stepId:"getBarsEnd",catch:function(ee){var ae=ee.error,Se=ee.track;Se({GET_BAR_END_TYPE:ae,duration_:(0,We.Pu)(z-$.current)})},log:function(ee){var ae=ee.track;ae({GET_BAR_END_TYPE:"SUCCESS",duration_:(0,We.Pu)(z-$.current)})}})})},[]),W=(0,Nr.o)("trd-chart","trade-ui"),_=W.getI18n,O=(0,he.ud)(),G=O.getI18n,te=(0,v._)(g(c.Ir),1),J=te[0],oe=(0,v._)(S(k.A7),2),Te=oe[1],N=(0,se.Ye)(),w=(0,Zt.JD)(s).isContUnit,re=(0,se.y_)(),ie=(0,r.useRef)(N),le=(0,r.useRef)(J),de=(0,r.useRef)(re);(0,r.useEffect)(function(){ie.current=N},[N]),(0,r.useEffect)(function(){le.current=J},[J]),(0,r.useEffect)(function(){de.current=re},[re]);var Be=(0,r.useMemo)(function(){return w?Rt.b:Rt.S},[w]),ve=(0,r.useMemo)(function(){return G("usdtFutures-13",{defaultValue:"Futures"})||""},[G]),Ce=(0,r.useMemo)(function(){return G("inverseFutures-13",{defaultValue:"Delivery"})||""},[G]),ce=(0,r.useMemo)(function(){return _("indexPrice",{defaultValue:"Index Price"})||""},[_]),Ke=(0,r.useMemo)(function(){return _("markPrice",{defaultValue:"Mark Price"})||""},[_]),Ze=(0,r.useMemo)(function(){return _("lastPrice",{defaultValue:"Last Price"})||""},[_]),Ge=(0,r.useMemo)(function(){return{exchanges:[{name:"Binance",value:"Binance",desc:""}],symbols_types:[{name:ve,value:Re.Futures},{name:"".concat(ve," ").concat(Ke),value:Re.FuturesMark},{name:"".concat(ve," ").concat(ce),value:Re.FuturesIndex},{name:Ce,value:Re.Delivery},{name:"".concat(Ce," ").concat(Ke),value:Re.DeliveryMark},{name:"".concat(Ce," ").concat(ce),value:Re.DeliveryIndex}]}},[Ce,ve,ce,Ke]),Xe=(0,r.useCallback)((0,o._)(function(){var z,Q,ee;return(0,i._)(this,function(ae){switch(ae.label){case 0:return ae.trys.push([0,2,,3]),[4,(0,en.c)(K)];case 1:return ae.sent(),[3,3];case 2:return z=ae.sent(),(0,gt.u4)(mn,{type:"onFetchSymbolsInfo",message:z.message,business:"futures"}),[3,3];case 3:return Q=ie.current,ee=Object.keys(Q).reduce(function(Se,xe){var ne=Q[xe]||Ct.o,ke=ne.symbol,Me=ne.pricePrecision,fe=ne.isDelivery,Pe=ne.contractStatus;if(!pn.includes(Pe))return Se;var _e=(0,Yt.S)(ne,G),je=_e.symbol,Ee=_e.contractTypeI18n,me=_e.deliveryDateFormat,ye="".concat(je," ").concat(Ee).concat(me?" ".concat(me):""),$e="".concat(ye," ").concat(Ze),Oe="".concat(ye," ").concat(ce),lr="".concat(ye," ").concat(Ke);return Se.concat([Mt({symbol:ke,description:$e,productType:fe?Re.Delivery:Re.Futures,pricePrecision:Me,params:{priceType:c.SJ.Last}}),Mt({symbol:ke,description:Oe,productType:fe?Re.DeliveryIndex:Re.FuturesIndex,pricePrecision:Me,params:{priceType:c.SJ.Index}}),Mt({symbol:ke,description:lr,productType:fe?Re.DeliveryMark:Re.FuturesMark,pricePrecision:(0,Xt.X)(Me),params:{priceType:c.SJ.Mark}})])},[]),[2,ee]}})}),[G,ce,Ke,Ze,K]),Ie=(0,r.useCallback)(function(z){var Q=z.ticker,ee=z.symbol,ae=z.interval,Se=z.priceType,xe=z.retryUntil,ne=ie.current[ee]||Ct.o,ke=ne.pair,Me=ne.contractType,fe=ne.contractVal,Pe=ne.isDelivery,_e=Pe?Be(fe):function(Oe){return Oe},je=(0,wt.h)({symbol:ee,pair:ke,contractType:Me,isDelivery:Pe,priceType:Se}),Ee,me,ye=function(Oe){return zt.y("tv","".concat(Pe?"cm":"um","|").concat(ee,"|").concat(Se,"|").concat(Oe.interval,"|").concat((Ee=Oe.limit)!==null&&Ee!==void 0?Ee:"","|").concat((me=Oe.endTime)!==null&&me!==void 0?me:""))};if(xe===void 0)return{transform:_e,onFetchKlineData:je};var $e=H(ae);return{transform:_e,onFetchKlineData:Qt.b(je,{enabled:Jt(xe),key:ye,signal:$e.getSignal(Q),subscribeWake:$e.subscribeWake})}},[H,Be]),He=(0,r.useCallback)((function(){var z=(0,o._)(function(Q){var ee,ae,Se,xe,ne,ke,Me,fe,Pe,_e,je,Ee,me,ye,$e,Oe,lr,Or;return(0,i._)(this,function(dr){switch(dr.label){case 0:return ee=Q.binanceInterval,ae=Q.rangeStartDate,Se=Q.rangeEndDate,xe=Q.tickerInfo,ne=Q.symbolInfo,ke=Q.firstDataRequest,$.current=Date.now(),l(),Me=xe.symbol,fe=Me===void 0?"":Me,Pe=xe.params,_e=Pe.priceType,je=_e===void 0?le.current:_e,de.current({symbol:fe,priceType:je})?(T(Date.now()),[2,[]]):(Ee=Math.max(0,ae*1e3),me=tn({firstDataRequest:ke,hasRequested:f.current,rangeEndDate:Se}),f.current=!0,$e=Ie({ticker:(ye=ne.ticker)!==null&&ye!==void 0?ye:"",symbol:fe,interval:ee,priceType:je,retryUntil:ke?me:void 0}),Oe=$e.transform,lr=$e.onFetchKlineData,[4,It({onFetchKlineData:lr,startTime:Ee,endTime:me,interval:ee})]);case 1:return Or=dr.sent(),T(Date.now()),[2,Or.map(Oe)]}})});return function(Q){return z.apply(this,arguments)}})(),[Ie,T,l]),ur=(0,r.useCallback)(function(z){var Q=z.binanceInterval,ee=z.callback,ae=z.tickerInfo,Se=z.symbolInfo,xe=ae.symbol,ne=xe===void 0?"":xe,ke=ae.params,Me=ke.priceType,fe=Me===void 0?le.current:Me;if(de.current({symbol:ne,priceType:fe}))return function(){return null};var Pe=ie.current[ne]||Ct.o,_e=Pe.pair,je=Pe.contractType,Ee=Pe.isDelivery,me,ye=(me=Se.ticker)!==null&&me!==void 0?me:"",$e=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe}).transform,Oe=H(Q),lr=Oe.openFeed({ticker:ye,onTick:ee,subscribeSignals:j(Ee),fetchSince:(function(){var dr=(0,o._)(function(Lr){var Ur,Yr,ct,De;return(0,i._)(this,function(ze){switch(ze.label){case 0:return Ur=Ie({ticker:ye,symbol:ne,interval:Q,priceType:fe,retryUntil:0}),Yr=Ur.transform,ct=Ur.onFetchKlineData,[4,It({onFetchKlineData:ct,startTime:Lr,endTime:0,interval:Q})];case 1:return De=ze.sent(),[2,De.map(Yr)]}})});return function(Lr){return dr.apply(this,arguments)}})()}),Or=(0,rn.B)({symbol:ne,pair:_e,contractType:je,interval:Q,priceType:fe,isDelivery:Ee,useMiniTicker:u,callback:function(dr){lr.push($e(dr))}});return function(){lr.close(),Or()}},[Ie,H,j,u]),Tr=(0,r.useCallback)((0,o._)(function(){var z;return(0,i._)(this,function(Q){switch(Q.label){case 0:return[4,K.fetchQuery(Wt.Bz.SERVER_TIME(s),function(){return(0,Gt.l)(s)},{})];case 1:return z=Q.sent(),[2,Math.floor(z/1e3)]}})}),[K,s]),_r=(0,r.useCallback)(function(z){return(0,cr._)({},z.template,(0,qt.sp)(s))},[s]),Cr=(0,r.useMemo)(function(){return ue()},[]),Er=Cr({config:Ge,onFetchSymbolsInfo:Xe,onFetchBars:He,onSubscribeBars:ur,onFetchServerTimeApi:Tr,onTransformSymbolInfo:_r}),we=Er.datafeed,Dr=Er.symbolsInfo;return(0,Ut.A)(function(){return Te()},[w]),{datafeed:we,symbolsInfo:Dr}},Pt="trd_chart_tv_load_timeout",hn=function(){var t=(0,We.PM)({id:"chartReady"}),n=(0,We.PM)({id:"getBarsStart",timeout:3}),a=(0,We.PM)({id:"getBarsEnd",timeout:6}),u=(0,We.PM)({id:"chartResponsive",timeout:3});We.HP.init({step:t._(n)._(a)._(u),eventId:Pt}),(0,We.HP)({eventId:Pt,stepId:"chartReady",catch:function(g){var S=g.error,K=g.track;K({CHART_READY_TYPE:S,connection:JSON.stringify(navigator.connection)})},log:function(g){var S=g.track;S({CHART_READY_TYPE:"SUCCESS",connection:JSON.stringify(navigator.connection)})}})},gn=function(t){(0,We.HP)({eventId:Pt,stepId:"chartResponsive",catch:function(a){var u=a.error,s=a.track;s({CHART_RESPONSIVE_TYPE:u,connection:JSON.stringify(navigator.connection)})},log:function(a){var u=a.track;u({CHART_RESPONSIVE_TYPE:"SUCCESS",duration_:(0,We.Pu)(Date.now()-t)})}})},Tn=[Qe.ZE,Qe.F3],Dt=function(t){var n=t.symbol,a=t.namespace,u=t.useMiniTicker,s=u===void 0?!1:u,g=c.y$(a),S=k.y$(a),K=(0,v._)(g(c.A8),1),Y=K[0],H=(0,v._)(g(c.Ir),2),j=H[0],$=H[1],f=(0,v._)(S(k.yj),1),l=f[0],T=(0,v._)(g(c.e6),1),W=T[0],_=(0,v._)(g(c._b),1),O=_[0],G=(0,he.ot)(),te=(0,pr.K7)(),J=te.pathname.includes(Ae.Q2),oe=te.pathname.includes(Ae.eq),Te=(0,r.useMemo)(function(){return J?(0,d._)(B.oT).concat(["hide_left_toolbar_by_default"]):B.oT},[J]),N=(0,se.Ye)(),w=(0,se.v3)({symbol:n}),re=(0,Fe.es)(n),ie=yn({namespace:a,useMiniTicker:s,isCM:re}),le=ie.datafeed,de=ie.symbolsInfo,Be=(0,Je.DP)(),ve=Be.theme,Ce=ve==="light"||ve==="light_glacier"?"light":"dark",ce=(0,yr.Z)(Ce),Ke=(0,r.useRef)(-1),Ze=Ht({symbol:n}).positionLines,Ge=Nt({symbol:n,namespace:a}).orderLines,Xe=ir({symbol:n}).executionOrders,Ie=pt({namespace:a,symbol:n}),He=(0,se.y0)(),ur=(0,r.useRef)(He),Tr=(0,r.useRef)(N),_r=(0,r.useRef)(l),Cr=(0,r.useRef)(J),Er=(0,r.useRef)(oe),we=(0,r.useRef)(de);(0,r.useEffect)(function(){we.current=de},[de]),(0,r.useEffect)(function(){ur.current=He},[He]),(0,r.useEffect)(function(){Tr.current=N},[N]),(0,r.useEffect)(function(){_r.current=l},[l]),(0,r.useEffect)(function(){Cr.current=J},[J]),(0,r.useEffect)(function(){Er.current=oe},[oe]),(0,Ve.a)(n,s),(0,sr.A)({symbol:n,interval:Y,priceType:j});var Dr=(0,r.useCallback)(function(){Ke.current=Date.now()},[]),z=(0,r.useCallback)((0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})()),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er=="string"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}),[a]),Q=(0,r.useMemo)(function(){return z},[z]),ee=(0,or.R)(),ae=(0,Ne.fQ)(a),Se=(0,Ne.m3)(a),xe=(0,Ne.S_)(a),ne=(0,Ne.Ds)(a),ke=(0,r.useRef)(n),Me=(0,r.useRef)(Y),fe=(0,r.useCallback)(function(){return ee("tradingView"),xe({panel_index:0,symbol:n,interval:Y}),hn()},[Y,ee,n,xe]),Pe=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be,vr,fr,er=(fr=(vr=(be=qe.symbolExt())===null||be===void 0?void 0:be.name)!==null&&vr!==void 0?vr:qe.symbol())!==null&&fr!==void 0?fr:"";ae({panel_index:ze,symbol:er,previous_symbol:ke.current}),ke.current=er},[ae]),_e=(0,r.useCallback)(function(De){var ze=De.index,qe=De.chart,be=qe.resolution();Se({panel_index:ze,interval:be,previous_interval:Me.current}),Me.current=be},[Se]),je=(0,r.useCallback)(function(){gn(Ke.current)},[]),Ee=(0,r.useCallback)(function(){},[]),me=(0,r.useMemo)(function(){return J?Ee:void 0},[Ee,J]),ye=(0,r.useCallback)((function(){var De=(0,o._)(function(ze){var qe,be,vr,fr,er,nr,mr,ar,Sr;return(0,i._)(this,function(Wr){switch(Wr.label){case 0:return qe=ze.chart,be=qe.symbolExt(),be?(vr=b()(we.current,function(Gr){return Gr.ticker===be.ticker||Gr.symbol===be.ticker}),fr=vr||{},er=fr.params,nr=er===void 0?{}:er,mr=nr.priceType,ar=mr===void 0?"":mr,ar&&$(ar),Sr=Tr.current[be.name],Sr?Er.current||Cr.current?[2]:[4,z()]:[2]):[2];case 1:return Wr.sent(),G({stateSymbol:Sr.symbol,isDeliveryColumn:Sr.isDelivery}),[2]}})});return function(ze){return De.apply(this,arguments)}})(),[G,z,$]),$e=qr(),Oe=$e.onCrosshairMoved,lr=$e.crosshairPrice,Or=wr({namespace:a,symbol:n,priceType:j,crosshairPrice:lr}),dr=lt(),Lr=(0,r.useMemo)(function(){return L({symbol:n,params:{priceType:j}})},[n,j]),Ur=(0,r.useMemo)(function(){return{symbol:Lr,theme:ce,limitOrders:Ge,positionOrders:Ze,executionOrders:Xe,labelLines:Ie,initialConfig:{tvConfig:{symbol:Lr,library_path:"/".concat(Ye.u_,"/trading-platform-30/"),theme:ce,datafeed:le,enabled_features:Te,context_menu:Or,custom_formatters:dr},onCrosshairMoved:Oe,onScriptLoaded:Dr,onSave:Q,onChartReadyDone:fe,onFirstTimeDataLoadedDone:je,onInitChart:me,onActiveChartChanged:ye,onSymbolChanged:Pe,onIntervalChanged:_e}}},[Lr,le,Te,Xe,Ie,Or,dr,Oe,ye,fe,je,me,_e,Q,Dr,Pe,Ge,Ze,ce]),Yr=Le({namespace:a,type:"tv"}),ct=(0,r.useCallback)(function(){ne({panel_index:0,symbol:n}),Yr()},[Yr,n,ne]);return(0,p.jsxs)(D.t,{name:"trading_view",onReset:ct,children:[(0,p.jsx)(Ue.A,{namespace:a,tradingViewProps:Ur}),w&&j==="index"&&O===c.Ev.Single&&W===c.tU.TradingView&&(0,p.jsx)(rr.B,{})]})};const Cn=Dt}}[70940]; } +export function createMirrorCallbacks(e,__dispatchChartMirror) { +var o=e(49253); +var v=e(77135); +var i=e(42288); +var M=e(43917); +var h=e.n(M); +var V=e(7980); +var U=e.n(V); +var Qe=e(94652); +var Tn=[Qe.ZE,Qe.F3]; +return { +native(a,Cr={current:false},_r={current:true}) { return (0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})()),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er=="string"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}); }, +patched(a,Cr={current:false},_r={current:true}) { return (0,o._)(function(){var De,ze,qe,be,vr,fr,er;return(0,i._)(this,function(nr){switch(nr.label){case 0:if(Cr.current||!_r.current)return[2,Promise.resolve()];nr.label=1;case 1:return nr.trys.push([1,5,,6]),De=h().createInstance({name:a}),[4,De.keys()];case 2:return ze=nr.sent(),qe=ze.filter(function(mr){return/^((#TV_SYMBOL-)|(myTradingView))/g.test(mr)}),be=qe.map((function(){var mr=(0,o._)(function(ar){return(0,i._)(this,function(Sr){return[2,De.getItem(ar)]})});return function(ar){return mr.apply(this,arguments)}})()),[4,Promise.all(be)];case 3:return vr=nr.sent(),fr=U()(qe,vr),[4,Promise.all(Tn.map((function(){var mr=(0,o._)(function(ar){var Sr,Wr;return(0,i._)(this,function(Gr){switch(Gr.label){case 0:return a===ar?[2,Promise.resolve()]:(Sr=h().createInstance({name:ar}),Wr=__dispatchChartMirror(Sr,fr,()=>fr.map((function(){var Sn=(0,o._)(function(Lt){var _t,Ot,At;return(0,i._)(this,function(Mn){return _t=(0,v._)(Lt,2),Ot=_t[0],At=_t[1],Ot?[2,Sr.setItem(Ot,At)]:[2,Promise.resolve()]})});return function(Lt){return Sn.apply(this,arguments)}})())),[4,Promise.all(Wr)]);case 1:return Gr.sent(),[2,Promise.resolve()]}})});return function(ar){return mr.apply(this,arguments)}})()))];case 4:return nr.sent(),[2,Promise.resolve()];case 5:return er=nr.sent(),console.warn(typeof er=="string"?er:er.message),[2,Promise.resolve()];case 6:return[2]}})}); } +}; +} diff --git a/test/fixtures/binance-chart-storage/mirror-scoped-manifest.json b/test/fixtures/binance-chart-storage/mirror-scoped-manifest.json new file mode 100644 index 0000000..43c1d70 --- /dev/null +++ b/test/fixtures/binance-chart-storage/mirror-scoped-manifest.json @@ -0,0 +1,53 @@ +{ + "description": "Static 70940 factory with one pinned mirror-write RHS replacement; exact original callback fixture uses original host dependencies.", + "source": { + "file": "TradingView.99bc5074.js", + "url": "https://bin.bnbstatic.com/static/chunks/TradingView.99bc5074.js", + "sha256": "b6a2cddd017fb4517abb4ea458e19905c59f955a3f99efa18beb1e82f3938c1c" + }, + "offset_contract": "Zero-based UTF-16 String.slice offsets; exclusive ends; SHA-256 uses UTF-8 bytes.", + "original_factory": { + "source_utf16_range": [ + 6665, + 36819 + ], + "sha256": "37d249d1ceec4a41b54a959c1ab2c4b52b3061e504cf8cd81a824962af8c6c88" + }, + "replacement": { + "source_utf16_range": [ + 33783, + 34025 + ], + "factory_utf16_range": [ + 27118, + 27360 + ], + "original_sha256": "668c3d2a30909f839e85dd5ddb8b545a39e0841fca4222e6d0a1e6f6322773cb", + "inserted_sha256": "cfb0b68a07f6e2ee0f6ba032628e22b29eccbca5e1e1991a7d18e1140c7f82fa" + }, + "original_callback": { + "label": "callback:70940:z", + "source_file": "TradingView.99bc5074.js", + "source_utf16_range": [ + 32997, + 34322 + ], + "fixture_utf16_range": [ + 48748, + 50073 + ], + "sha256": "79aa5f884c6cc6c32815b756a2a406b73231c0c97d058c781a45d0413fe6b6ee" + }, + "transformed_callback_sha256": "f8169f34717c42cc02268f16e1160e9b084add7eeba3b882bcd9bdf9670c8e5b", + "outputs": { + "mirror-module.js": { + "sha256": "2b2d5fa1aaab4e794770aa1a29cff8483f4e213b72ac9647c30964e7920ec658", + "bytes": 61596 + }, + "mirror-scoped-callback.js": { + "sha256": "02a0fc0287b5ddfaadfc97d8dfcf0d16814acafa8b8a643a887a8747ffe8a217", + "bytes": 33365 + } + }, + "evidence_limits": "Whole module source and inverse replacement are statically verified. Offline behavior executes exact callbacks with original dependency factories; full React module initialization is not executed." +} diff --git a/test/fixtures/binance-chart-storage/mirror-scoped.md b/test/fixtures/binance-chart-storage/mirror-scoped.md new file mode 100644 index 0000000..390e935 --- /dev/null +++ b/test/fixtures/binance-chart-storage/mirror-scoped.md @@ -0,0 +1,151 @@ +# Scoped mirror factory fixture + +`mirror-scoped-callback.js` retains the complete public `70940` factory for source +identity checks and the original mirror callback with its original lexical +declarations. Dependencies execute from the separately verified +`host-persistence.js` factories. The test wrapper supplies the namespace and the +two native control refs; it does not reimplement the mirror or save algorithm. + +`experiments/binance-chart-storage/generate-mirror-module.js` accepts the exact +public `TradingView.99bc5074.js` response file. It pins the whole source SHA-256, +uses Acorn to identify one `70940` method and one mirror `Wr=fr.map(...)` +expression, pins their ranges and original factory hash, rejects any occurrence +of the injected dispatch identifier, and proves that reversing the replacement +reproduces the entire original factory byte for byte. Generated files are +deterministic and contain static JavaScript methods, without runtime evaluation. + +The only transformed expression is: + +```js +__dispatchChartMirror(Sr,fr,()=>ORIGINAL_FR_MAP_EXPRESSION) +``` + +The thunk retains the original compiled async per-key map, including its falsy +key guard and native `setItem` call. `replaceChartMirrorFactory` checks the exact +original `Function.prototype.toString` result synchronously during registration. +The generated patched test callback is verified equal to the corresponding +callback slice of the complete generated factory. + +This boundary replaces the earlier global localForage facade as the current +experiment candidate. That facade changes the native transaction ordering of +`setItem` relative to reads, remove and clear. Here the native instances and +methods remain intact: source reads, mirror guards/catch, ordinary TradingView +save, Basic and reset paths retain their original source. Only a mirror's +destination writes enter the explicitly scoped batch writer. Its default `lab` +scope permits only `https://chart-storage.test`; `isolated-ususdt-probe` permits +only the exact top-level `https://www.binance.com/zh-CN/futures/USUSDT` page. The +latter cannot prove profile isolation: the separate harness must own a fresh +anonymous Chrome process/profile. Default production refusal, storage contracts +and limits are unchanged. Its whole-batch atomicity, +interleaving and explicit capacity errors still differ from native per-key writes. + +```sh +node experiments/binance-chart-storage/generate-mirror-module.js \ + /path/to/TradingView.99bc5074.js +``` + +The browser helper serves exact raw source through three local asset routes; +all other network requests remain governed by the isolated lab's blocking +route. Browser execution is necessary for the function-source test because the +Playwright Node module loader reformats imported JavaScript. Such reformatted +source correctly fails the strict production source check. + +Offline validation has distinct layers: complete-module source identity and inverse +replacement; exact original and transformed callback execution with real +localForage, original save/Basic functions and IndexedDB; synthetic 70940 factory +lifecycle in the original captured Rspack runtime. The synthetic lifecycle +test verifies registration, cache and cleanup behavior, not complete React +initialization of public module 70940. Both runtime-order regressions first +failed when a replacement callback synchronously stopped observation but still +installed its candidate; the bootstrap now rechecks active state before wrapping +the factory. Capture rejects as stopped while the original factory and cache +continue normally. + +The scoped suite contains 19 scenarios, and the separate real-IndexedDB writer +suite contains 18. Before the explicit page-probe scope, their combined experiment run, including historical suites, +passed 105 browser scenarios; eight Node origin/path/frame cases (including +non-lab writer refusal) and the full test-policy lint passed separately. Writer +tests cover atomic failure, quotas, limits, empty input and a finite stop fence. +After that fence, retained callbacks execute their exact original per-key thunk; +ordinary native APIs are never held behind the mirror fence. The 32-chain data +and transaction/read/write measurements are recorded in the +[experiment README](../../../experiments/binance-chart-storage/README.md). +The scope extension subsequently passed all 18 Node origin/path/frame cases +after three new cases first failed, all 37 mirror browser scenarios, and the +complete test-policy lint. + +A separate anonymous Chrome `154.0.8037.98` run also validated and executed the +complete static `70940` factory once. It supplied a native-only thunk dispatcher, +left the lab writer disabled, and recorded `dispatches: 0`. The inspected +screenshot showed the USUSDT chart and orderbook after native `dataReady()` became +true. That is complete-module startup evidence, not execution of the injected +mirror branch or real batch writes. The result and screenshot are in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-native-preflight-HeCAar/`. +It does not establish existing-profile compatibility, Tampermonkey delivery, +production batch behavior, cross-database atomicity or blank-page recovery. + +The later independent native-shape probe recorded a bounded interval change +from `60` to `15` with completed mirrors increasing from 6 to 7. All seven final +completions targeted `chart_delivery` with IndexedDB, without mirror or JSON-shape +failures; maxima were four entries, 49,003 batch bytes and 24,504 record bytes. +The inspected screenshot showed the data-ready chart and orderbook; observation +stopped and its temporary browser closed. Evidence is in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-shape-probe-yDfadK/`. +This native-only result does not establish real-page batch execution, and the +seven final mirrors are not attributed to one action. + +On **2026-10-07 Beijing time**, an independent anonymous Chrome `154.0.8037.98` +run enabled the batch writer through the explicit probe scope. Its report starts +at `2026-10-06T16:24:24.572Z`. Five batches committed five transactions, eight +writes and eleven skipped writes, with zero failures, rejections, aborts or +pending work; peaks were two batches and 98,006 bytes. The bounded one-hour to +fifteen-minute transition advanced completed mirrors from 4 to 5. After stop, +the four-hour stage advanced them from 5 to 6 and native calls from zero to one, +with the entire writer statistics unchanged. Each stage read back four entries +matching its captured dispatch snapshot. The chart was data-ready, the inspected +screenshot showed candles, moving averages, volume and the orderbook, and the +browser closed without page errors. Evidence is in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-mirror-bulk-probe-3YcdXL/`. +This is actual anonymous-page batch and mounted native-continuation evidence, +not installation or enablement in the user's logged-in profile. + +Three prior attempts stopped at an ambiguous anonymous-UI locator, before any +factory match or dispatch; using the unique button role fixed the check. They +are not storage failures. The standalone anonymous runner/entry/contract retain +fixed-URL runtime guards and diagnostic failure counts. Four Node contract tests +plus 18 scope/origin and 18 native-only preflight cases passed, totaling 40; the +37 mirror browser scenarios also passed. + +Before Tampermonkey delivery, a separate native-only acceptance must verify real +document-start timing and MAIN_WORLD execution. The official +[Content Script API](https://www.tampermonkey.net/documentation.php?q=content_script_api) +documentation gives Chrome document-start support to **UserScripts API Dynamic**, +but not default Content Script or ordinary UserScripts API mode. Official +[`@sandbox`](https://www.tampermonkey.net/documentation.php?q=sandbox) documentation +says `raw` requests MAIN_WORLD but can use another enabled sandbox if that +injection is unavailable. The metadata is not runtime proof. Full extension +acceptance must also show strict source pin before first execution, actual native +mirror completion and cleanup; the Chrome initialization-script probe does not +substitute for those checks. + +The native-only core, entry and build now produce `native-preflight.user.js` as +an independently reviewed, unpublished and uninstalled artifact. Its verified +four-source dependency graph excludes the writer and runtime loaders. It records +bounded in-memory statistics for 30 seconds, synchronously forwards the exact +native thunk result/error, and stops on manual request, pagehide, scope change +or deadline. No stored values are inspected and no storage calls are added. +Its existence does not prove Tampermonkey timing or MAIN_WORLD delivery; the +logged-in profile, long-running concurrent workload and blank-page root-cause +recovery also remain unverified. + +The generated native-only artifact was separately executed through `addInitScript` +in anonymous Chrome `154.0.8037.98` on October 7 Beijing time (report start +`2026-10-06T16:30:27.651Z`). Attempts, exact matches and factory executions each +equaled one; capture completed, with four native thunk dispatches and 15 entries +counted. There were no failures or page errors; manual stop retired observation +and the browser closed. The inspected screenshot showed the chart and orderbook. +Evidence is in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-native-artifact-probe-JSBjWC/`. +`completed` means module capture, and dispatch count does not certify asynchronous +storage commits. This verifies the generated artifact in a Chrome startup hook, +not Tampermonkey timing or installation; it remains unpublished and uninstalled. diff --git a/test/fixtures/binance-chart-storage/webpack-runtime.js b/test/fixtures/binance-chart-storage/webpack-runtime.js new file mode 100644 index 0000000..72ba63f --- /dev/null +++ b/test/fixtures/binance-chart-storage/webpack-runtime.js @@ -0,0 +1,2 @@ +(()=>{"use strict";var m={},h={};function a(e){var r=h[e];if(r!==void 0){if(r.error!==void 0)throw r.error;return r.exports}var c=h[e]={id:e,loaded:!1,exports:{}};try{m[e].call(c.exports,c,c.exports,a)}catch(f){throw c.error=f,f}return c.loaded=!0,c.exports}a.m=m,a.F={},a.E=e=>{Object.keys(a.F).map(r=>{a.F[r](e)})},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},(()=>{var e=Object.getPrototypeOf?c=>Object.getPrototypeOf(c):c=>c.__proto__,r;a.t=function(c,f){if(f&1&&(c=this(c)),f&8||typeof c=="object"&&c&&(f&4&&c.__esModule||f&16&&typeof c.then=="function"))return c;var d=Object.create(null);a.r(d);var b={};r=r||[null,e({}),e([]),e(e)];for(var t=f&2&&c;(typeof t=="object"||typeof t=="function")&&!~r.indexOf(t);t=e(t))Object.getOwnPropertyNames(t).forEach(o=>{b[o]=()=>c[o]});return b.default=()=>c,a.d(d,b),d}})(),a.d=(e,r)=>{for(var c in r)a.o(r,c)&&!a.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:r[c]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((r,c)=>(a.f[c](e,r),r),[])),a.u=e=>e==="29917"?"static/common/1c004a19.46f8194a.js":e==="37880"?"static/common/c10248db.38b54360.js":e==="98500"?"static/common/71bcc32a.aadad678.js":e==="5180"?"static/common/1aca08b0.cb16770a.js":e==="26835"?"static/common/c9f57757.b0115883.js":e==="8103"?"static/common/0b3a5307.4b0b62a5.js":e==="76423"?"static/common/6eaeece3.b6723ef9.js":e==="22730"?"static/common/18e0f071.821fecc7.js":"static/chunks/"+({18932:"prel-RenderlessCalculation",20178:"page-9f10",21291:"vendor.highcharts",22894:"prel-ChartTabs",28410:"page-0b97",28486:"dynamic-fingerprint",29677:"page-4df9",39023:"page-5565",40299:"page-aa4c",41327:"HeaderDecider",42318:"page-743f",46149:"prel-Audit",46233:"BasicKline",49255:"DepthChart",50003:"page-53b3",53251:"prel-Trades",54568:"page-8ccb",59052:"prel-SymbolInfo",59708:"prel-TPSLWrapper",64758:"prel-TickerWeb",66066:"pref-Market",67152:"page-d6d6",72878:"prel-Chart",74078:"page-0305",76130:"prel-SlippageTolerance",77458:"TradingView",77839:"prel-TradingData",82505:"dynamic-analytics-web-vitals",83026:"loaders",85893:"page-086a",8689:"page-30a1",87502:"layout-95d1",87521:"page-17ad",90981:"prel-SSOrderBook",93765:"prel-Header",95098:"dynamic-bids-interceptor"}[e]||e)+"."+{10225:"3c5c66ec",10312:"67aadbf3",10373:"1c5e71ff",10418:"15c9b495",1042:"24ca3209",10457:"98bb0740",10548:"0486e0cf",1064:"37b74f27",10641:"91ec5ca9",10693:"5779ffb9",10882:"82ab54f1",11001:"f6c6efdb",11130:"3e009226",11137:"197a5481",11446:"aca8f6eb",11528:"f41b832b",11644:"c3040a67",11655:"23709182",11665:"7ae2fd6e",11722:"79662536",1183:"5b66e887",11834:"4b3b0877",11893:"e4007fc9",11928:"4970c3c2",12050:"a926bfcd",12095:"c7dc524d",12240:"e27dbd25",12397:"30a952f0",12414:"3d3401a4",12687:"45f0758e",13083:"726de19b",13135:"97c3e339",13153:"ab80d87e",13280:"e16aae8a",13286:"5146f056",13665:"2318c6ff",13723:"c9e3d741",13746:"6ae42129",13816:"1c0be6c6",13969:"ad1978ae",14105:"10a31c5d",14209:"c833b24d",14213:"faeaca61",14561:"0b2162fa",14597:"8a918f02",14677:"37784089",14696:"c490365c",14708:"3060b49e",14949:"d2ea7a74",15084:"629ef051",15456:"1cc836b5",1585:"09773d0d",1601:"6f50eb6d",16024:"0b7f65fd",16293:"7d56b537",16362:"553dca61",16483:"6c76b538",16528:"a6b4363e",16574:"a0db880e",16578:"cd3eb3a0",16783:"06b8a6ae",16786:"47cd4cd6",16873:"442d9b04",16914:"8d55c642",1703:"97b21c33",17056:"5fdee7cb",17106:"f913e0cf",17149:"ab35609a",17313:"269ed0a8",17361:"21c961ce",174:"64566e32",17400:"ce71b742",17456:"aff0954a",17597:"d41a701e",17640:"6c2f1fee",177:"3449fe4a",17715:"83a146a7",1772:"26aecf2b",17797:"7d2e053f",18097:"b9ecf07e",18099:"57a617d8",18174:"20ec65b4",18284:"72152169",18285:"ffb35d67",18350:"73ebe7cd",18455:"cee8d3c0",18489:"e5e57f0a",1858:"d4ac5be9",18691:"0e7896e5",18784:"e7e72e9c",1884:"ae2d33c4",18887:"de68869c",18932:"82d27b4c",18946:"251a3445",19001:"e2649136",19006:"f7661727",19136:"7ca921d5",19236:"c3598035",19338:"38bbaeb8",19462:"504a0242",19469:"d9441f83",19738:"4b04a68f",19804:"3a49f782",19886:"50228f39",20008:"e876c9b2",20051:"3f08af62",20178:"1816bfa6",20197:"aaa14631",20744:"016d4913",2082:"474594df",20871:"b5c271ef",21062:"917c9468",2110:"cee6db60",21223:"14e604e5",21245:"1345ca3f",21291:"8b3a66df",21369:"9bb255f2",21562:"7d9b3ae5",21612:"3889d3ea",21685:"b1a1af72",218:"8b12434d",21848:"c84834fc",22225:"86430414",22295:"2b83db6f",22325:"9ae43989",22348:"4f96d829",2249:"0e85187c",22617:"9c0c3a35",22687:"38be9ff8",2272:"88b3a24c",22894:"c17e9930",22947:"ef3a901a",23059:"05b1b382",23086:"f9840b1f",23108:"3e126128",23196:"6451c778",23367:"11885292",23511:"32f2cec7",23568:"907e6f94",23569:"d2d3cbe2",23730:"6ef14aff",23877:"c79ee185",23900:"37bea33f",23907:"85e204bf",2411:"8316005d",24118:"37e08e55",24132:"91842e2a",24470:"2c13b038",24588:"5723f6ad",24607:"be947b3a",24656:"acfb9709",24661:"305f5490",24832:"50042250",24938:"be45fd82",25154:"c9e1f92f",25439:"de9c4d48",25488:"4663df09",25539:"8ff0c607",256:"2d949870",25662:"8007a5eb",25670:"a3f201f7",25717:"c2c54cd9",25810:"0f0635b6",25838:"008d9718",25991:"f7d3e4bd",26195:"d79c8508",26326:"6edbf225",26365:"1bad20f1",26369:"b5bee82a",26381:"97f23e2b",26620:"d90c4596",26645:"53adb74d",26730:"172ffdf3",26757:"dbe4dfa4",26807:"e3ec189e",27081:"d11d2959",27230:"0f3d016a",27316:"92592b6b",27407:"c6c725c9",27459:"4991bff4",27489:"51fba5c4",27834:"2a97b50a",27841:"c7afe0a9",27842:"cced28dd",28005:"4cd992d4",28115:"16107abb",28246:"0da17351",28283:"e3e8836b",28303:"5f4ab4f2",28410:"70d624bc",28462:"4edadae5",28486:"be11c6ec",28540:"993b06f7",28660:"105b5c63",28751:"fa11c2b3",28785:"15ac2449",28928:"9aeec892",28935:"84611a83",28941:"a32fa35a",29e3:"4d83c073",29042:"3c0cf97b",29127:"4ae5f84f",29153:"180158f3",29274:"821e5764",29554:"66c3d782",29645:"035d5218",29677:"ca3a9b5b",29737:"fe0644e4",29775:"5bd2c274",29958:"df20f889",29982:"da987c13",29995:"789b1d48",30129:"3df5df96",30151:"0fd5c909",30153:"53ba705e",30162:"a5bc45dd",30232:"c3e1c314",3025:"47890bf3",3031:"682039f1",30337:"b23b219c",30401:"dc74b6d5",30463:"280aef9f",30528:"20b5ee11",30657:"6c3da888",31031:"4f09645c",31361:"481204f7",31430:"ab76ff7f",31492:"2973c60c",31607:"6770bdb9",31630:"24c61e01",31692:"ba153ffb",31715:"6aea9a75",31835:"070d1bc1",31955:"0bf02739",32028:"a4cda7cb",32119:"ccb31ce9",32273:"52cd9bf2",32279:"eb3b45a3",32293:"eecfefe3",32536:"f022d7ba",3263:"81c9dffe",3281:"bc96d1b8",3283:"136d5681",32874:"d6461cc3",330:"cd28f4b9",3314:"51597d7f",33259:"ea69ca24",33585:"05b28886",33695:"89bfdc03",33762:"a0f5788b",34041:"82309b72",34209:"9e601b5c",34294:"c82844bc",34389:"4720d650",34840:"a088f08c",34853:"2b9bdadc",34922:"b4b96649",35207:"77bc27bd",35269:"ee707a01",35464:"cc1a1fcb",35584:"fba6ef09",35693:"656f5484",35817:"9696e92a",3597:"2c93be07",36124:"31cebc97",36261:"6adf855f",36370:"e40fb46c",36381:"03f50167",3646:"9f80bdcc",36554:"9a9a3760",36599:"f29e895e",36648:"dd8ae7dc",3675:"bac78f04",36764:"832dde9c",36842:"b5ccbe99",36963:"2b5f34ea",37347:"c9980466",37370:"b257312b",37511:"5ad8e45f",37528:"4fa4dd98",37849:"b7a02c90",37916:"255f878f",38127:"75256f4a",38225:"c6e51053",38228:"8aa8ef92",38264:"e286a9eb",38312:"f64c35eb",38530:"8ed55a14",38605:"98b64833",38624:"16f902ef",38651:"8eeeab23",38746:"3da52cb8",38894:"4c35e904",39023:"f00d7f13",39248:"bbd2f2b4",39424:"135a1f73",39430:"0db45235",39489:"fb9d2358",39771:"cd1b03e2",39867:"496d72fd",40025:"2de00c67",40120:"14d0daf2",40204:"d2a2ed5e",40299:"4b4cafab",40303:"35c54516",40362:"4473c8ad",40377:"2add01ae",40525:"0d7fbf9d",40653:"70251457",40733:"bebbe2d9",40784:"08580559",40836:"97c4628b",4117:"f54065d8",41215:"62ec16ff",413:"23faf171",41307:"0c6205a2",41327:"c2c72039",41484:"a3544d61",41719:"1eb731ed",41867:"ee90b7b3",41907:"20758557",42070:"dfb4cdb3",42318:"ce7935bd",42325:"f9807c55",42647:"859cdb24",42657:"f9704823",42708:"00c6417e",42774:"6f27164d",42802:"9e996f2b",42880:"08e7f205",42927:"2ed6c2b0",42928:"d2c8ed8b",43104:"1bdaa100",43137:"8ca93559",43239:"dcc4d4aa",433:"46bbbe63",43451:"c7c5c433",43465:"3a6add66",43658:"a1b86ccd",43682:"29f951c2",43718:"41fd9e07",43823:"8f3247f3",43958:"acab790c",44179:"f97ec499",4444:"ddfddd4a",44663:"7d400be2",4477:"35c45699",44986:"e8f6f573",45062:"ebdaf6e3",45145:"b90f22af",4531:"e37fe5fc",45453:"abdfc427",45454:"1cc38c32",45485:"10b8f6d0",45488:"3ac64fde",45576:"975beb7b",45623:"eb0321f7",45624:"fc8aede4",45740:"d4cfbefc",45776:"c683aabf",45847:"dece5c52",4587:"98211511",45886:"bf8b812b",45906:"a0ca96f1",45942:"cd9813b4",45971:"f4c4948f",46042:"56a88521",46131:"7df3ea2e",46149:"2c066cd8",46159:"80e34ed6",4617:"6caee24c",46176:"70fbf2aa",46233:"6a9c853d",46377:"f6f0ea8f",4640:"28158a85",46559:"ebdc0ac4",46707:"ad7c6ad2",46715:"6ec0b7f7",4693:"9f1d3a00",46973:"706b8b5a",47022:"b69436d1",47185:"a6a66801",47249:"22ec13a1",47279:"85d9bdb7",47301:"36b62a47",47345:"9a119fb4",47535:"38580ead",47650:"9b6e45b4",47734:"36ebd53f",47880:"35fc3994",47936:"c650ae65",47940:"13e6f69d",47957:"443bd607",47995:"31abacd2",48359:"d36a66f6",48424:"7f098087",48483:"d57946f2",4859:"515c7718",48656:"4fe45945",48665:"09236643",48717:"f75be50f",48771:"7fd8738f",49026:"b862f26c",49112:"d5c9d2d4",49184:"8fce41e4",49255:"da83b70a",49329:"62a3dede",49401:"355a102a",49426:"57389f4e",49469:"030b82d5",49566:"7ac26e2c",4965:"75851805",49718:"da73c8f8",50003:"b04a7eba",50090:"6c8e3cdd",50108:"310d7b68",502:"14c7cdf2",50237:"e37b124a",50302:"085acd9d",50324:"16c80245",50373:"efde0a06",50685:"e581b388",50713:"826dc03e",51014:"c464970e",51058:"bb2f273e",51112:"3ecc417b",51309:"4df8dce1",51338:"8765d75d",51457:"f1324fd8",51482:"6d7a93e3",51517:"7728e071",51643:"5e99b0e6",51656:"77d101ae",51767:"80feef31",5214:"9a366a87",52157:"894ac86a",52167:"ce228989",52392:"7d18af8e",52474:"3b1ce1dc",5250:"688bf571",5264:"a1b1b8b6",52692:"a4079c57",52712:"848094cb",52744:"697bad59",52947:"b66a427e",53100:"080dec18",53143:"7f4f7348",53251:"2ea5ba84",53412:"27385a0f",53426:"efa2022f",53498:"370525dd",53519:"bffc8201",53616:"11743d96",53642:"7946d620",5370:"4ada1671",53887:"b301182c",54023:"95e35de0",54037:"01f8f110",54077:"d76a05bc",5413:"21aee523",54144:"72776ab3",54209:"e6801c3b",54340:"24745d4b",54397:"84345093",54437:"c9b0b274",54534:"1c29f530",54568:"68d7bc92",54700:"8fcd167f",54757:"aa1dac2d",54855:"896de8ab",54868:"a647eba7",55045:"e0390712",55096:"141e5379",55197:"b49fc74c",55199:"6e014bbe",55295:"bb1a2120",55512:"0eca4600",55539:"6d95271c",55871:"efde14d5",55947:"456f4923",55997:"8dc3d48c",56171:"c08b19b8",56545:"6ee5a447",56679:"46a7195c",56761:"5affd063",56802:"0e0f8cab",5701:"877e83c8",57073:"2b80fcd6",57094:"144b6b73",57276:"ae6ee5f0",57455:"da052510",57534:"d809ae9a",57690:"377ffd97",57713:"d19a4df7",57862:"40bbd4e7",57898:"16437e66",58045:"afc14d52",58048:"df85f312",58318:"7fcf9a10",58450:"fd9d3277",58839:"0283c3a1",58941:"5f6c9433",59052:"6ab316b9",5919:"ae3247dd",59190:"70e821e7",59261:"15920a41",59265:"f4e6f31e",596:"6cbe6938",59633:"aa05a825",59687:"c5b82a9c",59708:"5ca10b7b",59744:"d688071c",59845:"a331734f",5992:"9490aefe",60234:"a1c0b28a",6026:"a70cc901",60700:"d0e56898",60849:"2755a221",61098:"4f17f116",61109:"ac20e9d3",61167:"9a610938",61203:"b3a9b1d8",61339:"d3551880",61422:"956f8ea8",61445:"bb07a853",61639:"86817191",61746:"d337065f",6175:"89d3da13",62078:"4be0196c",62093:"e2d7ee3e",62177:"8cfb9b4f",62350:"16b4415d",62355:"3f3d3087",62406:"2f78acfa",62429:"8a68e4f6",62438:"713419d3",62519:"adb3cb4e",62575:"51e11f17",62584:"0748a13e",62608:"37bb2846",62688:"023bfcde",6279:"0dd59d00",628:"a165f287",62820:"9db10007",63165:"ccf00fe1",63181:"99a96536",6337:"5aa547ba",63378:"58568b28",63394:"3e8c0b5f",63656:"34a249cc",63845:"caffc632",63861:"7ae75a90",63909:"83dcc12f",64004:"9186cbf4",64093:"9f2db64f",64229:"4c2ab342",64375:"e2b71fcd",64389:"8c14ed9c",64528:"3b7f7f78",64592:"c14ada26",6475:"addba162",64758:"cf1e4d3a",64972:"88b5df4d",65044:"6a4d525d",65133:"7d16bc40",6532:"bd970be4",65409:"76123d2a",65438:"a8889520",65569:"e333d2d2",6591:"48e47128",6592:"4718f75f",6599:"afc5a0c0",66066:"4546070d",6611:"3b6ddce7",66113:"185fe4ec",66119:"484d35a8",66202:"233a75ac",66232:"eb2658e3",66271:"2733ad80",66577:"4f1978e5",66836:"5205853f",67152:"ab9a01bf",67182:"600666cc",67183:"5c96a274",67186:"c56f0d19",67358:"125fc249",67388:"061d9c85",67389:"87dfbba2",67448:"709be521",67742:"f383536b",67768:"7d1d9191",68023:"bbc3c159",68046:"9aa249e2",68051:"f3f5e761",68127:"09b90613",68162:"44551abd",68305:"08060e11",68518:"c9c884a1",68612:"90625041",6864:"a703c7b5",68663:"cc4d81e5",68692:"83fa58e0",68892:"8d290ce4",68907:"3e32a7f9",69e3:"8dc4fb6c",69043:"454aee60",69128:"63051216",69243:"53855910",69244:"c72143c9",69642:"13948c33",7e3:"aedb9ce2",70176:"dfbb4841",7039:"01f4f95c",70459:"b170a6f1",70520:"a7cb8083",70635:"49db0f9c",70685:"c3ce58f9",70719:"ae6d2d03",70740:"7c86bca5",70773:"3930976a",71092:"0223c7a4",71119:"524ed0f2",71323:"63578916",71407:"400c7283",71518:"cb731422",71693:"7c6b3a04",71729:"f2323c48",7173:"af03e2a3",71977:"101d001f",71991:"4f969c57",72106:"5a2fa076",72146:"edcf0919",72288:"d4a71a4a",72429:"7da348e7",72878:"a10ab629",72903:"a26a3568",73033:"b6c5d82f",73072:"6b95c216",73113:"a54c3c75",7318:"d7d2824c",732:"a0870db7",73374:"187dfb3b",73766:"172f5da9",73784:"48a52515",73825:"104a0620",73956:"69fcfa80",74028:"334773c1",74078:"83f2d1cd",74085:"e67e6a1d",74119:"34d1f92e",74129:"d34e2d26",74296:"7e47e4bd",74462:"504141b6",7451:"4e13bab6",74561:"8df0e38e",74684:"05017d50",74695:"b46b776f",74742:"316044f9",74880:"31b006d4",7509:"92fcccb2",75498:"86af536d",755:"1e72ff2a",75641:"f20a757b",75802:"e45a8753",76130:"c602b3df",76142:"69e575c5",76297:"24a6bf30",76384:"3f26c003",76465:"84da78ec",76555:"fead84fd",7663:"0a79738c",76725:"283e98c9",76794:"c0868960",76905:"2ae2c9b9",76957:"b20e68d8",77454:"a3315458",77458:"99bc5074",77588:"f42adcb9",77839:"d2f84525",78054:"6d651eba",78159:"f9af2525",78197:"5778d9e0",78351:"8b5ce022",78448:"8dd8d3c7",78480:"124a7cb0",78600:"9af5613a",78642:"eef37fda",78683:"3618ace5",78723:"05163422",78849:"4c9d242a",78935:"b43e09f1",78955:"382ce295",78987:"0e87d100",79014:"4008d1e4",79200:"281d8389",79278:"a80e071e",79306:"0aa3b340",7932:"28f8b7cb",79469:"46586a4c",7968:"af593064",79681:"9a18b3a0",79797:"36a62c36",79858:"71479019",79929:"6121264c",80131:"18edd5f4",80230:"8bb7f31b",80388:"156292fe",80760:"407785c2",80844:"35110b5e",81125:"5c0e2486",81569:"4896c542",81603:"5a477491",81642:"050cdc3e",81695:"1ca67e63",81750:"71e2936d",81979:"0a5b104f",8201:"bab93392",82110:"cc6086c5",82238:"5a992ca1",82268:"eef26366",82311:"62476be9",8239:"f12cac91",82458:"2ce25501",82505:"4b3e2c18",8256:"027af7db",82749:"a3f91933",82811:"0f1788d8",82927:"7cdcd1f8",8301:"80f4db42",83026:"b43eee66",8335:"b7f5a15d",83362:"caa0bd18",83366:"5fe3e58b",83508:"e67e6220",83598:"70a46a92",83649:"1fc55112",83797:"9b4e920c",83873:"405743bf",84059:"88ba2dd5",84240:"14652a9e",84305:"a3d110aa",84368:"e28d6c87",84371:"c1e48310",84399:"68be4760",84442:"4d9d6c7d",84494:"134a4cc7",84505:"7e0696d8",84754:"cc7054d0",84967:"635e4c22",8515:"27a08389",85159:"e9ba41cb",85563:"6f6ad158",85868:"c3165ad8",85893:"de286fbd",86:"d3d747c8",86006:"f213d5cd",86011:"de5d0723",86084:"a401b2d9",8621:"32b45c16",86255:"db40cc4f",86428:"b40b4c01",86543:"6f938819",86666:"46956b10",8689:"52fab1f9",86998:"a135eb78",87067:"3dcb1386",87324:"504d7087",8739:"8deb0e5b",87502:"11c2ed10",87514:"8acd65bd",87521:"56fb6d2d",87586:"f563e16c",87784:"f20939c0",87792:"f6ba9bc1",88:"179ebcef",88037:"677a906b",88097:"290018df",88365:"40dabf78",88399:"98b4d7f5",88523:"f601626e",88759:"98edd658",88782:"2f903853",88790:"8829e39b",88856:"d5a07daa",8903:"64937915",89084:"fd608bc8",89116:"74468186",89129:"0ffb161b",89183:"b8205e5d",89752:"01354550",89940:"17c9a214",89947:"c701d040",89980:"dbeb748a",90:"4b27a20d",9e3:"f5f9623e",9006:"a1491482",90263:"ed8d5c14",904:"d809a2cb",90527:"a06c2abb",90561:"9f9deab7",90624:"226fb28e",90833:"fd26040e",90880:"9b4a701d",90899:"d7ae3657",90905:"b820037d",90934:"d4970c39",90981:"bd6cb732",91087:"c25ad8cc",91205:"fb0d5ac0",91212:"01a6d6bc",91227:"96cf2218",91231:"e03bf9e3",91335:"290ccc3c",91407:"94e7aa0f",91499:"cf295826",91588:"2531ba8c",91701:"f9e23389",91740:"65584ef2",92:"83ceb158",92069:"5973a4a7",92083:"93be4a64",92150:"f22886c0",9229:"5feafaf2",92551:"e76dd913",92560:"2f4fb601",92627:"7d9b816b",9272:"08c5aba6",92744:"2605188f",92758:"a596a715",92859:"e1842933",92986:"34059585",93039:"1ab22a99",93198:"e020b87e",93274:"1fd9a797",93374:"548adf3f",93377:"100f9e5b",93643:"1ddeb969",93703:"90f0a64d",93765:"53cf8c94",93773:"049be34a",9379:"8fd5fa75",9389:"95f253b6",94053:"4281e9f3",94161:"b51b843b",94393:"08f41d90",94464:"15b79fb8",94521:"2ed1a0e4",94560:"e204823b",94570:"0f2f163f",94678:"23a0300f",9491:"5af86c32",94950:"11455a02",95098:"7ed22b6f",95143:"3b94441d",95200:"89a088f9",95228:"d7a466ce",95253:"22239470",95331:"7b606e2a",95611:"f6addc75",95793:"a0abfb6d",95858:"b84e99ee",95969:"584b139b",9646:"5446a50f",96676:"93195645",96684:"db586427",96711:"88010ff0",96762:"2d3ae337",96771:"c26a4843",96860:"5e99ba1f",97171:"afa2f552",97355:"49d53253",97472:"647ee87c",97488:"d2f5af70",97689:"234e4f5e",97852:"bd633157",97855:"3edcfa97",97861:"b5740b4d",9791:"07257477",97916:"dc2f75be",9796:"e8435ce6",9797:"61369d8c",9823:"3b90e5ce",98288:"7b6a238b",98342:"dea5b472",98509:"13fae561",98560:"fa76a142",98678:"365301a3",98694:"8ff4e2e1",98936:"c0f8a3f4",99081:"9a56ed39",99194:"09a64ccb",9967:"8f3af36f",99716:"d13ebc6c"}[e]+".js",a.miniCssF=e=>"static/css/"+{10457:"5580853a",11665:"29604072",11722:"8ba10772",12414:"efc43f44",16362:"65c9d7ad",16578:"5589f57d",17400:"20bd6671",177:"efc43f44",17715:"5580853a",18285:"42ab2d87",18455:"0ae41131",19236:"8ba10772",19462:"433940aa",20178:"013b7c09",23568:"254c5154",23730:"9787ecdb",26195:"c11b9d95",27834:"72ce80d1",28005:"d9378a9a",28303:"0ae41131",29127:"d9378a9a",29677:"bdb3a2a4",29737:"2f4b6d21",31607:"6023956a",36124:"8dc94a61",36599:"f9f6c314",3675:"2f4b6d21",37528:"0ef8c18a",39023:"270ec8ee",39771:"9f62606e",40299:"270ec8ee",42318:"270ec8ee",42657:"d9378a9a",43104:"f01b57aa",43239:"5580853a",43451:"8bd5d3ec",45062:"9787ecdb",45942:"f9f6c314",46042:"8ba10772",46131:"254c5154",46149:"ce4c2546",47279:"2f4b6d21",49669:"5580853a",50003:"f9f6c314",52646:"edfae5cd",53251:"049d66d5",53642:"5580853a",54077:"65c9d7ad",54568:"d127bbaa",56171:"0ef8c18a",57534:"254c5154",58318:"db292439",59052:"d29ccc7e",5919:"2f4b6d21",6026:"633496d8",61098:"95dbb17f",63181:"d22254bd",6475:"bc957c3c",64758:"9787ecdb",66271:"42ab2d87",67152:"bdb3a2a4",67388:"42ab2d87",69243:"d2277bc0",7e3:"65c9d7ad",70635:"20bd6671",72050:"5580853a",72878:"e8a1f1c9",74078:"270ec8ee",75641:"254c5154",81750:"9787ecdb",8201:"8dc94a61",84305:"6fb050d6",8515:"5580853a",86998:"02c6e368",87502:"51c4195b",87521:"28b19ea5",88782:"254c5154",90981:"d2277bc0",92986:"8d6406ef",95200:"e4a568bf",96684:"5580853a",9967:"37acbf0a"}[e]+".chunk.css",a.g=(()=>{if(typeof globalThis=="object")return globalThis;try{return this||new Function("return this")()}catch{if(typeof window=="object")return window}})(),a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),(()=>{var e={},r="futures-trade-ui:";a.l=function(c,f,d,b){if(e[c]){e[c].push(f);return}var t,o;if(d!==void 0)for(var i=document.getElementsByTagName("script"),n=0;n{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e=[];a.O=(r,c,f,d)=>{if(c){d=d||0;for(var b=e.length;b>0&&e[b-1][2]>d;b--)e[b]=e[b-1];e[b]=[c,f,d];return}for(var t=1/0,b=0;b=d)&&Object.keys(a.O).every(u=>a.O[u](c[i]))?c.splice(i--,1):(o=!1,d"1.7.11",(function(){var e=function(){if(typeof globalThis<"u")return globalThis;if(typeof self<"u")return self;if(typeof window<"u")return window;if(typeof global<"u")return global;if(typeof this<"u")return this;throw new Error("Unable to locate global `this`")},r=e();r.__futures_trade_ui_dynamic_public_path__&&(a.p=r.__futures_trade_ui_dynamic_public_path__)})(),(()=>{if(!(typeof document>"u")){var e=function(d,b,t,o,i){var n=document.createElement("link");n.rel="stylesheet",n.type="text/css",a.nc&&(n.nonce=a.nc),n.href=b;var s=function(l){if(n.onerror=n.onload=null,l.type==="load")o();else{var p=l&&(l.type==="load"?"missing":l.type),g=l&&l.target&&l.target.href||b,u=new Error("Loading CSS chunk "+d+" failed.\\n("+g+")");u.code="CSS_CHUNK_LOAD_FAILED",u.type=p,u.request=g,n.parentNode&&n.parentNode.removeChild(n),i(u)}};return n.onerror=n.onload=s,t?t.parentNode.insertBefore(n,t.nextSibling):document.head.appendChild(n),n},r=function(d,b){for(var t=document.getElementsByTagName("link"),o=0;o{if((!a.o(f,d)||f[d]===void 0)&&/^(4(3(104|239|451)|6(042|131|149)|(029|727|966)9|2318|2657|5062|5942)|2(9(12|67|73)7|0178|3568|3730|6195|7834|8005|8303)|5(0003|2646|3251|3642|4077|4568|6171|7534|8318|9052|919)|6(475(|8)|026|1098|3181|6271|7152|7388|9243)|1(7(400|7|715)|(172|636|946)2|(166|828|845)5|0457|2414|6578|9236)|8(75(02|21)|1750|201|4305|515|6998|8782)|7((28|40)78|000|0635|2050|5641)|3(6(124|599|75)|1607|7528|9023|9771)|9(0981|2986|5200|6684|967))$/.test(d)){f[d]=null;var b=document.createElement("link");a.nc&&b.setAttribute("nonce",a.nc),b.rel="prefetch",b.as="style",b.href=a.p+a.miniCssF(d),document.head.appendChild(b)}}}})(),(()=>{var e={5547:0};a.f.j=function(f,d){var b=a.o(e,f)?e[f]:void 0;if(b!==0)if(b)d.push(b[2]);else if(/^(49669|52646|5547|72050)$/.test(f))e[f]=0;else{var t=new Promise((s,l)=>b=e[f]=[s,l]);d.push(b[2]=t);var o=a.p+a.u(f),i=new Error,n=function(s){if(a.o(e,f)&&(b=e[f],b!==0&&(e[f]=void 0),b)){var l=s&&(s.type==="load"?"missing":s.type),p=s&&s.target&&s.target.src;i.message="Loading chunk "+f+` failed. +(`+l+": "+p+")",i.name="ChunkLoadError",i.type=l,i.request=p,b[1](i)}};a.l(o,n,"chunk-"+f,f)}},a.F.j=f=>{if((!a.o(e,f)||e[f]===void 0)&&!/^(49669|52646|5547|72050)$/.test(f)){e[f]=null;var d=document.createElement("link");a.nc&&d.setAttribute("nonce",a.nc),d.rel="prefetch",d.as="script",d.href=a.p+a.u(f),document.head.appendChild(d)}},a.O.j=f=>e[f]===0;var r=(f,d)=>{var[b,t,o]=d,i,n,s=0;if(b.some(p=>e[p]!==0)){for(i in t)a.o(t,i)&&(a.m[i]=t[i]);if(o)var l=o(a)}for(f&&f(d);s{var e={86998:["86666","41327","86666","41327","86666","41327","86666","41327"],54568:["19804","86666","74462","47650","1042","97916","37511","64229","13153","72878","86666","41327","86666","41327"],74078:["19804","86666","74462","47650","1042","97916","37511","64229","13153","72878","86666","41327","86666","41327"],38651:["19804","86666","74462","47650","1042","97916","37511","64229","13153","72878"],40299:["19804","86666","74462","47650","1042","97916","37511","64229","13153","72878","86666","41327","86666","41327"],42318:["19804","86666","74462","47650","1042","97916","37511","64229","13153","72878","86666","41327","86666","41327"],39023:["19804","86666","74462","47650","1042","97916","37511","64229","13153","72878","86666","41327","86666","41327"]};a.f.prefetch=(r,c)=>{Promise.all(c).then(()=>{var f=e[r];Array.isArray(f)&&f.map(a.E)})}})()})(); diff --git a/test/unit/binance-chart-storage-native-preflight.test.js b/test/unit/binance-chart-storage-native-preflight.test.js new file mode 100644 index 0000000..e887071 --- /dev/null +++ b/test/unit/binance-chart-storage-native-preflight.test.js @@ -0,0 +1,328 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { runInNewContext } from 'node:vm'; +import { startNativeMirrorPreflight } from '../../experiments/binance-chart-storage/native-preflight-core.js'; +import { compileNativePreflight } from '../../experiments/binance-chart-storage/build-native-preflight.js'; +import { createNativeMirrorFactory } from '../fixtures/binance-chart-storage/mirror-scoped-callback.js'; + +const PAGE = 'https://www.binance.com/zh-CN/futures/USUSDT'; +const QUEUE = 'webpackChunkfutures_trade_ui'; + +/** Browser globals are the external boundary; storage access is forbidden in every scenario. */ +function installPage(t, { url = PAGE, framed = false, queue } = {}) { + const events = new EventTarget(); + const keys = ['location', 'self', 'top', 'addEventListener', 'removeEventListener', 'indexedDB', 'localStorage', QUEUE]; + const saved = new Map(keys.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)])); + let storageAccesses = 0; + const values = { + location: new URL(url), self: globalThis, top: framed ? {} : globalThis, + addEventListener: events.addEventListener.bind(events), removeEventListener: events.removeEventListener.bind(events), + }; + for (const [key, value] of Object.entries(values)) Object.defineProperty(globalThis, key, { value, configurable: true }); + for (const key of ['indexedDB', 'localStorage']) Object.defineProperty(globalThis, key, { + get() { storageAccesses += 1; throw new Error('Storage access is forbidden'); }, configurable: true, + }); + if (queue) Object.defineProperty(globalThis, QUEUE, { value: queue, configurable: true, writable: true }); + else delete globalThis[QUEUE]; + t.after(() => { + events.dispatchEvent(new Event('pagehide')); + assert.equal(storageAccesses, 0); + for (const [key, descriptor] of saved) { + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete globalThis[key]; + } + }); + return { events }; +} + +function start(t) { + const session = startNativeMirrorPreflight(); + t.after(() => session.stop()); + return session; +} + +test('user keeps the original native array and one synchronous invocation without inspecting chart values', t => { + // Given the real preflight starts before an untouched page runtime and chart entries prohibit value reads. + installPage(t); + const session = start(t); + const entries = new Array(2); + Object.defineProperty(entries, 0, { get() { throw new Error('Chart values must remain unread'); } }); + const nativeResult = [Promise.resolve('native')]; + let calls = 0; + + // When the host dispatches one mirror through the native-only service. + const result = session.dispatch({}, entries, () => { calls += 1; return nativeResult; }); + + // Then the original array returns immediately with exactly one invocation and only aggregate counts. + assert.equal(result, nativeResult); + assert.equal(calls, 1); + assert.equal(session.snapshot().dispatches, 1); + assert.equal(session.snapshot().entryCount, 2); + assert.equal(session.snapshot().active, true); +}); + +test('user receives the identical synchronous native exception without diagnostic wrapping', t => { + // Given an active preflight and a native operation that rejects synchronously. + installPage(t); + const session = start(t); + const failure = new Error('Native failure identity'); + let calls = 0; + + // When the native thunk throws through the observed dispatch. + const invoke = () => session.dispatch({}, [], () => { calls += 1; throw failure; }); + + // Then the exact exception escapes once while observation does not misclassify it as a diagnostic failure. + assert.throws(invoke, error => error === failure); + assert.equal(calls, 1); + assert.equal(session.snapshot().failure, null); + assert.equal(session.snapshot().dispatches, 1); +}); + +test('user stops counting while retained native callbacks remain synchronous and unchanged', t => { + // Given an active preflight owns the previously absent runtime queue. + installPage(t); + const session = start(t); + const nativeResult = []; + let calls = 0; + + // When observation stops and the host later invokes the retained dispatcher. + session.stop(); + const result = session.dispatch({}, [['unread', {}]], () => { calls += 1; return nativeResult; }); + + // Then stop removes the unused queue, freezes counters and preserves native return identity. + assert.equal(result, nativeResult); + assert.equal(calls, 1); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); + assert.equal(session.snapshot().active, false); + assert.equal(session.snapshot().stopReason, 'manual'); + assert.equal(session.snapshot().dispatches, 0); + assert.equal(session.snapshot().entryCount, 0); +}); + +test('user automatically stops observation exactly at the thirty second deadline', t => { + // Given the preflight uses the controlled browser deadline clock. + installPage(t); + t.mock.timers.enable({ apis: ['setTimeout'] }); + const session = start(t); + + // When elapsed time reaches the observation deadline from its final active millisecond. + t.mock.timers.tick(29_999); + const before = session.snapshot(); + t.mock.timers.tick(1); + const result = session.dispatch({}, [], () => 'native-after-deadline'); + + // Then counting stops at the boundary and later native calls remain enabled. + assert.equal(before.active, true); + assert.equal(session.snapshot().active, false); + assert.equal(session.snapshot().stopReason, 'deadline'); + assert.equal(session.snapshot().dispatches, 0); + assert.equal(result, 'native-after-deadline'); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); +}); + +test('user retires observation on pagehide without changing later native errors', t => { + // Given the preflight owns a page lifecycle listener. + const page = installPage(t); + const session = start(t); + const failure = new Error('Native after pagehide'); + + // When the page leaves and the retained native thunk subsequently fails. + page.events.dispatchEvent(new Event('pagehide')); + const invoke = () => session.dispatch({}, [], () => { throw failure; }); + + // Then cleanup precedes the native failure and neither counting nor queue interception remains active. + assert.throws(invoke, error => error === failure); + assert.equal(session.snapshot().active, false); + assert.equal(session.snapshot().stopReason, 'pagehide'); + assert.equal(session.snapshot().dispatches, 0); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); +}); + +test('user retires counters after a route change while preserving the pending native mirror', t => { + // Given the experiment began on its only supported top-level URL. + installPage(t); + const session = start(t); + const nativeResult = []; + + // When a retained mirror callback runs after an SPA route change. + location.pathname = '/zh-CN/futures/DIAUSDT'; + const result = session.dispatch({}, [], () => nativeResult); + + // Then the new route receives no diagnostic admission while its original callback still executes. + assert.equal(result, nativeResult); + assert.equal(session.snapshot().active, false); + assert.equal(session.snapshot().stopReason, 'scope_changed'); + assert.equal(session.snapshot().dispatches, 0); +}); + +test('user keeps the native result when entry-count observation fails without exposing the thrown message', t => { + // Given only the diagnostic length read fails and its exception includes an untrusted message. + installPage(t); + const session = start(t); + const entries = new Proxy([], { get() { throw new Error('Untrusted diagnostic detail'); } }); + const nativeResult = []; + let calls = 0; + + // When the diagnostic boundary encounters the failed count read before native execution. + const result = session.dispatch({}, entries, () => { calls += 1; return nativeResult; }); + + // Then the original thunk runs once and the public failure contains only fixed name and code fields. + assert.equal(result, nativeResult); + assert.equal(calls, 1); + assert.deepEqual(session.snapshot().failure, { name: 'PreflightError', code: 'entry_count_unavailable' }); + assert.equal(session.snapshot().active, false); + assert.equal(JSON.stringify(session.snapshot()).includes('Untrusted diagnostic detail'), false); +}); + +test('user rejects source drift before registration while preserving the original host factory', async t => { + // Given a real bootstrap observes an unsupported host factory source. + installPage(t); + const session = start(t); + const original = function changedHost(module) { module.exports = 'native-source-drift'; }; + const chunk = [['source-drift'], { 70940: original }]; + + // When the host registers its changed factory and then executes the preserved registration. + globalThis[QUEUE].push(chunk); + await Promise.resolve(); + const module = { exports: {} }; + globalThis[QUEUE][0][1][70940](module); + + // Then matching fails visibly while the unchanged host module remains executable. + assert.equal(globalThis[QUEUE][0][1][70940], original); + assert.equal(module.exports, 'native-source-drift'); + assert.equal(session.snapshot().attempts, 1); + assert.equal(session.snapshot().matches, 0); + assert.deepEqual(session.snapshot().failure, { name: 'PreflightError', code: 'source_mismatch' }); + assert.equal(session.snapshot().active, false); +}); + +test('user matches the exact public factory and restores registration when stopping before execution', t => { + // Given the complete pinned public factory accompanies a real pre-runtime bootstrap. + installPage(t); + const session = start(t); + const original = createNativeMirrorFactory(); + + // When the exact source registers and the user stops before the host executes it. + globalThis[QUEUE].push([['pinned-source'], { 70940: original }]); + const accepted = session.snapshot(); + session.stop(); + + // Then one exact match is recorded and cleanup restores the original registration without executing it. + assert.equal(accepted.attempts, 1); + assert.equal(accepted.matches, 1); + assert.equal(accepted.executions, 0); + assert.equal(globalThis[QUEUE][0][1][70940], original); + assert.equal(Object.hasOwn(globalThis[QUEUE], 'push'), false); +}); + +test('user retains host execution errors after the exact full factory enters its original dependency boundary', async t => { + // Given the exact full factory is accepted and its external module resolver rejects a dependency. + installPage(t); + const session = start(t); + const original = createNativeMirrorFactory(); + globalThis[QUEUE].push([['pinned-execution'], { 70940: original }]); + const wrapped = globalThis[QUEUE][0][1][70940]; + const failure = new Error('Native dependency unavailable'); + const require = () => { throw failure; }; + require.m = { 70940: wrapped }; + require.r = exports => { Object.defineProperty(exports, '__esModule', { value: true }); }; + require.d = (exports, getters) => { + for (const [key, get] of Object.entries(getters)) Object.defineProperty(exports, key, { get }); + }; + + // When the original first dependency request fails inside the statically replaced factory. + assert.throws(() => wrapped({ exports: {} }, {}, require), error => error === failure); + await Promise.resolve(); + + // Then the host receives its own error while observation records one attempted execution and a fixed failure. + assert.equal(session.snapshot().executions, 1); + assert.equal(session.snapshot().completed, false); + assert.equal(require.m[70940], original); + assert.deepEqual(session.snapshot().failure, { name: 'PreflightError', code: 'module_execution_failed' }); +}); + +test('user rejects a late bootstrap without replacing the existing runtime dispatcher', t => { + // Given the page runtime already owns the queue dispatcher. + const queue = []; + const push = function runtimePush(chunk) { return Array.prototype.push.call(this, chunk); }; + queue.push = push; + installPage(t, { queue }); + + // When the preflight starts after runtime ownership has been established. + const session = start(t); + + // Then rejection is diagnostic only and leaves the host queue untouched. + assert.equal(queue.push, push); + assert.equal(queue.length, 0); + assert.equal(session.snapshot().active, false); + assert.deepEqual(session.snapshot().failure, { name: 'PreflightError', code: 'bootstrap_rejected' }); +}); + +for (const [url, framed] of [ + [PAGE + '?changed=1', false], [PAGE + '#changed', false], [PAGE, true], + ['https://chart-storage.test', false], ['https://www.binance.com/zh-CN/futures/DIAUSDT', false], +]) { + test(`user cannot activate native preflight outside its exact top-level URL at ${url} framed=${framed}`, t => { + // Given the execution context differs from the one explicitly supported incident page. + installPage(t, { url, framed }); + + // When the native-only entrypoint attempts to start. + const session = start(t); + + // Then scope rejection happens without creating the runtime queue or accessing storage. + assert.equal(session.snapshot().active, false); + assert.deepEqual(session.snapshot().failure, { name: 'PreflightError', code: 'unsupported_page' }); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); + }); +} + +test('user receives a deterministic static artifact with only the intended native-only dependencies', async () => { + // Given the checked-in local artifact and its dedicated in-memory compiler share one metadata source. + const artifact = await readFile(new URL('../../experiments/binance-chart-storage/native-preflight.user.js', import.meta.url), 'utf8'); + + // When the standalone experiment is compiled without writing files. + const built = await compileNativePreflight(); + + // Then its metadata, static dependency graph and generated bytes contain no writer or remote loader. + assert.equal(built.code, artifact); + assert.match(artifact, /@version\s+0\.0\.1\n/); + assert.match(artifact, /@match\s+https:\/\/www\.binance\.com\/zh-CN\/futures\/USUSDT\n/); + for (const directive of ['@sandbox raw', '@grant none', '@noframes', '@run-at document-start']) { + assert.equal(artifact.split('\n').some(line => line.replace(/\s+/g, ' ').trim() === '// ' + directive), true); + } + assert.doesNotMatch(artifact, /@(?:require|updateURL|downloadURL|resource)\b|\beval\s*\(|\bnew\s+Function\s*\(/); + assert.deepEqual(built.inputs.sort(), [ + 'experiments/binance-chart-storage/bootstrap.js', + 'experiments/binance-chart-storage/mirror-module.js', + 'experiments/binance-chart-storage/native-preflight-core.js', + 'experiments/binance-chart-storage/native-preflight-entry.user.js', + ]); +}); + +test('user receives only snapshot and stop controls from the generated userscript', async t => { + // Given an isolated page realm and the exact local generated artifact. + const artifact = await readFile(new URL('../../experiments/binance-chart-storage/native-preflight.user.js', import.meta.url), 'utf8'); + const events = new EventTarget(); + const page = { location: new URL(PAGE), setTimeout, clearTimeout, + addEventListener: events.addEventListener.bind(events), removeEventListener: events.removeEventListener.bind(events) }; + page.self = page; + page.top = page; + t.after(() => page.__BINANCE_MIRROR_PREFLIGHT__.stop()); + + // When the userscript executes and its public stop control is used. + runInNewContext(artifact, page); + const api = page.__BINANCE_MIRROR_PREFLIGHT__; + const before = api.snapshot(); + api.stop(); + runInNewContext(artifact, page); + + // Then only two frozen controls are exposed and repeated injection preserves the existing owner. + assert.deepEqual(Object.keys(api), ['snapshot', 'stop']); + assert.equal(Object.isFrozen(api), true); + assert.equal(before.active, true); + assert.equal(api.snapshot().active, false); + assert.equal(api.snapshot().stopReason, 'manual'); + assert.equal(page.__BINANCE_MIRROR_PREFLIGHT__, api); + assert.equal(Object.hasOwn(page, QUEUE), false); +}); diff --git a/test/unit/binance-chart-storage-origin.test.js b/test/unit/binance-chart-storage-origin.test.js new file mode 100644 index 0000000..9e6cb36 --- /dev/null +++ b/test/unit/binance-chart-storage-origin.test.js @@ -0,0 +1,107 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { installChartStoragePrototype } from '../../experiments/binance-chart-storage/adapter.js'; +import { observeChartStorageModule } from '../../experiments/binance-chart-storage/capture.js'; +import { observeChartStorageBootstrap } from '../../experiments/binance-chart-storage/bootstrap.js'; +import { startChartStoragePreflight } from '../../experiments/binance-chart-storage/preflight.js'; +import { createChartMirrorWriter } from '../../experiments/binance-chart-storage/mirror-writer.js'; + +for (const origin of ['https://www.binance.com', 'https://outside-storage.test', 'http://chart-storage.test']) { + test(`user cannot activate storage experiments on ${origin}`, () => { + // Given an environment outside the isolated HTTPS lab and untouched host entrypoints. + const previousLocation = Object.getOwnPropertyDescriptor(globalThis, 'location'); + Object.defineProperty(globalThis, 'location', { value: { origin }, configurable: true }); + const localforage = { createInstance() { throw new Error('Host factory must not execute'); } }; + const createInstance = localforage.createInstance; + const queue = []; + const push = queue.push; + try { + // When the lab-only public entrypoints are invoked outside their allowed origin. + assert.throws(() => installChartStoragePrototype(localforage), /isolated lab origin/); + assert.throws(() => observeChartStorageModule(queue), /isolated lab origin/); + assert.throws(() => createChartMirrorWriter(), /isolated lab origin/); + + // Then neither the host instance factory nor the module registration queue is changed. + assert.equal(localforage.createInstance, createInstance); + assert.equal(queue.push, push); + assert.equal(queue.length, 0); + } finally { + if (previousLocation) Object.defineProperty(globalThis, 'location', previousLocation); + else delete globalThis.location; + } + }); +} + +for (const [url, framed, scope, accepted] of [ + ['https://chart-storage.test', false, 'lab', true], + ['https://www.binance.com/zh-CN/futures/USUSDT', false, 'isolated-ususdt-probe', true], + ['https://www.binance.com/zh-CN/futures/USUSDT', false, undefined, false], + ['https://www.binance.com/zh-CN/futures/USUSDT', true, 'isolated-ususdt-probe', false], + ['https://www.binance.com/zh-CN/futures/home', false, 'isolated-ususdt-probe', false], + ['https://www.binance.com/zh-CN/futures/DIAUSDT', false, 'isolated-ususdt-probe', false], + ['http://www.binance.com/zh-CN/futures/USUSDT', false, 'isolated-ususdt-probe', false], + ['https://outside-storage.test/zh-CN/futures/USUSDT', false, 'isolated-ususdt-probe', false], + ['https://chart-storage.test', false, 'isolated-ususdt-probe', false], + ['https://chart-storage.test', false, 'unknown', false], +]) { + test(`user ${accepted ? 'can initialize' : 'cannot initialize'} the mirror writer for ${url} framed=${framed} scope=${scope}`, async () => { + // Given an explicit page scope and a storage boundary that must remain untouched at initialization. + const saved = new Map(['location', 'self', 'top', 'indexedDB'].map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)])); + Object.defineProperty(globalThis, 'location', { value: new URL(url), configurable: true }); + Object.defineProperty(globalThis, 'self', { value: globalThis, configurable: true }); + Object.defineProperty(globalThis, 'top', { value: framed ? {} : globalThis, configurable: true }); + let storageAccesses = 0; + Object.defineProperty(globalThis, 'indexedDB', { get() { storageAccesses += 1; throw new Error('Unexpected storage access'); }, configurable: true }); + try { + // When the caller requests only the selected scope without dispatching any records. + const create = () => createChartMirrorWriter({ scope }); + let writer; + if (accepted) writer = create(); + + // Then only the explicit valid scope admits an empty writer and no scope initializes a database. + if (accepted) { + assert.equal(writer.getStats().acceptedBatches, 0); + assert.equal(writer.getStats().transactions, 0); + await writer.stop(); + assert.equal(writer.getStats().pendingBatches, 0); + } else { + assert.throws(create, /Chart mirror writer requires|Unknown chart mirror scope/); + } + assert.equal(storageAccesses, 0); + } finally { + for (const [key, descriptor] of saved) { + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete globalThis[key]; + } + } + }); +} + +for (const [url, framed] of [ + ['https://outside-storage.test/zh-CN/futures/USUSDT', false], + ['http://www.binance.com/zh-CN/futures/USUSDT', false], + ['https://www.binance.com/zh-CN/futures/home', false], + ['https://www.binance.com/zh-CN/my/wallet/futures', false], + ['https://www.binance.com/zh-CN/futures/USUSDT', true], +]) { + test(`user cannot observe chart startup outside the pinned top-level page at ${url} framed=${framed}`, () => { + // Given a wrong origin, route or frame and no owned bootstrap queue. + const saved = new Map(['location', 'self', 'top'].map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)])); + Object.defineProperty(globalThis, 'location', { value: new URL(url), configurable: true }); + Object.defineProperty(globalThis, 'self', { value: globalThis, configurable: true }); + Object.defineProperty(globalThis, 'top', { value: framed ? {} : globalThis, configurable: true }); + try { + // When either observation entrypoint attempts to initialize on that page. + assert.throws(() => observeChartStorageBootstrap(), /top-level USUSDT incident page/); + assert.throws(() => startChartStoragePreflight(), /top-level USUSDT incident page/); + + // Then rejection precedes the creation of the page's runtime queue. + assert.equal(Object.hasOwn(globalThis, 'webpackChunkfutures_trade_ui'), false); + } finally { + for (const [key, descriptor] of saved) { + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete globalThis[key]; + } + } + }); +} diff --git a/test/unit/binance-chart-storage-probe.test.js b/test/unit/binance-chart-storage-probe.test.js new file mode 100644 index 0000000..26aa055 --- /dev/null +++ b/test/unit/binance-chart-storage-probe.test.js @@ -0,0 +1,61 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { captureAnonymousMirrorBatch } from '../../experiments/binance-chart-storage/anonymous-probe-contract.js'; + +test('user sees route and snapshot admission failures even when the host later completes a healthy mirror', () => { + // Given the probe owns its fixed top-level URL and has not accepted any records. + const saved = new Map(['location', 'self', 'top'].map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)])); + Object.defineProperty(globalThis, 'location', { value: new URL('https://www.binance.com/zh-CN/futures/USUSDT'), configurable: true }); + Object.defineProperty(globalThis, 'self', { value: globalThis, configurable: true }); + Object.defineProperty(globalThis, 'top', { value: globalThis, configurable: true }); + const target = {}; + const state = { failed: 0 }; + try { + // When a same-document route change or invalid chart value reaches the diagnostic boundary. + location.pathname = '/zh-CN/futures/DIAUSDT'; + assert.throws(() => captureAnonymousMirrorBatch(target, [], state), /left its fixed top-level URL/); + location.pathname = '/zh-CN/futures/USUSDT'; + assert.throws(() => captureAnonymousMirrorBatch(target, [['myTradingView', new Date()]], state), /Chart values must be plain JSON/); + const value = { revision: 2, nested: [1, 2] }; + const batch = captureAnonymousMirrorBatch(target, [['myTradingView', value]], state); + value.nested.push(3); + + // Then both failures remain counted and the healthy batch retains its independent input snapshot. + assert.equal(state.failed, 2); + assert.equal(batch.target, target); + assert.deepEqual(batch.entries, [['myTradingView', { revision: 2, nested: [1, 2] }]]); + } finally { + for (const [key, descriptor] of saved) { + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete globalThis[key]; + } + } +}); + +for (const [url, framed] of [ + ['https://www.binance.com/zh-CN/futures/USUSDT?changed=1', false], + ['https://www.binance.com/zh-CN/futures/USUSDT#changed', false], + ['https://www.binance.com/zh-CN/futures/USUSDT', true], +]) { + test(`user cannot capture a probe mirror after leaving its exact URL or frame at ${url} framed=${framed}`, () => { + // Given a retained caller now belongs to a changed URL or a child frame. + const saved = new Map(['location', 'self', 'top'].map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)])); + Object.defineProperty(globalThis, 'location', { value: new URL(url), configurable: true }); + Object.defineProperty(globalThis, 'self', { value: globalThis, configurable: true }); + Object.defineProperty(globalThis, 'top', { value: framed ? {} : globalThis, configurable: true }); + const state = { failed: 0 }; + try { + // When the retained callback attempts to prepare another mirror batch. + const capture = () => captureAnonymousMirrorBatch({}, [], state); + + // Then admission fails before even an empty batch reaches the writer. + assert.throws(capture, /left its fixed top-level URL/); + assert.equal(state.failed, 1); + } finally { + for (const [key, descriptor] of saved) { + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete globalThis[key]; + } + } + }); +} diff --git a/test/unit/binance-chart-storage-runtime.test.js b/test/unit/binance-chart-storage-runtime.test.js new file mode 100644 index 0000000..2eddf60 --- /dev/null +++ b/test/unit/binance-chart-storage-runtime.test.js @@ -0,0 +1,289 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { getEventListeners } from 'node:events'; +import { startChartStorageOptimizer } from '../../src/binance-orderbook-trade/chart-storage/runtime.js'; +import { createNativeMirrorFactory } from '../fixtures/binance-chart-storage/mirror-scoped-callback.js'; + +const PAGE = 'https://www.binance.com/zh-CN/futures/BTCUSDT'; +const QUEUE = 'webpackChunkfutures_trade_ui'; +const EMPTY_WRITER = { + acceptedBatches: 0, rejectedBatches: 0, failedBatches: 0, + pendingBatches: 0, pendingBytes: 0, peakPendingBatches: 0, peakPendingBytes: 0, + transactions: 0, committedTransactions: 0, abortedTransactions: 0, + committedWrites: 0, skippedWrites: 0, nativePassthroughBatches: 0, +}; + +/** Only browser globals are substituted; startup must never open native storage. */ +function installPage(t, { url = PAGE, framed = false, queue } = {}) { + const events = new EventTarget(); + const sessions = []; + const keys = ['location', 'self', 'top', 'addEventListener', 'removeEventListener', 'indexedDB', 'localStorage', QUEUE]; + const saved = new Map(keys.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)])); + let storageAccesses = 0; + const values = { + location: new URL(url), self: globalThis, top: framed ? {} : globalThis, + addEventListener: events.addEventListener.bind(events), removeEventListener: events.removeEventListener.bind(events), + }; + for (const [key, value] of Object.entries(values)) Object.defineProperty(globalThis, key, { value, configurable: true }); + for (const key of ['indexedDB', 'localStorage']) Object.defineProperty(globalThis, key, { + get() { storageAccesses += 1; throw new Error('Startup must not access native storage'); }, configurable: true, + }); + if (queue !== undefined) Object.defineProperty(globalThis, QUEUE, { value: queue, configurable: true, writable: true }); + else delete globalThis[QUEUE]; + t.after(async () => { + try { + await Promise.all(sessions.map(session => session.stop())); + assert.equal(storageAccesses, 0); + assert.equal(getEventListeners(events, 'pagehide').length, 0); + } finally { + for (const [key, descriptor] of saved) { + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete globalThis[key]; + } + } + }); + return { + events, + start() { + const session = startChartStorageOptimizer(); + sessions.push(session); + return session; + }, + }; +} + +for (const [url, framed] of [ + ['https://www.binance.com/', false], + ['https://www.binance.com/zh-CN/futures', false], + ['https://www.binance.com/zh-CN/futures/home', false], + ['https://www.binance.com/zh-CN/futures/BTCUSD', false], + ['https://www.binance.com/zh-CN/futures/BTCUSDT/details', false], + ['https://www.binance.com/zh-CN/futures/%ZZ', false], + ['https://www.binance.com/zh-CN/my/wallet/futures', false], + ['https://outside-storage.test/zh-CN/futures/BTCUSDT', false], + ['http://www.binance.com/zh-CN/futures/BTCUSDT', false], + [PAGE, true], +]) { + test(`user cannot initialize chart storage on ${url} framed=${framed}`, t => { + // Given the page differs from the supported top-level perpetual trading routes. + const page = installPage(t, { url, framed }); + + // When the actual optimizer entrypoint checks its page scope. + const start = () => page.start(); + + // Then rejection leaves the page queue and lifecycle listeners untouched. + assert.throws(start, { message: 'Chart storage requires a top-level Binance trading page' }); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); + assert.equal(getEventListeners(page.events, 'pagehide').length, 0); + }); +} + +for (const url of [ + PAGE, + 'https://www.binance.com/en/futures/ETHUSDC', + 'https://www.binance.com/futures/BTCUSDT/', + 'https://www.binance.com/zh-CN/futures/币安人生USDT', + 'https://www.binance.com/zh-TW/futures/1000PEPEUSDT?type=perpetual#chart', +]) { + test(`user can start and cleanly stop chart storage on ${url}`, async t => { + // Given a supported trading route has no page runtime or database initialization. + const page = installPage(t, { url }); + + // When the real runtime starts observation and then finishes an explicit stop. + const session = page.start(); + const waiting = session.snapshot(); + const listening = getEventListeners(page.events, 'pagehide').length; + const stopped = await session.stop(); + + // Then only fixed startup counters exist and all queue and listener ownership is released. + assert.deepEqual(waiting, { status: 'waiting', reason: null, attempts: 0, matches: 0, executions: 0, phase: 'active', writer: EMPTY_WRITER }); + assert.equal(listening, 1); + assert.deepEqual(stopped, { status: 'native', reason: 'manual', attempts: 0, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER }); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); + assert.equal(getEventListeners(page.events, 'pagehide').length, 0); + }); +} + +for (const kind of ['populated', 'runtime-owned']) { + test(`user preserves the native ${kind} queue when chart storage starts too late`, async t => { + // Given the host already populated its chunk queue or installed its own dispatcher. + const original = function nativeHost(module) { module.exports = 'native-late-start'; }; + const chunk = [['existing-host'], { 70940: original }]; + const queue = kind === 'populated' ? [chunk] : []; + if (kind === 'runtime-owned') queue.push = function runtimePush(value) { return Array.prototype.push.call(this, value); }; + const push = queue.push; + const before = Object.getOwnPropertyDescriptors(queue); + const page = installPage(t, { queue }); + + // When the optimizer encounters an already-owned registration boundary. + const session = page.start(); + const stopped = await session.stop(); + const module = { exports: {} }; + original(module); + + // Then native host execution and every existing queue descriptor remain unchanged. + assert.equal(globalThis[QUEUE], queue); + assert.equal(queue.push, push); + assert.deepEqual(Object.getOwnPropertyDescriptors(queue), before); + assert.equal(module.exports, 'native-late-start'); + assert.deepEqual(stopped, { status: 'native', reason: 'bootstrap_unavailable', attempts: 0, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER }); + }); +} + +test('user keeps the original factory executable when strict source registration mismatches', async t => { + // Given an early runtime observes a host factory that differs from the pinned module. + const page = installPage(t); + const session = page.start(); + const original = function changedHost(module) { module.exports = { chart: 'native-value' }; }; + const chunk = [['source-drift'], { 70940: original }]; + + // When the unchanged host factory registers and executes after the optimizer retires. + globalThis[QUEUE].push(chunk); + const stopped = await session.stop(); + const module = { exports: {} }; + globalThis[QUEUE][0][1][70940](module); + + // Then registration and its data stay native while diagnostics expose only fixed mismatch counts. + assert.equal(globalThis[QUEUE][0], chunk); + assert.equal(globalThis[QUEUE][0][1][70940], original); + assert.equal(Object.hasOwn(globalThis[QUEUE], 'push'), false); + assert.deepEqual(module.exports, { chart: 'native-value' }); + assert.deepEqual(stopped, { status: 'native', reason: 'source_mismatch', attempts: 1, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER }); +}); + +test('user restores the exact pinned factory when stopping before its first execution', async t => { + // Given the real runtime owns an empty queue and receives the complete pinned factory. + const page = installPage(t); + const session = page.start(); + const original = createNativeMirrorFactory(); + + // When strict registration accepts the factory and repeated stop requests share one completion. + globalThis[QUEUE].push([['pinned-registration'], { 70940: original }]); + const registered = globalThis[QUEUE][0][1][70940]; + const accepted = session.snapshot(); + const stopping = session.stop(); + const repeated = session.stop(); + const stopped = await stopping; + + // Then registration was transformed exactly once and cleanup restores the original unexecuted factory. + assert.notEqual(registered, original); + assert.equal(stopping, repeated); + assert.deepEqual(accepted, { status: 'waiting', reason: null, attempts: 1, matches: 1, executions: 0, phase: 'active', writer: EMPTY_WRITER }); + assert.equal(globalThis[QUEUE][0][1][70940], original); + assert.equal(Object.hasOwn(globalThis[QUEUE], 'push'), false); + assert.deepEqual(stopped, { status: 'native', reason: 'manual', attempts: 1, matches: 1, executions: 0, phase: 'stopped', writer: EMPTY_WRITER }); +}); + +test('user retires the capture hook exactly at thirty seconds without leaving a page listener', async t => { + // Given the production capture deadline runs on Node's controlled timer boundary. + const page = installPage(t); + t.mock.timers.enable({ apis: ['setTimeout'] }); + const session = page.start(); + + // When the final waiting millisecond passes and the deadline cleanup finishes. + t.mock.timers.tick(29_999); + const before = session.snapshot(); + const ownedBefore = Object.hasOwn(globalThis, QUEUE); + t.mock.timers.tick(1); + const stopped = await session.stop(); + t.mock.timers.tick(60_000); + + // Then capture remains available before the deadline and permanently restores native behavior at it. + assert.equal(before.status, 'waiting'); + assert.equal(before.phase, 'active'); + assert.equal(ownedBefore, true); + assert.deepEqual(stopped, { status: 'native', reason: 'capture_deadline', attempts: 0, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER }); + assert.deepEqual(session.snapshot(), stopped); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); + assert.equal(getEventListeners(page.events, 'pagehide').length, 0); +}); + +test('user releases registration and cancels the capture deadline on pagehide', async t => { + // Given a pinned but unexecuted factory waits behind the page lifecycle listener. + const page = installPage(t); + t.mock.timers.enable({ apis: ['setTimeout'] }); + const session = page.start(); + const original = createNativeMirrorFactory(); + globalThis[QUEUE].push([['leaving-page'], { 70940: original }]); + + // When the real page lifecycle event fires before the capture deadline. + page.events.dispatchEvent(new Event('pagehide')); + const stopped = await session.stop(); + t.mock.timers.tick(30_000); + + // Then the original factory and native push are restored and the deadline cannot replace the stop reason. + assert.equal(globalThis[QUEUE][0][1][70940], original); + assert.equal(Object.hasOwn(globalThis[QUEUE], 'push'), false); + assert.equal(getEventListeners(page.events, 'pagehide').length, 0); + assert.deepEqual(stopped, { status: 'native', reason: 'pagehide', attempts: 1, matches: 1, executions: 0, phase: 'stopped', writer: EMPTY_WRITER }); + assert.deepEqual(session.snapshot(), stopped); +}); + +test('user preserves the exact native dependency exception during pinned factory execution', async t => { + // Given the original pinned factory and transformed factory share the same rejecting dependency boundary. + const page = installPage(t); + const session = page.start(); + const original = createNativeMirrorFactory(); + globalThis[QUEUE].push([['pinned-execution'], { 70940: original }]); + const wrapped = globalThis[QUEUE][0][1][70940]; + const failure = new Error('Native dependency unavailable'); + const requested = []; + const require = id => { requested.push(id); throw failure; }; + require.m = { 70940: wrapped }; + require.r = exports => { Object.defineProperty(exports, '__esModule', { value: true }); }; + require.d = (exports, getters) => { + for (const [key, get] of Object.entries(getters)) Object.defineProperty(exports, key, { get }); + }; + const nativeModule = { exports: {} }; + assert.throws(() => original(nativeModule, nativeModule.exports, require), error => error === failure); + + // When the transformed full factory reaches the original first require and its capture rejection settles. + const module = { exports: {} }; + assert.throws(() => wrapped(module, module.exports, require), error => error === failure); + await Promise.resolve(); + const stopped = await session.stop(); + + // Then host error identity and dependency ID survive while registration restores without exposing the error text. + assert.deepEqual(requested, [49253, 49253]); + assert.equal(require.m[70940], original); + assert.equal(globalThis[QUEUE][0][1][70940], original); + assert.deepEqual(stopped, { status: 'native', reason: 'capture_failed', attempts: 1, matches: 1, executions: 1, phase: 'stopped', writer: EMPTY_WRITER }); +}); + +test('user retires a queued capture when navigation leaves the trading scope before registration', async t => { + // Given startup began on a valid route before the host registered its pinned module. + const page = installPage(t); + const session = page.start(); + const original = createNativeMirrorFactory(); + const chunk = [['after-navigation'], { 70940: original }]; + + // When SPA navigation changes the route before the factory reaches the queue. + location.pathname = '/zh-CN/futures/home'; + globalThis[QUEUE].push(chunk); + const stopped = await session.stop(); + + // Then the new page keeps the original registration and no strict match or execution is attempted. + assert.equal(globalThis[QUEUE][0], chunk); + assert.equal(globalThis[QUEUE][0][1][70940], original); + assert.equal(Object.hasOwn(globalThis[QUEUE], 'push'), false); + assert.deepEqual(stopped, { status: 'native', reason: 'scope_changed', attempts: 0, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER }); +}); + +test('user receives detached fixed-count snapshots through only two frozen runtime controls', async t => { + // Given the actual runtime has accepted one pinned registration without reading chart data. + const page = installPage(t); + const session = page.start(); + globalThis[QUEUE].push([['snapshot-schema'], { 70940: createNativeMirrorFactory() }]); + + // When a consumer changes its returned snapshot and nested counters. + const snapshot = session.snapshot(); + snapshot.matches = 99; + snapshot.writer.acceptedBatches = 99; + const fresh = session.snapshot(); + await session.stop(); + + // Then only snapshot and stop are exposed and diagnostics retain the fixed data-free schema and actual counts. + assert.equal(Object.isFrozen(session), true); + assert.deepEqual(Object.keys(session).sort(), ['snapshot', 'stop']); + assert.deepEqual(fresh, { status: 'waiting', reason: null, attempts: 1, matches: 1, executions: 0, phase: 'active', writer: EMPTY_WRITER }); +});