diff --git a/docs/binance-chart-storage-development.md b/docs/binance-chart-storage-development.md index 754dcab..1b3f5af 100644 --- a/docs/binance-chart-storage-development.md +++ b/docs/binance-chart-storage-development.md @@ -17,17 +17,41 @@ behavior. Landing-to-trading SPA navigation does not re-run the storage installe 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. +Two complete pinned factories are intercepted before their first execution: +`70940` changes only the mirror destination expression, while `76535` restricts +the native save extractor's input to each chart's current MainSeries symbols. +One queue observer owns both targets. Capture, source rejection and deadline +outcomes are independent; one changed upstream module does not cancel a matching +other module. Late injection leaves both native. Metadata does not guarantee +early injection on every extension configuration; rejection remains explicit. + +### Drawing ownership + +Native loading puts historical symbols' drawings into every chart that shares +their ownerSource. A different symbol's tab can therefore retain an old invisible +copy of drawings that another page has edited or deleted. The native extractor +previously wrote all those groups back on every save, restoring deleted drawings. + +Before native drawing-ID deduplication, the scoped snapshot retains only drawings +belonging to the current MainSeries symbols of their own chart. All indicator +panes inherit that chart's symbol set. Other sources and drawing properties stay +unchanged. Native extraction still produces an explicit empty array for a current +symbol with no drawings, while unrelated symbols are not written. A whole chart +containing drawings without any MainSeries is rejected before storage writes; +an indicator pane without its own MainSeries is valid. The patch does not delete +historical keys, replace save events or serialize charts through a new API. + +All concurrently open trading tabs need the new version; an old tab can still +write its stale hidden copies. Same-symbol concurrent editors and source-only +cross-database interruptions remain separate conflicts, not solved by ownership +filtering. No trading action is needed to validate the fix. 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. +null. Ordinary saves retain their native write sequence after drawing ownership +filtering; source reads, Basic chart settings and `clear()` remain native. 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 @@ -46,11 +70,14 @@ 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 +startup status and an independent `drawingScope` status. `stop()` drains the current instance's accepted mirror 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. +Stopping mirror optimization does not remove drawing ownership protection. If +that factory has not been captured, its original bounded startup observation +continues; pagehide terminates outstanding observations for both targets. `acceptedBatches` counts created optimized transactions. `committedTransactions` counts real completion events, while `committedWrites` and `skippedWrites` count @@ -79,6 +106,7 @@ 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 +node scripts/generate-chart-drawing-save-module.mjs /path/to/verified/36648.dd8ae7dc.js ``` ## Validation and release diff --git a/docs/binance-futures-blank-page-diagnosis.md b/docs/binance-futures-blank-page-diagnosis.md index e7899fc..794f204 100644 --- a/docs/binance-futures-blank-page-diagnosis.md +++ b/docs/binance-futures-blank-page-diagnosis.md @@ -930,3 +930,49 @@ 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/`. + +### Deleted drawings restored by other symbols: 2026-10-07 (Beijing) + +After v2.7.218, the user reported that removing all 20 MUBARAKUSDT drawings and +waiting one, five or more than ten minutes still restored them after reload. +Other trading tabs were open, each on a different symbol. Read-only inspection +found 20 matching MUBARAK records in both chart databases. The MUBARAK serializer +contained 893 drawings across 99 symbols; the GRIFFAIN serializer retained the +same 20 MUBARAK drawing IDs as hidden sources, with shared ownerSource `ZmamTt`. +The current page's two stored symbol keys and drawing symbols were uppercase, +ruling out the separately reproduced mixed-case-key candidate for this incident. +No user drawing was deleted and no user page was refreshed during diagnosis. +The aggregate evidence is `/tmp/binance-drawing-ownership-VuHzpW/incident.json`. + +Native `zt` loads all historical symbol groups by ownerSource. Native `kt` then +extracts all LineTool sources, including other symbols' invisible copies, and +`xt` writes every group. Consequently, a different-symbol tab can overwrite an +already committed deletion during its next save. Exact host callbacks and real +IndexedDB reproduced 20 -> 0 -> 20 with both native and v2.7.218 writers. No +interrupted save, duplicate-symbol tab or mirror-transaction failure was needed. + +Version 2.7.219 scopes the input of the native `kt` extractor to each chart's +current MainSeries symbols, before global drawing-ID deduplication. Indicator +panes inherit their own chart's symbols. The original extractor still writes an +empty active-symbol array after deletion and preserves other symbols' database +records. The complete pinned 76535 factory differs only at that input expression; +its event handlers, source persistence sequence and load path remain native. +Drawing protection has its own capture status and remains active after mirror +optimization stops. Existing old-version tabs retain their old write behavior +until they load the new version. + +The repaired real-IDB scenario stays at 20 -> 0 -> 0. Eleven deletion scenarios +also cover multi-chart ownership before deduplication, indicator-pane drawings, +current empty arrays, preservation of a newer unrelated-symbol record and invalid +whole-chart ownership rejection before writes. A separate source-only cross-db +interruption still permits native stale overwrite and is explicitly not this +incident's root cause. All 2,167 Node/DOM and 143 chart-storage browser tests, test +lint, build, syntax checks and independent source review passed. + +The complete v2.7.219 artifact passed anonymous Chrome verification with zero +page errors, ready charts, five optimized transactions and four native commits +after stop. Drawing protection remained active throughout. Screenshots were +inspected and the anonymous browser was closed. This smoke did not create/delete +drawings; deletion and reload integrity were exercised by the exact host/IDB +scenarios. Its report is in +`/var/folders/99/d7dxx5j94_x8n09snmknq0x80000gn/T/binance-chart-storage-smoke-R0V9Km/`. diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md index 4fb524c..7021fb1 100644 --- a/docs/binance-orderbook-trade-development.md +++ b/docs/binance-orderbook-trade-development.md @@ -23,8 +23,10 @@ scripts/binance-orderbook-trade.user.js ### 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 +module restricts native drawing saves to each chart's current symbols, batches +the pinned TradingView mirror destination writes and skips unchanged records. +This prevents different-symbol tabs from writing back stale hidden drawings. +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 diff --git a/e2e/binance-orderbook/specs/chart-storage-delete-regression.pw.js b/e2e/binance-orderbook/specs/chart-storage-delete-regression.pw.js new file mode 100644 index 0000000..a5a7301 --- /dev/null +++ b/e2e/binance-orderbook/specs/chart-storage-delete-regression.pw.js @@ -0,0 +1,373 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { test, expect } from '../test.js'; +import { STORAGE_LAB_ORIGIN } from '../helpers/chart-storage-lab.js'; +import { openChartMirrorScoped } from '../helpers/chart-storage-mirror-scoped.js'; + +const deletedSymbol = 'MUBARAKUSDT'; +const otherSymbol = 'ETHUSDT'; +const drawingKey = `#TV_SYMBOL-${deletedSymbol}`; + +function chart(chartId, symbol, count, mainSourceId = `main-${symbol}`) { + return { + chartId, + panes: [{ + mainSourceId, + sources: [ + { id: mainSourceId, type: 'MainSeries', state: { symbol } }, + ...Array.from({ length: count }, (_, index) => ({ + id: `manual-${symbol}-${index}`, type: 'LineToolTrendLine', ownerSource: mainSourceId, + state: { symbol }, points: [{ time: 1000, price: index + 20 }, { time: 2000, price: index + 25 }], + })), + ], + }], + }; +} + +const snapshot = (chartId, symbol, count) => ({ layout: 's', charts: [chart(chartId, symbol, count)] }); + +/** Use the verified native callback fixture with the current production destination writer. */ +async function openDrawingHost(page, mode, namespace = 'chart_futures') { + await openChartMirrorScoped(page, { mode: 'native' }); + if (mode !== 'native') { + 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'), + }); + }); + } + if (mode === 'scoped-production') { + const manifest = JSON.parse(await readFile(new URL('../../../test/fixtures/binance-chart-storage/drawing-save-manifest.json', import.meta.url), 'utf8')); + for (const [asset, path] of [ + ['drawing-save-scope.js', '../../../src/binance-orderbook-trade/chart-storage/drawing-save-scope.js'], + ['drawing-save-scoped.js', '../../../test/fixtures/binance-chart-storage/drawing-save-scoped.js'], + ]) { + const source = await readFile(new URL(path, import.meta.url), 'utf8'); + if (asset === 'drawing-save-scoped.js') { + assert.equal(createHash('sha256').update(source).digest('hex'), manifest.outputs.fixture, 'Drawing save fixture hash changed'); + } + await page.route(`${STORAGE_LAB_ORIGIN}/${asset}`, route => route.fulfill({ + contentType: 'text/javascript', body: source, + })); + } + } + await page.evaluate(async ({ mode, namespace }) => { + const state = window.__MIRROR_SCOPED__; + let api = window.__CHART_HOST__.tradingView; + if (mode !== 'native') { + const { createChartMirrorWriter } = await import('/production-writer.js'); + const { createMirrorCallbacks } = await import('/mirror-scoped-callback.js'); + let runtimeRequire; + self.webpackChunkfutures_trade_ui.push([['drawing-regression'], {}, require => { runtimeRequire = require; }]); + state.writer = createChartMirrorWriter(); + const callbacks = createMirrorCallbacks(runtimeRequire, (target, entries, nativeThunk) => { + state.dispatches += 1; + return state.writer.dispatch(target, entries, nativeThunk); + }); + state.create = callbacks.patched; + if (mode === 'scoped-production') { + const { scopeChartDrawingSnapshot } = await import('/drawing-save-scope.js'); + const { createDrawingSaveCallbacks } = await import('/drawing-save-scoped.js'); + api = createDrawingSaveCallbacks(runtimeRequire, scopeChartDrawingSnapshot); + } + } + const storage = state.localforage.createInstance({ name: namespace }); + await storage.ready(); + window.__DRAWING_HOST__ = { + storage, api, + save: api.createSaveCallback(storage, 'myTradingView', {}, state.create(namespace)), + }; + }, { mode, namespace }); +} + +async function saveDrawingSnapshot(page, value) { + await page.evaluate(value => window.__DRAWING_HOST__.save(value), value); +} + +async function readDrawingState(page) { + return page.evaluate(async ({ drawingKey, deletedSymbol }) => { + const read = async name => { + const storage = window.__STORAGE_LAB__.localforage.createInstance({ name }); + const loaded = await window.__DRAWING_HOST__.api.load({ storage, key: 'myTradingView' }); + return { + stored: await storage.getItem(drawingKey), + loaded: loaded.chartSave.charts.flatMap(chart => chart.panes.flatMap(pane => pane.sources)) + .filter(source => source.type.startsWith('LineTool') && source.state.symbol === deletedSymbol), + }; + }; + const state = window.__MIRROR_SCOPED__; + return { + futures: await read('chart_futures'), delivery: await read('chart_delivery'), + writer: state.writer ? state.writer.getStats() : null, + }; + }, { drawingKey, deletedSymbol }); +} + +async function attachResult(testInfo, mode, scenario, result) { + const evidence = { mode, scenario, ...result }; + await testInfo.attach(`drawing-deletion-${mode}-${scenario}.json`, { + body: JSON.stringify(evidence, null, 2), contentType: 'application/json', + }); + console.info(JSON.stringify({ + mode, scenario, + states: Object.fromEntries(Object.entries(result) + .filter(([, value]) => value && typeof value === 'object' && Object.hasOwn(value, 'futures')) + .map(([stage, value]) => [stage, { + futuresStored: value.futures.stored.length, futuresLoaded: value.futures.loaded.length, + deliveryStored: value.delivery.stored.length, deliveryLoaded: value.delivery.loaded.length, + writer: value.writer, + }])), + })); +} + +for (const mode of ['native', 'production', 'scoped-production']) { + test(`user keeps twenty deleted drawings absent after another symbol saves and a new page opens with ${mode}`, async ({ page, context }, testInfo) => { + // Given the complete host save and mirror persist twenty manual drawings in both databases. + await openDrawingHost(page, mode); + const other = await context.newPage(); + await openDrawingHost(other, mode); + await saveDrawingSnapshot(page, snapshot('0', deletedSymbol, 20)); + const initial = await readDrawingState(page); + expect(initial.futures.stored).toHaveLength(20); + expect(initial.delivery.loaded).toHaveLength(20); + + // When deletion fully commits and another same-namespace symbol saves before the deleting page closes. + await saveDrawingSnapshot(page, snapshot('0', deletedSymbol, 0)); + const deleted = await readDrawingState(page); + await saveDrawingSnapshot(other, snapshot('1', otherSymbol, 1)); + const afterOtherSave = await readDrawingState(other); + await page.close(); + const reopened = await context.newPage(); + await openDrawingHost(reopened, mode); + const reloaded = await readDrawingState(reopened); + await attachResult(testInfo, mode, 'completed-save', { + initialCount: initial.futures.stored.length, deleted, afterOtherSave, reloaded, + }); + + // Then both actual stored arrays and host load results stay empty after the original page is destroyed. + for (const state of [deleted, afterOtherSave, reloaded]) { + expect(state.futures).toEqual({ stored: [], loaded: [] }); + expect(state.delivery).toEqual({ stored: [], loaded: [] }); + } + if (mode !== 'native') { + expect(deleted.writer).toMatchObject({ acceptedBatches: 2, committedTransactions: 2, pendingBatches: 0, failedBatches: 0 }); + expect(afterOtherSave.writer).toMatchObject({ acceptedBatches: 1, committedTransactions: 1, pendingBatches: 0, failedBatches: 0 }); + } else { + expect(deleted.writer).toBe(null); + } + await other.close(); + await reopened.close(); + }); + + test(`user receives stale opposite-namespace drawings after a source-only deletion with ${mode}`, async ({ page, context }, testInfo) => { + // Given both namespaces retain the complete initial twenty-drawing save. + await openDrawingHost(page, mode); + const other = await context.newPage(); + await openDrawingHost(other, mode, 'chart_delivery'); + await saveDrawingSnapshot(page, snapshot('0', deletedSymbol, 20)); + const drawings = chart('0', deletedSymbol, 20).panes[0].sources.slice(1); + const initial = await readDrawingState(page); + expect(initial.futures).toEqual({ stored: drawings, loaded: drawings }); + expect(initial.delivery).toEqual({ stored: drawings, loaded: drawings }); + + // When only the public source-save stage commits deletion before close and the other namespace later mirrors. + await page.evaluate(save => window.__DRAWING_HOST__.api.save({ + storage: window.__DRAWING_HOST__.storage, save, key: 'myTradingView', widget: {}, + }), snapshot('0', deletedSymbol, 0)); + const deleted = await readDrawingState(page); + await page.close(); + await saveDrawingSnapshot(other, snapshot('1', otherSymbol, 1)); + const afterOtherSave = await readDrawingState(other); + const reopened = await context.newPage(); + await openDrawingHost(reopened, mode); + const reloaded = await readDrawingState(reopened); + await attachResult(testInfo, mode, 'source-only-save', { + scope: 'Separate from the retained-snapshot incident: the public source-save stage is invoked directly; no interrupted callback or live close timing is inferred.', + initial, deleted, afterOtherSave, reloaded, + }); + + // Then an actual old mirror restores all twenty drawings while both modes expose the same overwrite behavior. + expect(deleted.futures).toEqual({ stored: [], loaded: [] }); + expect(deleted.delivery).toEqual({ stored: drawings, loaded: drawings }); + expect(afterOtherSave.futures).toEqual({ stored: drawings, loaded: drawings }); + expect(reloaded.futures).toEqual({ stored: drawings, loaded: drawings }); + expect(reloaded.delivery).toEqual({ stored: drawings, loaded: drawings }); + if (mode !== 'native') { + expect(deleted.writer).toMatchObject({ acceptedBatches: 1, committedTransactions: 1, pendingBatches: 0, failedBatches: 0 }); + expect(afterOtherSave.writer).toMatchObject({ acceptedBatches: 1, committedTransactions: 1, pendingBatches: 0, failedBatches: 0 }); + } else { + expect(afterOtherSave.writer).toBe(null); + } + await reopened.close(); + await other.close(); + }); + + const retainedOutcome = mode === 'scoped-production' ? 'keeps deleted drawings absent' : 'receives deleted drawings'; + test(`user ${retainedOutcome} after another symbol saves its retained same-namespace snapshot with ${mode}`, async ({ page, context }, testInfo) => { + // Given the native loader retains historical drawings sharing the current chart's ownerSource across symbols. + await openDrawingHost(page, mode); + const other = await context.newPage(); + await openDrawingHost(other, mode); + const mainSourceId = 'ZmamTt'; + const initialChart = chart('0', deletedSymbol, 20, mainSourceId); + const drawings = initialChart.panes[0].sources.slice(1); + await saveDrawingSnapshot(page, { layout: 's', charts: [initialChart] }); + await saveDrawingSnapshot(other, { layout: 's', charts: [chart('0', otherSymbol, 0, mainSourceId)] }); + const retained = await other.evaluate(async () => { + const state = window.__DRAWING_HOST__; + const loaded = await state.api.load({ storage: state.storage, key: 'myTradingView' }); + state.retainedSnapshot = JSON.parse(JSON.stringify(loaded.chartSave)); + return state.retainedSnapshot; + }); + expect(retained).toEqual({ + layout: 's', + charts: [{ + chartId: '0', panes: [{ + mainSourceId, + sources: [{ id: mainSourceId, type: 'MainSeries', state: { symbol: otherSymbol } }, ...drawings], + }], + }], + }); + + // When deletion commits in both databases before the other symbol saves its previously loaded hidden drawings. + await saveDrawingSnapshot(page, { layout: 's', charts: [chart('0', deletedSymbol, 0, mainSourceId)] }); + const deleted = await readDrawingState(page); + await page.close(); + await other.evaluate(() => window.__DRAWING_HOST__.save(window.__DRAWING_HOST__.retainedSnapshot)); + const afterOtherSave = await readDrawingState(other); + const reopened = await context.newPage(); + await openDrawingHost(reopened, mode); + const reloaded = await readDrawingState(reopened); + await attachResult(testInfo, mode, 'retained-same-namespace-snapshot', { + scope: 'Both tabs use chart_futures. A native load supplies the hidden historical drawings; every save awaits its complete native source and mirror workflow.', + retained, deleted, afterOtherSave, reloaded, + }); + + // Then the scoped save preserves deletion while both unchanged baselines restore the exact old drawings. + expect(deleted.futures).toEqual({ stored: [], loaded: [] }); + expect(deleted.delivery).toEqual({ stored: [], loaded: [] }); + const expectedDrawings = mode === 'scoped-production' ? [] : drawings; + expect(afterOtherSave.futures).toEqual({ stored: expectedDrawings, loaded: expectedDrawings }); + expect(afterOtherSave.delivery).toEqual({ stored: expectedDrawings, loaded: expectedDrawings }); + expect(reloaded.futures).toEqual({ stored: expectedDrawings, loaded: expectedDrawings }); + expect(reloaded.delivery).toEqual({ stored: expectedDrawings, loaded: expectedDrawings }); + if (mode !== 'native') { + expect(deleted.writer).toMatchObject({ acceptedBatches: 2, committedTransactions: 2, pendingBatches: 0, failedBatches: 0 }); + expect(afterOtherSave.writer).toMatchObject({ acceptedBatches: 2, committedTransactions: 2, pendingBatches: 0, failedBatches: 0 }); + } else { + expect(afterOtherSave.writer).toBe(null); + } + await reopened.close(); + await other.close(); + }); +} + +test('user saves each chart current drawings before deduplication and preserves newer unrelated drawings', async ({ page, context }, testInfo) => { + // Given native loads populate both charts with historical drawings sharing one owner and repeated drawing IDs. + const mode = 'scoped-production'; + const historicalSymbol = 'BTCUSDT'; + const mainSourceId = 'ZmamTt'; + await openDrawingHost(page, mode); + const other = await context.newPage(); + await openDrawingHost(other, mode); + await saveDrawingSnapshot(page, { layout: '2h', charts: [chart('0', historicalSymbol, 1, mainSourceId)] }); + await saveDrawingSnapshot(page, { + layout: '2h', charts: [chart('0', deletedSymbol, 1, mainSourceId), chart('1', otherSymbol, 1, mainSourceId)], + }); + const retained = await page.evaluate(async () => { + const { api, storage } = window.__DRAWING_HOST__; + const loaded = await api.load({ storage, key: 'myTradingView' }); + return JSON.parse(JSON.stringify(loaded.chartSave)); + }); + const mainDrawing = (value, symbol) => value.panes[0].sources + .find(source => source.type === 'LineToolTrendLine' && source.state.symbol === symbol); + for (const value of retained.charts) { + expect(value.panes[0].sources.filter(source => source.type === 'LineToolTrendLine') + .map(source => source.state.symbol).sort()).toEqual([historicalSymbol, otherSymbol, deletedSymbol].sort()); + } + mainDrawing(retained.charts[0], deletedSymbol).points[1].price = 101; + mainDrawing(retained.charts[1], otherSymbol).points[1].price = 202; + expect(mainDrawing(retained.charts[0], otherSymbol).points[1].price).toBe(25); + const study = { id: 'rsi-study', type: 'Study', state: { studyId: 'RSI@tv-basicstudies', length: 14 } }; + const indicatorDrawing = { + ...chart('0', deletedSymbol, 1, 'rsi-owner').panes[0].sources[1], id: 'manual-MUBARAKUSDT-rsi', + }; + retained.charts[0].panes.push({ mainSourceId: 'rsi-owner', sources: [study, indicatorDrawing] }); + const latestHistorical = chart('0', historicalSymbol, 1, mainSourceId); + latestHistorical.panes[0].sources[1].points[1].price = 303; + + // When another tab updates the historical symbol before both current charts save their retained serialized state. + await saveDrawingSnapshot(other, { layout: 's', charts: [latestHistorical] }); + await saveDrawingSnapshot(page, retained); + const readPersisted = async () => page.evaluate(async symbols => { + const read = async name => { + const storage = window.__STORAGE_LAB__.localforage.createInstance({ name }); + return { + drawings: Object.fromEntries(await Promise.all(symbols.map(async symbol => [symbol, await storage.getItem(`#TV_SYMBOL-${symbol}`)]))), + snapshot: await storage.getItem('myTradingView'), + loaded: await window.__DRAWING_HOST__.api.load({ storage, key: 'myTradingView' }), + }; + }; + return { futures: await read('chart_futures'), delivery: await read('chart_delivery') }; + }, [deletedSymbol, otherSymbol, historicalSymbol]); + const saved = await readPersisted(); + + // Then each current chart keeps its edited value and indicator drawing while the historical database value stays newer. + const expectedDrawings = { + [deletedSymbol]: [mainDrawing(retained.charts[0], deletedSymbol), indicatorDrawing], + [otherSymbol]: [mainDrawing(retained.charts[1], otherSymbol)], + [historicalSymbol]: [latestHistorical.panes[0].sources[1]], + }; + for (const database of [saved.futures, saved.delivery]) { + expect(database.drawings).toEqual(expectedDrawings); + expect(database.snapshot.charts[0].panes[1]).toEqual({ mainSourceId: 'rsi-owner', sources: [study] }); + expect(database.loaded.chartSave.charts[0].panes[1]).toEqual({ mainSourceId: 'rsi-owner', sources: [study, indicatorDrawing] }); + } + + // When the first chart removes all its current drawings while the other chart still carries their hidden old copies. + const cleared = JSON.parse(JSON.stringify(retained)); + for (const pane of cleared.charts[0].panes) { + pane.sources = pane.sources.filter(source => !source.type.startsWith('LineTool') || source.state.symbol !== deletedSymbol); + } + await saveDrawingSnapshot(page, cleared); + const deleted = await readPersisted(); + await testInfo.attach('drawing-multichart-scoped-production.json', { + body: JSON.stringify({ retained, saved, deleted }, null, 2), contentType: 'application/json', + }); + + // Then an explicit empty array survives hidden duplicate IDs without changing the other current or historical symbols. + for (const database of [deleted.futures, deleted.delivery]) { + expect(database.drawings).toEqual({ ...expectedDrawings, [deletedSymbol]: [] }); + expect(database.loaded.chartSave.charts[0].panes[1]).toEqual({ mainSourceId: 'rsi-owner', sources: [study] }); + } + await other.close(); +}); + +test('user keeps persisted drawings when an entire saved chart lacks its MainSeries symbol', async ({ page }) => { + // Given a valid saved chart has drawing records and a malformed replacement retains drawings without any MainSeries. + await openDrawingHost(page, 'scoped-production'); + await saveDrawingSnapshot(page, snapshot('0', deletedSymbol, 2)); + const before = await readDrawingState(page); + const malformed = await page.evaluate(async () => { + const { api, storage } = window.__DRAWING_HOST__; + const loaded = await api.load({ storage, key: 'myTradingView' }); + const value = JSON.parse(JSON.stringify(loaded.chartSave)); + for (const pane of value.charts[0].panes) { + pane.sources = pane.sources.filter(source => source.type !== 'MainSeries'); + } + return value; + }); + + // When the public save callback receives the unsupported whole-chart shape. + await expect(saveDrawingSnapshot(page, malformed)).rejects.toThrow('Drawing save requires a MainSeries symbol for its chart'); + const after = await readDrawingState(page); + + // Then the error leaves both databases and the committed mirror counters exactly as they were before the save. + expect(after).toEqual(before); + expect(after.futures.stored).toEqual(chart('0', deletedSymbol, 2).panes[0].sources.slice(1)); + expect(after.writer).toMatchObject({ acceptedBatches: 1, committedTransactions: 1, pendingBatches: 0, failedBatches: 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 index 8d265a7..f8b0f5d 100644 --- a/e2e/binance-orderbook/specs/chart-storage-orderbook-entry.pw.js +++ b/e2e/binance-orderbook/specs/chart-storage-orderbook-entry.pw.js @@ -135,7 +135,9 @@ for (const kind of ['source-mismatch', 'late-bootstrap']) { 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' }); + expect(storage).toMatchObject({ sameChunk: true, sameFactory: true, + nativePush: kind === 'late-bootstrap', nativeValue: 'native-chart-module' }); + expect(storage.snapshot.drawingScope.status).toBe(kind === 'late-bootstrap' ? 'unavailable' : 'waiting'); 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'); diff --git a/scripts/binance-chart-storage-smoke.mjs b/scripts/binance-chart-storage-smoke.mjs index aa5c272..2f49cf9 100644 --- a/scripts/binance-chart-storage-smoke.mjs +++ b/scripts/binance-chart-storage-smoke.mjs @@ -76,6 +76,8 @@ function assertOptimized(snapshot) { assert.equal(snapshot.phase, 'active'); assert.equal(snapshot.matches, 1); assert.equal(snapshot.executions, 1); + assert.equal(snapshot.drawingScope.status, 'active'); + assert.equal(snapshot.drawingScope.executions, 1); for (const field of ['rejectedBatches', 'failedBatches', 'abortedTransactions', 'pendingBatches', 'pendingBytes', 'nativePassthroughBatches']) { assert.equal(snapshot.writer[field], 0, field); } @@ -184,6 +186,8 @@ try { const initial = await snapshot(); assert.equal(initial.optimizer.matches, 1); assert.equal(initial.optimizer.executions, 1); + assert.equal(initial.optimizer.drawingScope.status, 'active'); + assert.equal(initial.optimizer.drawingScope.executions, 1); assert.equal(initial.orderbook.panelCount, 1); assert.equal(initial.orderbook.styleCount, 1); assert.equal(initial.orderbook.initialized, true); @@ -195,6 +199,7 @@ try { stage = 'stop'; report.stopped = await bounded(page.evaluate(() => self.__BINANCE_CHART_STORAGE__.stop())); assert.equal(report.stopped.phase, 'stopped'); + assert.equal(report.stopped.drawingScope.status, 'active'); stage = 'native_after_stop'; await changeInterval('4小时', '240', false); assert.equal(report.navigationRefused, undefined); diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index 962cd7c..f8eb350 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.218 +// @version 2.7.219 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -5212,11 +5212,16 @@ // src/binance-orderbook-trade/chart-storage/bootstrap.js var QUEUE_NAME = "webpackChunkfutures_trade_ui"; - function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } = {}) { + function observeChartStorageBootstrap({ targets, onCapture, onFailure, onComplete }) { 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 registrations = new Map(Object.entries(targets).map(([id, replaceFactory]) => { + if (typeof replaceFactory !== "function") throw new Error("Storage replacement must be a synchronous function"); + return [id, { id, replaceFactory, terminal: false }]; + })); + if (registrations.size === 0) throw new Error("Storage bootstrap requires explicit module targets"); + for (const callback of [onCapture, onFailure, onComplete]) { + if (typeof callback !== "function") throw new Error("Storage observation callbacks must be synchronous functions"); + } 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 : []; @@ -5230,21 +5235,18 @@ 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 runtimeRequire; 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; + function restoreFactory(target) { + if (target.observedFactory && runtimeRequire && runtimeRequire.m[target.id] === target.observedFactory) { + runtimeRequire.m[target.id] = target.originalFactory; + } + if (target.observedChunk && target.observedChunk[1][target.id] === target.observedFactory) { + target.observedChunk[1][target.id] = target.originalFactory; + } + } + function restoreQueue() { 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"); @@ -5259,64 +5261,77 @@ } } } - function finish(succeeded, result) { - if (!active) return; + function completeIfFinished() { + if (!active || [...registrations.values()].some((target) => !target.terminal)) return; active = false; + let reason = null; try { - restore(); - } catch (cleanupError) { - if (succeeded) { - succeeded = false; - result = cleanupError; - } + restoreQueue(); + } catch { + reason = "ownership_lost"; } - if (succeeded) resolveCapture(result); - else rejectCapture(result); + onComplete(reason); + } + function finish(target, reason) { + if (target.terminal) return; + target.terminal = true; + restoreFactory(target); + if (reason === "captured") onCapture(target.id); + else onFailure(target.id, reason); + completeIfFinished(); + } + function stop(reason = "stopped") { + for (const target of registrations.values()) finish(target, reason); } 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) { + if (!Array.isArray(chunk) || !Array.isArray(chunk[0]) || !chunk[1] || typeof chunk[1] !== "object" || chunk[2] !== void 0 && typeof chunk[2] !== "function") { + stop("unsupported_registration"); + return chunk; + } + let observedChunk; + for (const target of registrations.values()) { + if (target.terminal || !Object.hasOwn(chunk[1], target.id)) continue; + if (target.originalFactory || typeof chunk[1][target.id] !== "function") { + finish(target, "unsupported_registration"); + continue; + } + target.originalFactory = chunk[1][target.id]; + let executionFactory; + try { + executionFactory = target.replaceFactory(target.originalFactory); + } catch { + finish(target, "source_mismatch"); + continue; + } + if (target.terminal) continue; + if (typeof executionFactory !== "function") { + finish(target, "unsupported_registration"); + continue; + } + target.observedFactory = function(module, exports, require2) { runtimeRequire = require2; let returned; try { returned = executionFactory.call(this, module, exports, require2); } catch (hostError) { - finish(false, hostError); + finish(target, "execution_failed"); 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); - } - } + finish(target, "captured"); 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; + if (!observedChunk) { + const hostRuntime = chunk[2]; + observedChunk = [chunk[0], { ...chunk[1] }, (require2) => { + runtimeRequire = require2; + if (hostRuntime !== void 0) return hostRuntime(require2); + }]; + } + observedChunk[1][target.id] = target.observedFactory; + target.observedChunk = observedChunk; } + return observedChunk || chunk; } function preRuntimePush(...chunks) { if (!active || runtimePush) return nativeAppend.apply(this, chunks); @@ -5339,7 +5354,7 @@ return; } if (runtimePush || typeof dispatcher !== "function") { - finish(false, new Error("Storage bootstrap observed an unexpected push replacement")); + stop("unexpected_dispatcher"); Object.defineProperty(queue, "push", { value: dispatcher, writable: true, configurable: true, enumerable: true }); return; } @@ -5347,9 +5362,11 @@ } 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")); + stop, + stopTarget(id, reason = "stopped") { + const target = registrations.get(id); + if (!target) throw new Error("Storage bootstrap target is not registered"); + finish(target, reason); } }); } @@ -6098,6 +6115,377 @@ return createMirrorFactory(dispatch); } + // src/binance-orderbook-trade/chart-storage/drawing-save-scope.js + function scopeChartDrawingSnapshot(save) { + return { + ...save, + charts: save.charts.map((chart) => { + const sources = chart.panes.flatMap((pane) => pane.sources); + const symbols = new Set(sources.filter((source) => source.type === "MainSeries").map((source) => source.state.symbol.toUpperCase())); + if (symbols.size === 0 && sources.some((source) => source.type.startsWith("LineTool"))) { + throw new Error("Drawing save requires a MainSeries symbol for its chart"); + } + return { + ...chart, + panes: chart.panes.map((pane) => ({ + ...pane, + sources: pane.sources.filter((source) => !source.type.startsWith("LineTool") || symbols.has(source.state.symbol.toUpperCase())) + })) + }; + }) + }; + } + + // src/binance-orderbook-trade/chart-storage/drawing-save-module.js + var originalFactorySource2 = '76535(w,B,r){r.d(B,{A:()=>_e,X:()=>Te});var A=r(31085),e=r(41594),f=r(43917),g=r.n(f),G=r(19020),T=r(13067);const b=null,v="myTradingView";var K=r(78441),U=r(53837),o=r(4260),S=r(80817);const P=t=>{switch(t){case"zh-CN":case"cn":return"zh";case"zh-HK":case"zh-TW":case"zh-TC":case"tw":return"zh_TW";case"ja":return"ja";case"ko":return"ko";case"th":return"th";case"he":return"he_IL";case"ru":return"ru";case"cs":return"cs";case"sv":return"sv";case"vi":case"vn":return"vi";case"tr":return"tr";case"ro":return"ro";case"pt-PT":case"pt-BR":case"pt":return"pt";case"pl":return"pl";case"nl":return"nl_NL";case"it":return"it";case"fr":return"fr";case"es":case"es-LA":return"es";case"de":return"de";case"id":return"id_ID";default:return"en"}};var x=r(46108),ot=r.n(x),Z=r(75857),rt=r.n(Z),at=r(87809),j=r.n(at),X=r(34789),Q=r.n(X),ct=r(19953),it=r.n(ct),lt=r(82438),ut=r.n(lt),a=r(87835),E=r.n(a),O=r(75257);const W=t=>/^(LineTool)/g.test(t),_t=t=>`${t}.customSettings`,ft=t=>`${t}.layout`,J=t=>t.chartId,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}},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},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}},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},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}},$t=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell;return{...n,direction:n.isBuy?"buy":"sell",arrowStyles:{height:8,spacing:1,color:i},textStyles:{color:s.PrimaryText}}},[t])},jt=[],Zt=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.yC),[l]=s(o._b),u=$t(),d=(0,e.useMemo)(()=>n.map(u),[u,n]);return(0,e.useMemo)(()=>i&&l===o.Ev.Single?d:jt,[i,l,d])};var ht=r(92873);const Xt=({storage:t,storageName:n})=>{const{getI18n:s}=(0,ht.o)("","trade-ui"),i=(0,e.useMemo)(()=>s("trd-chart-tv-initialization-error",{defaultValue:"Something went wrong while initializing TradingView."})||"",[s]),l=(0,e.useMemo)(()=>s("trd-chart-crash-reload",{defaultValue:"Refresh"})||"",[s]),u=(0,e.useMemo)(()=>s("trd-chart-init-error",{defaultValue:"Failed to initialize TradingView due to malformed config, please try to refresh"})||"",[s]),d=(0,e.useCallback)(()=>window.location.reload(),[]),I=(0,e.useCallback)(async()=>{await t.removeItem(n),d()},[d,t,n]),[N,_]=(0,e.useState)(!1),[m,y]=(0,e.useState)(""),[R,C]=(0,e.useState)(""),[Y,h]=(0,e.useState)(async()=>{}),H=(0,e.useCallback)(()=>{_(!0),y(u),C(l),h(I)},[I,l,u]),V=(0,e.useCallback)(()=>{_(!0),y(i),C(l),h(d)},[i,l,d]);return{showError:N,mainText:m,buttonText:R,buttonCallback:Y,onTradingViewInitError:H,onChartReadyError:V}},Qt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell;return{...n,lineStyles:{color:i,length:45,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.PrimaryText,borderColor:i,backgroundColor:i},cancelButtonStyles:{color:s.IconNormal}}},[t])},Jt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.Buy,length:45,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.TextBuy,borderColor:s.Buy,backgroundColor:s.DepthBuyBg},cancelButtonStyles:{color:s.IconNormal}}},[t])},qt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.EmphasizeText,length:100,unit:"percentage",style:0,extended:!1},bodyStyles:{color:s.RedGreenBgText,borderColor:s.EmphasizeText,backgroundColor:s.EmphasizeText},cancelButtonStyles:{color:s.IconNormal}}},[t])},te=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.DisableText,length:100,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.PrimaryText,borderColor:s.InputLine,backgroundColor:s.InputLine},cancelButtonStyles:{color:s.IconNormal,borderColor:s.InputLine,backgroundColor:s.BasicBg}}},[t])},ee=[],se=({namespace:t,labelLines:n})=>{const s=(0,o.y$)(t),[i]=s(o.O),[l]=s(o.S5),[u]=s(o.ks),[d]=s(o.yA),[I]=s(o.pT),[N]=s(o._b),_=Qt(),m=Jt(),y=qt(),R=te(),C=(0,e.useMemo)(()=>i?n.filter(M=>M.type===o.ND.AverageBuyPrice).map(M=>_({...M,isBuy:!0})):[],[n,i,_]),Y=(0,e.useMemo)(()=>l?n.filter(M=>M.type===o.ND.AverageSellPrice).map(M=>_({...M,isBuy:!1})):[],[n,l,_]),h=(0,e.useMemo)(()=>u?n.filter(M=>M.type===o.ND.BreakEvenPrice).map(m):[],[n,u,m]),H=(0,e.useMemo)(()=>d?n.filter(M=>M.type===o.ND.LiquidationPrice).map(y):[],[n,d,y]),V=(0,e.useMemo)(()=>I?n.filter(M=>M.type===o.ND.PriceAlert).map(R):[],[n,I,R]),D=(0,e.useMemo)(()=>[...C,...Y,...h,...H,...V],[C,Y,h,H,V]);return(0,e.useMemo)(()=>N===o.Ev.Single?D:ee,[D,N])},ne=()=>{const{getI18n:t}=(0,ht.o)("","kline-ui");return(0,e.useMemo)(()=>({openOrder:{dragTooltip:t("order-drag-tooltip",{defaultValue:"Drag for price modify"}),modifyTooltip:t("order-modify-tooltip",{defaultValue:"Click for modify"}),cancelTooltip:t("order-cancel-tooltip",{defaultValue:"Click for cancel"})}}),[t])},oe=({position:t,extended:n})=>{switch(t){case"center":return{length:35,unit:"percentage",extended:n};case"right":return{length:24,unit:"pixel",extended:n};case"left":return{length:-24,unit:"pixel",extended:n};case"left-most":return{length:100,unit:"percentage",extended:n};default:return{length:65,unit:"percentage",extended:n}}},re=()=>{const t=ne(),n=(0,T.w)();return(0,e.useCallback)((s,i)=>{const{colors:l}=n,u=s.isBuy?l.Buy:l.Sell;return{...s,tooltip:s.tooltip??t.openOrder.dragTooltip,modifyTooltip:s.modifyTooltip??t.openOrder.modifyTooltip,cancelTooltip:s.cancelTooltip??t.openOrder.cancelTooltip,lineStyles:{...oe(i),color:u,style:2,extended:!1},bodyStyles:{borderColor:l.CardBg,backgroundColor:u,color:l.RedGreenBgText},quantityStyles:{borderColor:u,backgroundColor:l.CardBg,color:u},cancelButtonStyles:{borderColor:u,backgroundColor:l.CardBg,color:u}}},[t.openOrder.dragTooltip,t.openOrder.modifyTooltip,t.openOrder.cancelTooltip,n])},ae=[],ce=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.Nh),[l]=s(o.R3),[u]=s(o.Ec),[d]=s(o._b),[I]=s(o.CV),[N]=s(o.bv),_=re(),m=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>(C===void 0||C===o.aA.LimitOrder)&&i).map(C=>_(C,{position:I,extended:N})),[n,i,_,I,N]),y=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>C===o.aA.BotPreview&&l).map(C=>_(C,{position:"legacy",extended:!1})),[n,l,_]),R=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>C===o.aA.OrderPreview&&u).map(C=>_(C,{position:"left-most",extended:!1})),[n,u,_]);return(0,e.useMemo)(()=>d===o.Ev.Single?[...m,...y,...R]:ae,[m,y,R,d])},ie=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell,l=n.PNL>0?s.Buy:n.PNL<0?s.Sell:i;return{...n,lineStyles:{color:l,length:100,unit:"percentage",style:2,extended:!1},bodyStyles:{borderColor:l,backgroundColor:l,color:s.RedGreenBgText},quantityStyles:{borderColor:i,backgroundColor:s.CardBg,color:i},reverseButtonStyles:{borderColor:i,backgroundColor:s.CardBg,color:i},closeButtonStyles:{borderColor:i,backgroundColor:s.CardBg,color:i}}},[t])},le=[],ue=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.FH),[l]=s(o._b),u=ie(),d=(0,e.useMemo)(()=>n.map(u),[u,n]);return(0,e.useMemo)(()=>i&&l===o.Ev.Single?[...d]:le,[i,l,d])};var Ee=r(6868);const de=({namespace:t})=>{const n=(0,o.y$)(t),[s,i]=n(o.mn),l=(0,Ee.r)(),u=l(_=>_.hasHydrated),d=l(_=>_.lastUpdatedTimestamp),I=(0,e.useMemo)(()=>u&&d!==s,[u,d,s]),N=(0,e.useCallback)(()=>{i(d)},[d,i]);return(0,e.useMemo)(()=>({overwritingThemeOnLoaded:I,syncThemeTimestamp:N}),[I,N])},Te=()=>r.e("66202").then(r.bind(r,78273)),Ce=(0,e.lazy)(()=>r.e("66202").then(r.bind(r,78273))),Ie=t=>(0,e.useMemo)(()=>g().createInstance({name:t}),[t]),St=t=>{switch(t){case o.Ev.HorizontalTwo:return"2v";case o.Ev.VerticalTwo:return"2h";case o.Ev.HorizontalThree:return"3v";case o.Ev.VerticalThree:return"3h";case o.Ev.LeftOneRightTwo:return"3s";case o.Ev.TopOneBottomTwo:return"1-2";case o.Ev.Four:return"4";case o.Ev.VerticalFour:return"4h";case o.Ev.LeftOneRightThree:return"4s";case o.Ev.TopOneBottomThree:return"1-3";case o.Ev.Five:return"2-3";case o.Ev.SixTwoByThree:return"6";case o.Ev.SixThreeByTwo:return"6c";case o.Ev.Eight:return"8";default:return"s"}},_e=({tradingViewProps:t,namespace:n,storageName:s=v,containerId:i="tradingview",disablePointerEvents:l=!1})=>{const u=(0,o.y$)(n),d=(0,U.y$)(n),I=(0,K.y)(n),[N,_]=u(o.A8),m=u(o.WK),[y]=u(o.e6),[R,C]=u(o.iw),[Y]=u(o._b),[,h]=I(K.N),[,H]=d(U.yj),[V]=d(U.Cf),[D,M]=(0,e.useState)(!1),z=Ie(n),q=(0,G.Bl)(),Nt=(0,e.useMemo)(()=>P(q),[q]),fe=(0,T.w)(),tt=(0,e.useMemo)(()=>`${n}-${i}`,[i,n]),p=(0,e.useRef)(),Et=(0,e.useRef)(N),Rt=(0,e.useRef)(R),dt=(0,e.useRef)(Y),et=(0,e.useRef)(z),st=(0,e.useRef)(s),Lt=(0,e.useRef)(V),he=(0,e.useRef)(D);(0,e.useEffect)(()=>{Et.current=N},[N]),(0,e.useEffect)(()=>{Rt.current=R},[R]),(0,e.useEffect)(()=>{dt.current=Y},[Y]),(0,e.useEffect)(()=>{et.current=z},[z]),(0,e.useEffect)(()=>{st.current=s},[s]),(0,e.useEffect)(()=>{he.current=D},[D]),(0,e.useEffect)(()=>{Lt.current=V},[V]);const Se=ce({namespace:n,orders:t.limitOrders}),Ne=ue({namespace:n,orders:t.positionOrders}),Re=Zt({namespace:n,orders:t.executionOrders}),Le=se({namespace:n,labelLines:t.labelLines}),{overwritingThemeOnLoaded:Oe,syncThemeTimestamp:nt}=de({namespace:n}),Ot=(0,e.useCallback)(({resolution:c,chartType:L})=>{const F=(0,S.Cz)(c);_(L===3?"time":F)},[_]),At=(0,e.useCallback)(async()=>{p.current&&await Wt({storage:et.current,key:st.current,widget:p.current})},[]),mt=(0,e.useCallback)(async c=>zt({storage:et.current,key:st.current}),[]),Tt=(0,e.useCallback)(async c=>{const{chartSave:L}=await xt({storage:et.current,save:c,key:st.current,widget:p.current});await t.initialConfig.onSave?.(L)},[t.initialConfig]),yt=(0,e.useCallback)(async()=>{const c=await mt(dt.current);if(t.initialConfig.onLoadConfig?.(),!c.chartSave)return c;const L=St(dt.current);return c.chartSave.layout===L?c:{...c,chartSave:{...c.chartSave,layout:L}}},[mt,t.initialConfig]),gt=(0,e.useCallback)(c=>{h(c),t.initialConfig.onLoading?.(c)},[h,t.initialConfig]),$=(0,e.useCallback)((c,L=!0)=>{L?c.setChartType(Et.current==="time"?2:Rt.current):Et.current==="time"&&c.setChartType(2)},[]),Ae=(0,e.useCallback)(c=>{p.current=c,H(c)},[H]),Dt=(0,e.useCallback)(()=>{setTimeout(()=>{if(p.current)try{const c=p.current?.activeChart();$(c)}catch{}},0),M(!0),t.initialConfig.onChartReadyDone?.()},[$,t.initialConfig]),Mt=(0,e.useCallback)(()=>{nt(),t.initialConfig.onSetThemeDone?.()},[nt,t.initialConfig]),vt=(0,e.useCallback)(()=>{nt(),t.initialConfig.onFirstTimeSetThemeDone?.()},[nt,t.initialConfig]),pt=(0,e.useCallback)(c=>{t.initialConfig.onIntervalChanged?.(c)},[t.initialConfig]),Ct=(0,e.useCallback)(async c=>{const{chart:L}=c;if(!L.symbolExt())return;const k=L.chartType();C(k),Ot({resolution:L.resolution(),chartType:k}),await At(),t.initialConfig.onActiveChartChanged?.(c)},[At,Ot,C,t.initialConfig]),Bt=(0,e.useCallback)(c=>{window.open(`https://www.tradingview.com/x/${c}`,"_blank")},[]),Ut=(0,e.useCallback)(c=>{t.initialConfig.onAddStudy?.(c)},[t.initialConfig]),Kt=(0,e.useCallback)(c=>{t.initialConfig.onSymbolChanged?.(c)},[t.initialConfig]),Gt=(0,e.useCallback)(c=>{t.initialConfig.onFirstTimeDataLoadedDone?.(c)},[t.initialConfig]),bt=(0,e.useCallback)(c=>{t.initialConfig.onDataLoadedDone?.(c)},[t.initialConfig]),Pt=(0,e.useCallback)(({chart:c,tradingViewSave:L})=>{if(L)return;[{length:7,color:"rgba(241, 156, 56, 0.7)"},{length:25,color:"rgba(234, 61, 247, 0.7)"},{length:99,color:"rgba(116, 252, 253, 0.7)"}].forEach(({length:k,color:It})=>{c.createStudy("Moving Average",!1,!1,{length:k},{"plot.linewidth":1,"plot.color":It,showLabelsOnPriceScale:!1})})},[]),Yt=(0,e.useCallback)(c=>{(t.initialConfig.onInitChart||Pt)(c);const F=c.chart,k=F.resolution(),It=(0,S.Cz)(k);Lt.current.includes(It)||F.setResolution("1D")},[Pt,t.initialConfig.onInitChart]),Ft=(0,e.useCallback)(()=>{setTimeout(()=>{if(p.current)try{const c=p.current.activeChartIndex(),L=p.current.activeChart(),F=L.chartType();Ct({chart:L,index:c}),C(F),p.current.save(k=>{Tt(k)})}catch{}},100)},[]);(0,e.useEffect)(()=>{if(!(!D||!p.current))try{p.current.activeChart().setChartType(R)}catch{}},[R,D]),(0,e.useEffect)(()=>{if(!(!D||!p.current))try{$(p.current.activeChart())}catch{}},[D,N,$]),(0,e.useEffect)(()=>{if(!(!D||!p.current))try{p.current.setLayout(St(Y))}catch{}},[Y,D]),(0,e.useEffect)(function(){if(D&&p.current)try{const L=p.current.activeChart();L.onChartTypeChanged().subscribe({},()=>{$(L,!1)})}catch{}},[$,D]);const{onTradingViewInitError:Ht,onChartReadyError:Vt}=Xt({storage:z,storageName:s});(0,e.useEffect)(()=>{if(!D)return;const c=l?"none":"auto";try{const F=document.getElementById(tt)?.querySelector("iframe");if(!F)return;F.style.pointerEvents=c}catch{}},[D,tt,l]);const me=(0,e.useMemo)(()=>({...t.initialConfig,tvConfig:{...t.initialConfig.tvConfig,container:tt,interval:m,locale:Nt},onSave:Tt,onLoadConfig:yt,onLoading:gt,onChartReadyDone:Dt,onChartReadyError:Vt,onTradingViewInitError:Ht,onIntervalChanged:pt,onFirstTimeSetThemeDone:vt,onSetThemeDone:Mt,onFirstTimeDataLoadedDone:Gt,onDataLoadedDone:bt,onInitChart:Yt,onActiveChartChanged:Ct,onSymbolChanged:Kt,onAddStudy:Ut,onScreenshotReady:Bt,onLayoutChanged:Ft}),[tt,m,Nt,Ct,Ut,Dt,Vt,bt,Gt,vt,Yt,pt,Ft,yt,gt,Tt,Bt,Mt,Kt,Ht,t.initialConfig]);return(0,A.jsx)("div",{className:"h-full",style:{display:y!==o.tU.TradingView?"none":"block"},children:(0,A.jsx)(e.Suspense,{fallback:null,children:(0,A.jsx)(Ce,{...t,interval:m,limitOrders:Se,positionOrders:Ne,executionOrders:Re,labelLines:Le,overwritingThemeOnLoaded:Oe,themeConfig:fe,initialConfig:me,onInitialized:Ae})})})}}'; + function createChartDrawingSaveFactory(__scopeChartDrawingSnapshot = scopeChartDrawingSnapshot) { + return { 76535(w, B, r) { + r.d(B, { A: () => _e, X: () => Te }); + var A = r(31085), e = r(41594), f = r(43917), g = r.n(f), G = r(19020), T = r(13067); + const b = null, v = "myTradingView"; + var K = r(78441), U = r(53837), o = r(4260), S = r(80817); + const P = (t) => { + switch (t) { + case "zh-CN": + case "cn": + return "zh"; + case "zh-HK": + case "zh-TW": + case "zh-TC": + case "tw": + return "zh_TW"; + case "ja": + return "ja"; + case "ko": + return "ko"; + case "th": + return "th"; + case "he": + return "he_IL"; + case "ru": + return "ru"; + case "cs": + return "cs"; + case "sv": + return "sv"; + case "vi": + case "vn": + return "vi"; + case "tr": + return "tr"; + case "ro": + return "ro"; + case "pt-PT": + case "pt-BR": + case "pt": + return "pt"; + case "pl": + return "pl"; + case "nl": + return "nl_NL"; + case "it": + return "it"; + case "fr": + return "fr"; + case "es": + case "es-LA": + return "es"; + case "de": + return "de"; + case "id": + return "id_ID"; + default: + return "en"; + } + }; + var x = r(46108), ot = r.n(x), Z = r(75857), rt = r.n(Z), at = r(87809), j = r.n(at), X = r(34789), Q = r.n(X), ct = r(19953), it = r.n(ct), lt = r(82438), ut = r.n(lt), a = r(87835), E = r.n(a), O = r(75257); + const W = (t) => /^(LineTool)/g.test(t), _t = (t) => `${t}.customSettings`, ft = (t) => `${t}.layout`, J = (t) => t.chartId, 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 }; + }, 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; + }, xt = async ({ storage: t, save: n, key: s, widget: i }) => { + if (!i) return { chartSave: n, drawingsSave: {} }; + const { drawings: l, charts: u } = kt({ save: __scopeChartDrawingSnapshot(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 }; + }, 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; + }, 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 }; + }, $t = () => { + const t = (0, T.w)(); + return (0, e.useCallback)((n) => { + const { colors: s } = t, i = n.isBuy ? s.Buy : s.Sell; + return { ...n, direction: n.isBuy ? "buy" : "sell", arrowStyles: { height: 8, spacing: 1, color: i }, textStyles: { color: s.PrimaryText } }; + }, [t]); + }, jt = [], Zt = ({ namespace: t, orders: n }) => { + const s = (0, o.y$)(t), [i] = s(o.yC), [l] = s(o._b), u = $t(), d = (0, e.useMemo)(() => n.map(u), [u, n]); + return (0, e.useMemo)(() => i && l === o.Ev.Single ? d : jt, [i, l, d]); + }; + var ht = r(92873); + const Xt = ({ storage: t, storageName: n }) => { + const { getI18n: s } = (0, ht.o)("", "trade-ui"), i = (0, e.useMemo)(() => s("trd-chart-tv-initialization-error", { defaultValue: "Something went wrong while initializing TradingView." }) || "", [s]), l = (0, e.useMemo)(() => s("trd-chart-crash-reload", { defaultValue: "Refresh" }) || "", [s]), u = (0, e.useMemo)(() => s("trd-chart-init-error", { defaultValue: "Failed to initialize TradingView due to malformed config, please try to refresh" }) || "", [s]), d = (0, e.useCallback)(() => window.location.reload(), []), I = (0, e.useCallback)(async () => { + await t.removeItem(n), d(); + }, [d, t, n]), [N, _] = (0, e.useState)(false), [m, y] = (0, e.useState)(""), [R, C] = (0, e.useState)(""), [Y, h] = (0, e.useState)(async () => { + }), H = (0, e.useCallback)(() => { + _(true), y(u), C(l), h(I); + }, [I, l, u]), V = (0, e.useCallback)(() => { + _(true), y(i), C(l), h(d); + }, [i, l, d]); + return { showError: N, mainText: m, buttonText: R, buttonCallback: Y, onTradingViewInitError: H, onChartReadyError: V }; + }, Qt = () => { + const t = (0, T.w)(); + return (0, e.useCallback)((n) => { + const { colors: s } = t, i = n.isBuy ? s.Buy : s.Sell; + return { ...n, lineStyles: { color: i, length: 45, unit: "percentage", style: 2, extended: false }, bodyStyles: { color: s.PrimaryText, borderColor: i, backgroundColor: i }, cancelButtonStyles: { color: s.IconNormal } }; + }, [t]); + }, Jt = () => { + const t = (0, T.w)(); + return (0, e.useCallback)((n) => { + const { colors: s } = t; + return { ...n, lineStyles: { color: s.Buy, length: 45, unit: "percentage", style: 2, extended: false }, bodyStyles: { color: s.TextBuy, borderColor: s.Buy, backgroundColor: s.DepthBuyBg }, cancelButtonStyles: { color: s.IconNormal } }; + }, [t]); + }, qt = () => { + const t = (0, T.w)(); + return (0, e.useCallback)((n) => { + const { colors: s } = t; + return { ...n, lineStyles: { color: s.EmphasizeText, length: 100, unit: "percentage", style: 0, extended: false }, bodyStyles: { color: s.RedGreenBgText, borderColor: s.EmphasizeText, backgroundColor: s.EmphasizeText }, cancelButtonStyles: { color: s.IconNormal } }; + }, [t]); + }, te = () => { + const t = (0, T.w)(); + return (0, e.useCallback)((n) => { + const { colors: s } = t; + return { ...n, lineStyles: { color: s.DisableText, length: 100, unit: "percentage", style: 2, extended: false }, bodyStyles: { color: s.PrimaryText, borderColor: s.InputLine, backgroundColor: s.InputLine }, cancelButtonStyles: { color: s.IconNormal, borderColor: s.InputLine, backgroundColor: s.BasicBg } }; + }, [t]); + }, ee = [], se = ({ namespace: t, labelLines: n }) => { + const s = (0, o.y$)(t), [i] = s(o.O), [l] = s(o.S5), [u] = s(o.ks), [d] = s(o.yA), [I] = s(o.pT), [N] = s(o._b), _ = Qt(), m = Jt(), y = qt(), R = te(), C = (0, e.useMemo)(() => i ? n.filter((M) => M.type === o.ND.AverageBuyPrice).map((M) => _({ ...M, isBuy: true })) : [], [n, i, _]), Y = (0, e.useMemo)(() => l ? n.filter((M) => M.type === o.ND.AverageSellPrice).map((M) => _({ ...M, isBuy: false })) : [], [n, l, _]), h = (0, e.useMemo)(() => u ? n.filter((M) => M.type === o.ND.BreakEvenPrice).map(m) : [], [n, u, m]), H = (0, e.useMemo)(() => d ? n.filter((M) => M.type === o.ND.LiquidationPrice).map(y) : [], [n, d, y]), V = (0, e.useMemo)(() => I ? n.filter((M) => M.type === o.ND.PriceAlert).map(R) : [], [n, I, R]), D = (0, e.useMemo)(() => [...C, ...Y, ...h, ...H, ...V], [C, Y, h, H, V]); + return (0, e.useMemo)(() => N === o.Ev.Single ? D : ee, [D, N]); + }, ne = () => { + const { getI18n: t } = (0, ht.o)("", "kline-ui"); + return (0, e.useMemo)(() => ({ openOrder: { dragTooltip: t("order-drag-tooltip", { defaultValue: "Drag for price modify" }), modifyTooltip: t("order-modify-tooltip", { defaultValue: "Click for modify" }), cancelTooltip: t("order-cancel-tooltip", { defaultValue: "Click for cancel" }) } }), [t]); + }, oe = ({ position: t, extended: n }) => { + switch (t) { + case "center": + return { length: 35, unit: "percentage", extended: n }; + case "right": + return { length: 24, unit: "pixel", extended: n }; + case "left": + return { length: -24, unit: "pixel", extended: n }; + case "left-most": + return { length: 100, unit: "percentage", extended: n }; + default: + return { length: 65, unit: "percentage", extended: n }; + } + }, re = () => { + const t = ne(), n = (0, T.w)(); + return (0, e.useCallback)((s, i) => { + const { colors: l } = n, u = s.isBuy ? l.Buy : l.Sell; + return { ...s, tooltip: s.tooltip ?? t.openOrder.dragTooltip, modifyTooltip: s.modifyTooltip ?? t.openOrder.modifyTooltip, cancelTooltip: s.cancelTooltip ?? t.openOrder.cancelTooltip, lineStyles: { ...oe(i), color: u, style: 2, extended: false }, bodyStyles: { borderColor: l.CardBg, backgroundColor: u, color: l.RedGreenBgText }, quantityStyles: { borderColor: u, backgroundColor: l.CardBg, color: u }, cancelButtonStyles: { borderColor: u, backgroundColor: l.CardBg, color: u } }; + }, [t.openOrder.dragTooltip, t.openOrder.modifyTooltip, t.openOrder.cancelTooltip, n]); + }, ae = [], ce = ({ namespace: t, orders: n }) => { + const s = (0, o.y$)(t), [i] = s(o.Nh), [l] = s(o.R3), [u] = s(o.Ec), [d] = s(o._b), [I] = s(o.CV), [N] = s(o.bv), _ = re(), m = (0, e.useMemo)(() => n.filter(({ limitOrderType: C }) => (C === void 0 || C === o.aA.LimitOrder) && i).map((C) => _(C, { position: I, extended: N })), [n, i, _, I, N]), y = (0, e.useMemo)(() => n.filter(({ limitOrderType: C }) => C === o.aA.BotPreview && l).map((C) => _(C, { position: "legacy", extended: false })), [n, l, _]), R = (0, e.useMemo)(() => n.filter(({ limitOrderType: C }) => C === o.aA.OrderPreview && u).map((C) => _(C, { position: "left-most", extended: false })), [n, u, _]); + return (0, e.useMemo)(() => d === o.Ev.Single ? [...m, ...y, ...R] : ae, [m, y, R, d]); + }, ie = () => { + const t = (0, T.w)(); + return (0, e.useCallback)((n) => { + const { colors: s } = t, i = n.isBuy ? s.Buy : s.Sell, l = n.PNL > 0 ? s.Buy : n.PNL < 0 ? s.Sell : i; + return { ...n, lineStyles: { color: l, length: 100, unit: "percentage", style: 2, extended: false }, bodyStyles: { borderColor: l, backgroundColor: l, color: s.RedGreenBgText }, quantityStyles: { borderColor: i, backgroundColor: s.CardBg, color: i }, reverseButtonStyles: { borderColor: i, backgroundColor: s.CardBg, color: i }, closeButtonStyles: { borderColor: i, backgroundColor: s.CardBg, color: i } }; + }, [t]); + }, le = [], ue = ({ namespace: t, orders: n }) => { + const s = (0, o.y$)(t), [i] = s(o.FH), [l] = s(o._b), u = ie(), d = (0, e.useMemo)(() => n.map(u), [u, n]); + return (0, e.useMemo)(() => i && l === o.Ev.Single ? [...d] : le, [i, l, d]); + }; + var Ee = r(6868); + const de = ({ namespace: t }) => { + const n = (0, o.y$)(t), [s, i] = n(o.mn), l = (0, Ee.r)(), u = l((_) => _.hasHydrated), d = l((_) => _.lastUpdatedTimestamp), I = (0, e.useMemo)(() => u && d !== s, [u, d, s]), N = (0, e.useCallback)(() => { + i(d); + }, [d, i]); + return (0, e.useMemo)(() => ({ overwritingThemeOnLoaded: I, syncThemeTimestamp: N }), [I, N]); + }, Te = () => r.e("66202").then(r.bind(r, 78273)), Ce = (0, e.lazy)(() => r.e("66202").then(r.bind(r, 78273))), Ie = (t) => (0, e.useMemo)(() => g().createInstance({ name: t }), [t]), St = (t) => { + switch (t) { + case o.Ev.HorizontalTwo: + return "2v"; + case o.Ev.VerticalTwo: + return "2h"; + case o.Ev.HorizontalThree: + return "3v"; + case o.Ev.VerticalThree: + return "3h"; + case o.Ev.LeftOneRightTwo: + return "3s"; + case o.Ev.TopOneBottomTwo: + return "1-2"; + case o.Ev.Four: + return "4"; + case o.Ev.VerticalFour: + return "4h"; + case o.Ev.LeftOneRightThree: + return "4s"; + case o.Ev.TopOneBottomThree: + return "1-3"; + case o.Ev.Five: + return "2-3"; + case o.Ev.SixTwoByThree: + return "6"; + case o.Ev.SixThreeByTwo: + return "6c"; + case o.Ev.Eight: + return "8"; + default: + return "s"; + } + }, _e = ({ tradingViewProps: t, namespace: n, storageName: s = v, containerId: i = "tradingview", disablePointerEvents: l = false }) => { + const u = (0, o.y$)(n), d = (0, U.y$)(n), I = (0, K.y)(n), [N, _] = u(o.A8), m = u(o.WK), [y] = u(o.e6), [R, C] = u(o.iw), [Y] = u(o._b), [, h] = I(K.N), [, H] = d(U.yj), [V] = d(U.Cf), [D, M] = (0, e.useState)(false), z = Ie(n), q = (0, G.Bl)(), Nt = (0, e.useMemo)(() => P(q), [q]), fe = (0, T.w)(), tt = (0, e.useMemo)(() => `${n}-${i}`, [i, n]), p = (0, e.useRef)(), Et = (0, e.useRef)(N), Rt = (0, e.useRef)(R), dt = (0, e.useRef)(Y), et = (0, e.useRef)(z), st = (0, e.useRef)(s), Lt = (0, e.useRef)(V), he = (0, e.useRef)(D); + (0, e.useEffect)(() => { + Et.current = N; + }, [N]), (0, e.useEffect)(() => { + Rt.current = R; + }, [R]), (0, e.useEffect)(() => { + dt.current = Y; + }, [Y]), (0, e.useEffect)(() => { + et.current = z; + }, [z]), (0, e.useEffect)(() => { + st.current = s; + }, [s]), (0, e.useEffect)(() => { + he.current = D; + }, [D]), (0, e.useEffect)(() => { + Lt.current = V; + }, [V]); + const Se = ce({ namespace: n, orders: t.limitOrders }), Ne = ue({ namespace: n, orders: t.positionOrders }), Re = Zt({ namespace: n, orders: t.executionOrders }), Le = se({ namespace: n, labelLines: t.labelLines }), { overwritingThemeOnLoaded: Oe, syncThemeTimestamp: nt } = de({ namespace: n }), Ot = (0, e.useCallback)(({ resolution: c, chartType: L }) => { + const F = (0, S.Cz)(c); + _(L === 3 ? "time" : F); + }, [_]), At = (0, e.useCallback)(async () => { + p.current && await Wt({ storage: et.current, key: st.current, widget: p.current }); + }, []), mt = (0, e.useCallback)(async (c) => zt({ storage: et.current, key: st.current }), []), Tt = (0, e.useCallback)(async (c) => { + const { chartSave: L } = await xt({ storage: et.current, save: c, key: st.current, widget: p.current }); + await t.initialConfig.onSave?.(L); + }, [t.initialConfig]), yt = (0, e.useCallback)(async () => { + const c = await mt(dt.current); + if (t.initialConfig.onLoadConfig?.(), !c.chartSave) return c; + const L = St(dt.current); + return c.chartSave.layout === L ? c : { ...c, chartSave: { ...c.chartSave, layout: L } }; + }, [mt, t.initialConfig]), gt = (0, e.useCallback)((c) => { + h(c), t.initialConfig.onLoading?.(c); + }, [h, t.initialConfig]), $ = (0, e.useCallback)((c, L = true) => { + L ? c.setChartType(Et.current === "time" ? 2 : Rt.current) : Et.current === "time" && c.setChartType(2); + }, []), Ae = (0, e.useCallback)((c) => { + p.current = c, H(c); + }, [H]), Dt = (0, e.useCallback)(() => { + setTimeout(() => { + if (p.current) try { + const c = p.current?.activeChart(); + $(c); + } catch { + } + }, 0), M(true), t.initialConfig.onChartReadyDone?.(); + }, [$, t.initialConfig]), Mt = (0, e.useCallback)(() => { + nt(), t.initialConfig.onSetThemeDone?.(); + }, [nt, t.initialConfig]), vt = (0, e.useCallback)(() => { + nt(), t.initialConfig.onFirstTimeSetThemeDone?.(); + }, [nt, t.initialConfig]), pt = (0, e.useCallback)((c) => { + t.initialConfig.onIntervalChanged?.(c); + }, [t.initialConfig]), Ct = (0, e.useCallback)(async (c) => { + const { chart: L } = c; + if (!L.symbolExt()) return; + const k = L.chartType(); + C(k), Ot({ resolution: L.resolution(), chartType: k }), await At(), t.initialConfig.onActiveChartChanged?.(c); + }, [At, Ot, C, t.initialConfig]), Bt = (0, e.useCallback)((c) => { + window.open(`https://www.tradingview.com/x/${c}`, "_blank"); + }, []), Ut = (0, e.useCallback)((c) => { + t.initialConfig.onAddStudy?.(c); + }, [t.initialConfig]), Kt = (0, e.useCallback)((c) => { + t.initialConfig.onSymbolChanged?.(c); + }, [t.initialConfig]), Gt = (0, e.useCallback)((c) => { + t.initialConfig.onFirstTimeDataLoadedDone?.(c); + }, [t.initialConfig]), bt = (0, e.useCallback)((c) => { + t.initialConfig.onDataLoadedDone?.(c); + }, [t.initialConfig]), Pt = (0, e.useCallback)(({ chart: c, tradingViewSave: L }) => { + if (L) return; + [{ length: 7, color: "rgba(241, 156, 56, 0.7)" }, { length: 25, color: "rgba(234, 61, 247, 0.7)" }, { length: 99, color: "rgba(116, 252, 253, 0.7)" }].forEach(({ length: k, color: It }) => { + c.createStudy("Moving Average", false, false, { length: k }, { "plot.linewidth": 1, "plot.color": It, showLabelsOnPriceScale: false }); + }); + }, []), Yt = (0, e.useCallback)((c) => { + (t.initialConfig.onInitChart || Pt)(c); + const F = c.chart, k = F.resolution(), It = (0, S.Cz)(k); + Lt.current.includes(It) || F.setResolution("1D"); + }, [Pt, t.initialConfig.onInitChart]), Ft = (0, e.useCallback)(() => { + setTimeout(() => { + if (p.current) try { + const c = p.current.activeChartIndex(), L = p.current.activeChart(), F = L.chartType(); + Ct({ chart: L, index: c }), C(F), p.current.save((k) => { + Tt(k); + }); + } catch { + } + }, 100); + }, []); + (0, e.useEffect)(() => { + if (!(!D || !p.current)) try { + p.current.activeChart().setChartType(R); + } catch { + } + }, [R, D]), (0, e.useEffect)(() => { + if (!(!D || !p.current)) try { + $(p.current.activeChart()); + } catch { + } + }, [D, N, $]), (0, e.useEffect)(() => { + if (!(!D || !p.current)) try { + p.current.setLayout(St(Y)); + } catch { + } + }, [Y, D]), (0, e.useEffect)(function() { + if (D && p.current) try { + const L = p.current.activeChart(); + L.onChartTypeChanged().subscribe({}, () => { + $(L, false); + }); + } catch { + } + }, [$, D]); + const { onTradingViewInitError: Ht, onChartReadyError: Vt } = Xt({ storage: z, storageName: s }); + (0, e.useEffect)(() => { + if (!D) return; + const c = l ? "none" : "auto"; + try { + const F = document.getElementById(tt)?.querySelector("iframe"); + if (!F) return; + F.style.pointerEvents = c; + } catch { + } + }, [D, tt, l]); + const me = (0, e.useMemo)(() => ({ ...t.initialConfig, tvConfig: { ...t.initialConfig.tvConfig, container: tt, interval: m, locale: Nt }, onSave: Tt, onLoadConfig: yt, onLoading: gt, onChartReadyDone: Dt, onChartReadyError: Vt, onTradingViewInitError: Ht, onIntervalChanged: pt, onFirstTimeSetThemeDone: vt, onSetThemeDone: Mt, onFirstTimeDataLoadedDone: Gt, onDataLoadedDone: bt, onInitChart: Yt, onActiveChartChanged: Ct, onSymbolChanged: Kt, onAddStudy: Ut, onScreenshotReady: Bt, onLayoutChanged: Ft }), [tt, m, Nt, Ct, Ut, Dt, Vt, bt, Gt, vt, Yt, pt, Ft, yt, gt, Tt, Bt, Mt, Kt, Ht, t.initialConfig]); + return (0, A.jsx)("div", { className: "h-full", style: { display: y !== o.tU.TradingView ? "none" : "block" }, children: (0, A.jsx)(e.Suspense, { fallback: null, children: (0, A.jsx)(Ce, { ...t, interval: m, limitOrders: Se, positionOrders: Ne, executionOrders: Re, labelLines: Le, overwritingThemeOnLoaded: Oe, themeConfig: fe, initialConfig: me, onInitialized: Ae }) }) }); + }; + } }[76535]; + } + function replaceChartDrawingSaveFactory(originalFactory) { + if (typeof originalFactory !== "function" || Function.prototype.toString.call(originalFactory) !== originalFactorySource2) { + throw new Error("Drawing save factory source does not match the pinned public module"); + } + return createChartDrawingSaveFactory(); + } + // src/binance-orderbook-trade/chart-storage/json.js var encoder = new TextEncoder(); var nativeObjectSource = Function.prototype.toString.call(Object); @@ -6377,79 +6765,103 @@ // src/binance-orderbook-trade/chart-storage/runtime.js var CAPTURE_DEADLINE_MS = 3e4; + var MIRROR_MODULE = "70940"; + var DRAWING_MODULE = "76535"; 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 }; + const drawingScope = { status: "waiting", reason: null, attempts: 0, matches: 0, executions: 0 }; let observer; let timer; let stopping; + let observationFinished = false; function snapshot() { const stats = writer.getStats(); return { ...state, status: stats.failedBatches > 0 ? "native_after_failure" : state.status, phase: writer.getPhase(), - writer: stats + writer: stats, + drawingScope: { ...drawingScope } }; } 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(); }); + if (observer) observer.stopTarget(MIRROR_MODULE, reason); + if (observationFinished) self.removeEventListener("pagehide", onPageHide); return stopping; } + function finishObservation() { + observationFinished = true; + clearTimeout(timer); + if (stopping) self.removeEventListener("pagehide", onPageHide); + } function onPageHide() { + if (observer) observer.stop("pagehide"); void stop("pagehide"); } function dispatch(target, entries, nativeThunk) { if (!isChartStoragePage()) void stop("scope_changed"); return writer.dispatch(target, entries, nativeThunk); } + function replaceFactory(id, original, replace) { + if (!isChartStoragePage()) { + observer.stop("scope_changed"); + void stop("scope_changed"); + return original; + } + const targetState = id === MIRROR_MODULE ? state : drawingScope; + targetState.attempts += 1; + const replacement = replace(original); + targetState.matches += 1; + return function(...args) { + targetState.executions += 1; + return Reflect.apply(replacement, this, args); + }; + } 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; + targets: { + [MIRROR_MODULE]: (original) => replaceFactory(MIRROR_MODULE, original, (factory) => replaceChartMirrorFactory(factory, dispatch)), + [DRAWING_MODULE]: (original) => replaceFactory(DRAWING_MODULE, original, replaceChartDrawingSaveFactory) + }, + onCapture(id) { + const targetState = id === MIRROR_MODULE ? state : drawingScope; + if (id !== MIRROR_MODULE || !stopping) targetState.status = "active"; + }, + onFailure(id, reason) { + if (id === MIRROR_MODULE) { + void stop(reason === "execution_failed" ? "capture_failed" : reason); + } else { + drawingScope.status = reason === "source_mismatch" ? "source_mismatch" : "unavailable"; + drawingScope.reason = reason; } - state.matches += 1; - return function(...args) { - state.executions += 1; - return Reflect.apply(replacement, this, args); - }; }, - onCapture() { - clearTimeout(timer); - state.status = "active"; + onComplete() { + finishObservation(); } }); } catch { + drawingScope.status = "unavailable"; + drawingScope.reason = "bootstrap_unavailable"; + finishObservation(); 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); + if (!observationFinished) { + self.addEventListener("pagehide", onPageHide, { once: true }); + timer = setTimeout(() => { + observer.stop("capture_deadline"); + if (state.status === "waiting") void stop("capture_deadline"); + }, CAPTURE_DEADLINE_MS); + } return Object.freeze({ snapshot, stop }); } diff --git a/scripts/generate-chart-drawing-save-module.mjs b/scripts/generate-chart-drawing-save-module.mjs new file mode 100644 index 0000000..abd396c --- /dev/null +++ b/scripts/generate-chart-drawing-save-module.mjs @@ -0,0 +1,100 @@ +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]; +assert(input, 'Supply the verified public 36648.dd8ae7dc.js response'); +const source = await readFile(input, 'utf8'); +const sha = value => createHash('sha256').update(value).digest('hex'); +assert.equal(sha(source), '8191b50fb47212425d03146834207645c0e89b8ce623e23a71477194ee9a60ff'); +const ast = parse(source, { ecmaVersion: 'latest' }); +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.method && node.key.value === 76535) factories.push(node); +}); +assert.equal(factories.length, 1); +const factory = factories[0]; +const original = source.slice(factory.start, factory.end); +const targets = []; +visit(factory, node => { + assert(node.type !== 'Identifier' || node.name !== '__scopeChartDrawingSnapshot', 'Scope helper is shadowed'); + if (node.type === 'CallExpression' && node.callee.name === 'kt' + && source.slice(node.start, node.end) === 'kt({save:n})') targets.push(node); +}); +assert.equal(targets.length, 1); +const target = targets[0]; +const raw = source.slice(target.start, target.end); +const inserted = 'kt({save:__scopeChartDrawingSnapshot(n)})'; +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); +const moduleSource = `/* Generated from the pinned native 76535 factory; see drawing-save-manifest.json. */ +import { scopeChartDrawingSnapshot } from './drawing-save-scope.js'; +const originalFactorySource = ${JSON.stringify(original)}; + +/** Preserve native initialization, event subscriptions and storage except snapshot ownership. */ +export function createChartDrawingSaveFactory(__scopeChartDrawingSnapshot = scopeChartDrawingSnapshot) { + return {${transformed}}[76535]; +} + +/** Unknown host source must remain native rather than accepting an approximate patch. */ +export function replaceChartDrawingSaveFactory(originalFactory) { + if (typeof originalFactory !== 'function' || Function.prototype.toString.call(originalFactory) !== originalFactorySource) { + throw new Error('Drawing save factory source does not match the pinned public module'); + } + return createChartDrawingSaveFactory(); +} +`; +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:76535:')); +assert(declarations.length > 0); +let replacementCount = 0; +const slices = declarations.map(fragment => { + const raw = source.slice(...fragment.source_utf16_range); + assert.equal(sha(raw), fragment.sha256, fragment.label); + if (fragment.source_utf16_range[0] <= target.start && fragment.source_utf16_range[1] >= target.end) { + replacementCount += 1; + return raw.slice(0, target.start - fragment.source_utf16_range[0]) + inserted + + raw.slice(target.end - fragment.source_utf16_range[0]); + } + return raw; +}); +assert.equal(replacementCount, 1); +const callback = hostManifest.fragments.find(fragment => fragment.label === 'callback:76535:Tt'); +assert(callback); +const callbackSource = source.slice(...callback.source_utf16_range); +assert.equal(sha(callbackSource), callback.sha256); +const fixtureSource = `/* Exact native dependencies and save callback, with the same sole production replacement. */ +export function createNativeDrawingSaveFactory() { return {${original}}[76535]; } +export function createDrawingSaveCallbacks(r,__scopeChartDrawingSnapshot) { +${slices.map(raw => `var ${raw};`).join('\n')} +return { save: xt, load: zt, createSaveCallback(storage,key,widget,onSave) { +const et={current:storage},st={current:key},p={current:widget},t={initialConfig:{onSave}}; +return ${callbackSource}; +} }; +} +`; +parse(moduleSource, { ecmaVersion: 'latest', sourceType: 'module' }); +parse(fixtureSource, { ecmaVersion: 'latest', sourceType: 'module' }); +await writeFile(new URL('../src/binance-orderbook-trade/chart-storage/drawing-save-module.js', import.meta.url), moduleSource); +await writeFile(new URL('drawing-save-scoped.js', fixtureRoot), fixtureSource); +const manifest = { + source: { url: 'https://bin.bnbstatic.com/static/chunks/36648.dd8ae7dc.js', sha256: sha(source) }, + originalFactory: { range: [factory.start, factory.end], sha256: sha(original) }, + replacement: { range: [target.start, target.end], original: raw, inserted }, + outputs: { module: sha(moduleSource), fixture: sha(fixtureSource) }, + scope: 'Only xt calls kt with a snapshot restricted to each chart\'s current MainSeries symbols. Other native methods and events are unchanged.', +}; +await writeFile(new URL('drawing-save-manifest.json', fixtureRoot), JSON.stringify(manifest, null, 2) + '\n'); +console.log(JSON.stringify(manifest)); diff --git a/src/binance-orderbook-trade/chart-storage/bootstrap.js b/src/binance-orderbook-trade/chart-storage/bootstrap.js index fbded35..50890a6 100644 --- a/src/binance-orderbook-trade/chart-storage/bootstrap.js +++ b/src/binance-orderbook-trade/chart-storage/bootstrap.js @@ -3,16 +3,20 @@ 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. + * Own one early Rspack queue accessor for explicitly pinned factory replacements. + * Each target terminates independently; restoring registration never changes the + * runtime's module cache or an already-created export. */ -export function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } = {}) { +export function observeChartStorageBootstrap({ targets, onCapture, onFailure, onComplete }) { 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 registrations = new Map(Object.entries(targets).map(([id, replaceFactory]) => { + if (typeof replaceFactory !== 'function') throw new Error('Storage replacement must be a synchronous function'); + return [id, { id, replaceFactory, terminal: false }]; + })); + if (registrations.size === 0) throw new Error('Storage bootstrap requires explicit module targets'); + for (const callback of [onCapture, onFailure, onComplete]) { + if (typeof callback !== 'function') throw new Error('Storage observation callbacks must be synchronous functions'); + } const globalDescriptor = Object.getOwnPropertyDescriptor(self, QUEUE_NAME); if (globalDescriptor && !Object.hasOwn(globalDescriptor, 'value')) throw new Error('Storage bootstrap requires a native queue property'); @@ -28,19 +32,20 @@ export function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } if (createdQueue) Object.defineProperty(self, QUEUE_NAME, { value: queue, writable: true, configurable: true, enumerable: true }); let runtimePush; + let runtimeRequire; 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; + function restoreFactory(target) { + if (target.observedFactory && runtimeRequire && runtimeRequire.m[target.id] === target.observedFactory) { + runtimeRequire.m[target.id] = target.originalFactory; + } + if (target.observedChunk && target.observedChunk[1][target.id] === target.observedFactory) { + target.observedChunk[1][target.id] = target.originalFactory; + } + } + + function restoreQueue() { 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'); @@ -56,67 +61,84 @@ export function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } } } - /** Observational failure is isolated from Rspack's persistent module-error cache. */ - function finish(succeeded, result) { - if (!active) return; + /** Observational cleanup must not enter Rspack's persistent module-error cache. */ + function completeIfFinished() { + if (!active || [...registrations.values()].some(target => !target.terminal)) return; active = false; + let reason = null; try { - restore(); - } catch (cleanupError) { - if (succeeded) { - succeeded = false; - result = cleanupError; - } + restoreQueue(); + } catch { + // A new owner keeps its accessor; report the lost boundary without replacing it. + reason = 'ownership_lost'; } - if (succeeded) resolveCapture(result); - else rejectCapture(result); + onComplete(reason); + } + + function finish(target, reason) { + if (target.terminal) return; + target.terminal = true; + restoreFactory(target); + if (reason === 'captured') onCapture(target.id); + else onFailure(target.id, reason); + completeIfFinished(); + } + + function stop(reason = 'stopped') { + for (const target of registrations.values()) finish(target, reason); } 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 (!Array.isArray(chunk) || !Array.isArray(chunk[0]) || !chunk[1] || typeof chunk[1] !== 'object' || + (chunk[2] !== undefined && typeof chunk[2] !== 'function')) { + stop('unsupported_registration'); + return chunk; + } + let observedChunk; + for (const target of registrations.values()) { + if (target.terminal || !Object.hasOwn(chunk[1], target.id)) continue; + if (target.originalFactory || typeof chunk[1][target.id] !== 'function') { + finish(target, 'unsupported_registration'); + continue; + } + target.originalFactory = chunk[1][target.id]; + let executionFactory; + try { + executionFactory = target.replaceFactory(target.originalFactory); + } catch { + // A failed complete-source pin leaves only this target's registration native. + finish(target, 'source_mismatch'); + continue; } - 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) { + if (target.terminal) continue; + if (typeof executionFactory !== 'function') { + finish(target, 'unsupported_registration'); + continue; + } + target.observedFactory = function (module, exports, require) { runtimeRequire = require; let returned; try { returned = executionFactory.call(this, module, exports, require); } catch (hostError) { - finish(false, hostError); + finish(target, 'execution_failed'); 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); - } - } + finish(target, 'captured'); 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; + if (!observedChunk) { + const hostRuntime = chunk[2]; + observedChunk = [chunk[0], { ...chunk[1] }, require => { + runtimeRequire = require; + if (hostRuntime !== undefined) return hostRuntime(require); + }]; + } + observedChunk[1][target.id] = target.observedFactory; + target.observedChunk = observedChunk; } + return observedChunk || chunk; } /** Rspack retains this parent callback; after takeover it must only append. */ @@ -144,7 +166,7 @@ export function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } return; } if (runtimePush || typeof dispatcher !== 'function') { - finish(false, new Error('Storage bootstrap observed an unexpected push replacement')); + stop('unexpected_dispatcher'); Object.defineProperty(queue, 'push', { value: dispatcher, writable: true, configurable: true, enumerable: true }); return; } @@ -153,9 +175,11 @@ export function observeChartStorageBootstrap({ onCapture, replaceMirrorFactory } 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')); + stop, + stopTarget(id, reason = 'stopped') { + const target = registrations.get(id); + if (!target) throw new Error('Storage bootstrap target is not registered'); + finish(target, reason); }, }); } diff --git a/src/binance-orderbook-trade/chart-storage/drawing-save-module.js b/src/binance-orderbook-trade/chart-storage/drawing-save-module.js new file mode 100644 index 0000000..6e34281 --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/drawing-save-module.js @@ -0,0 +1,16 @@ +/* Generated from the pinned native 76535 factory; see drawing-save-manifest.json. */ +import { scopeChartDrawingSnapshot } from './drawing-save-scope.js'; +const originalFactorySource = "76535(w,B,r){r.d(B,{A:()=>_e,X:()=>Te});var A=r(31085),e=r(41594),f=r(43917),g=r.n(f),G=r(19020),T=r(13067);const b=null,v=\"myTradingView\";var K=r(78441),U=r(53837),o=r(4260),S=r(80817);const P=t=>{switch(t){case\"zh-CN\":case\"cn\":return\"zh\";case\"zh-HK\":case\"zh-TW\":case\"zh-TC\":case\"tw\":return\"zh_TW\";case\"ja\":return\"ja\";case\"ko\":return\"ko\";case\"th\":return\"th\";case\"he\":return\"he_IL\";case\"ru\":return\"ru\";case\"cs\":return\"cs\";case\"sv\":return\"sv\";case\"vi\":case\"vn\":return\"vi\";case\"tr\":return\"tr\";case\"ro\":return\"ro\";case\"pt-PT\":case\"pt-BR\":case\"pt\":return\"pt\";case\"pl\":return\"pl\";case\"nl\":return\"nl_NL\";case\"it\":return\"it\";case\"fr\":return\"fr\";case\"es\":case\"es-LA\":return\"es\";case\"de\":return\"de\";case\"id\":return\"id_ID\";default:return\"en\"}};var x=r(46108),ot=r.n(x),Z=r(75857),rt=r.n(Z),at=r(87809),j=r.n(at),X=r(34789),Q=r.n(X),ct=r(19953),it=r.n(ct),lt=r(82438),ut=r.n(lt),a=r(87835),E=r.n(a),O=r(75257);const W=t=>/^(LineTool)/g.test(t),_t=t=>`${t}.customSettings`,ft=t=>`${t}.layout`,J=t=>t.chartId,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}},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},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}},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},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}},$t=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell;return{...n,direction:n.isBuy?\"buy\":\"sell\",arrowStyles:{height:8,spacing:1,color:i},textStyles:{color:s.PrimaryText}}},[t])},jt=[],Zt=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.yC),[l]=s(o._b),u=$t(),d=(0,e.useMemo)(()=>n.map(u),[u,n]);return(0,e.useMemo)(()=>i&&l===o.Ev.Single?d:jt,[i,l,d])};var ht=r(92873);const Xt=({storage:t,storageName:n})=>{const{getI18n:s}=(0,ht.o)(\"\",\"trade-ui\"),i=(0,e.useMemo)(()=>s(\"trd-chart-tv-initialization-error\",{defaultValue:\"Something went wrong while initializing TradingView.\"})||\"\",[s]),l=(0,e.useMemo)(()=>s(\"trd-chart-crash-reload\",{defaultValue:\"Refresh\"})||\"\",[s]),u=(0,e.useMemo)(()=>s(\"trd-chart-init-error\",{defaultValue:\"Failed to initialize TradingView due to malformed config, please try to refresh\"})||\"\",[s]),d=(0,e.useCallback)(()=>window.location.reload(),[]),I=(0,e.useCallback)(async()=>{await t.removeItem(n),d()},[d,t,n]),[N,_]=(0,e.useState)(!1),[m,y]=(0,e.useState)(\"\"),[R,C]=(0,e.useState)(\"\"),[Y,h]=(0,e.useState)(async()=>{}),H=(0,e.useCallback)(()=>{_(!0),y(u),C(l),h(I)},[I,l,u]),V=(0,e.useCallback)(()=>{_(!0),y(i),C(l),h(d)},[i,l,d]);return{showError:N,mainText:m,buttonText:R,buttonCallback:Y,onTradingViewInitError:H,onChartReadyError:V}},Qt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell;return{...n,lineStyles:{color:i,length:45,unit:\"percentage\",style:2,extended:!1},bodyStyles:{color:s.PrimaryText,borderColor:i,backgroundColor:i},cancelButtonStyles:{color:s.IconNormal}}},[t])},Jt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.Buy,length:45,unit:\"percentage\",style:2,extended:!1},bodyStyles:{color:s.TextBuy,borderColor:s.Buy,backgroundColor:s.DepthBuyBg},cancelButtonStyles:{color:s.IconNormal}}},[t])},qt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.EmphasizeText,length:100,unit:\"percentage\",style:0,extended:!1},bodyStyles:{color:s.RedGreenBgText,borderColor:s.EmphasizeText,backgroundColor:s.EmphasizeText},cancelButtonStyles:{color:s.IconNormal}}},[t])},te=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.DisableText,length:100,unit:\"percentage\",style:2,extended:!1},bodyStyles:{color:s.PrimaryText,borderColor:s.InputLine,backgroundColor:s.InputLine},cancelButtonStyles:{color:s.IconNormal,borderColor:s.InputLine,backgroundColor:s.BasicBg}}},[t])},ee=[],se=({namespace:t,labelLines:n})=>{const s=(0,o.y$)(t),[i]=s(o.O),[l]=s(o.S5),[u]=s(o.ks),[d]=s(o.yA),[I]=s(o.pT),[N]=s(o._b),_=Qt(),m=Jt(),y=qt(),R=te(),C=(0,e.useMemo)(()=>i?n.filter(M=>M.type===o.ND.AverageBuyPrice).map(M=>_({...M,isBuy:!0})):[],[n,i,_]),Y=(0,e.useMemo)(()=>l?n.filter(M=>M.type===o.ND.AverageSellPrice).map(M=>_({...M,isBuy:!1})):[],[n,l,_]),h=(0,e.useMemo)(()=>u?n.filter(M=>M.type===o.ND.BreakEvenPrice).map(m):[],[n,u,m]),H=(0,e.useMemo)(()=>d?n.filter(M=>M.type===o.ND.LiquidationPrice).map(y):[],[n,d,y]),V=(0,e.useMemo)(()=>I?n.filter(M=>M.type===o.ND.PriceAlert).map(R):[],[n,I,R]),D=(0,e.useMemo)(()=>[...C,...Y,...h,...H,...V],[C,Y,h,H,V]);return(0,e.useMemo)(()=>N===o.Ev.Single?D:ee,[D,N])},ne=()=>{const{getI18n:t}=(0,ht.o)(\"\",\"kline-ui\");return(0,e.useMemo)(()=>({openOrder:{dragTooltip:t(\"order-drag-tooltip\",{defaultValue:\"Drag for price modify\"}),modifyTooltip:t(\"order-modify-tooltip\",{defaultValue:\"Click for modify\"}),cancelTooltip:t(\"order-cancel-tooltip\",{defaultValue:\"Click for cancel\"})}}),[t])},oe=({position:t,extended:n})=>{switch(t){case\"center\":return{length:35,unit:\"percentage\",extended:n};case\"right\":return{length:24,unit:\"pixel\",extended:n};case\"left\":return{length:-24,unit:\"pixel\",extended:n};case\"left-most\":return{length:100,unit:\"percentage\",extended:n};default:return{length:65,unit:\"percentage\",extended:n}}},re=()=>{const t=ne(),n=(0,T.w)();return(0,e.useCallback)((s,i)=>{const{colors:l}=n,u=s.isBuy?l.Buy:l.Sell;return{...s,tooltip:s.tooltip??t.openOrder.dragTooltip,modifyTooltip:s.modifyTooltip??t.openOrder.modifyTooltip,cancelTooltip:s.cancelTooltip??t.openOrder.cancelTooltip,lineStyles:{...oe(i),color:u,style:2,extended:!1},bodyStyles:{borderColor:l.CardBg,backgroundColor:u,color:l.RedGreenBgText},quantityStyles:{borderColor:u,backgroundColor:l.CardBg,color:u},cancelButtonStyles:{borderColor:u,backgroundColor:l.CardBg,color:u}}},[t.openOrder.dragTooltip,t.openOrder.modifyTooltip,t.openOrder.cancelTooltip,n])},ae=[],ce=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.Nh),[l]=s(o.R3),[u]=s(o.Ec),[d]=s(o._b),[I]=s(o.CV),[N]=s(o.bv),_=re(),m=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>(C===void 0||C===o.aA.LimitOrder)&&i).map(C=>_(C,{position:I,extended:N})),[n,i,_,I,N]),y=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>C===o.aA.BotPreview&&l).map(C=>_(C,{position:\"legacy\",extended:!1})),[n,l,_]),R=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>C===o.aA.OrderPreview&&u).map(C=>_(C,{position:\"left-most\",extended:!1})),[n,u,_]);return(0,e.useMemo)(()=>d===o.Ev.Single?[...m,...y,...R]:ae,[m,y,R,d])},ie=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell,l=n.PNL>0?s.Buy:n.PNL<0?s.Sell:i;return{...n,lineStyles:{color:l,length:100,unit:\"percentage\",style:2,extended:!1},bodyStyles:{borderColor:l,backgroundColor:l,color:s.RedGreenBgText},quantityStyles:{borderColor:i,backgroundColor:s.CardBg,color:i},reverseButtonStyles:{borderColor:i,backgroundColor:s.CardBg,color:i},closeButtonStyles:{borderColor:i,backgroundColor:s.CardBg,color:i}}},[t])},le=[],ue=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.FH),[l]=s(o._b),u=ie(),d=(0,e.useMemo)(()=>n.map(u),[u,n]);return(0,e.useMemo)(()=>i&&l===o.Ev.Single?[...d]:le,[i,l,d])};var Ee=r(6868);const de=({namespace:t})=>{const n=(0,o.y$)(t),[s,i]=n(o.mn),l=(0,Ee.r)(),u=l(_=>_.hasHydrated),d=l(_=>_.lastUpdatedTimestamp),I=(0,e.useMemo)(()=>u&&d!==s,[u,d,s]),N=(0,e.useCallback)(()=>{i(d)},[d,i]);return(0,e.useMemo)(()=>({overwritingThemeOnLoaded:I,syncThemeTimestamp:N}),[I,N])},Te=()=>r.e(\"66202\").then(r.bind(r,78273)),Ce=(0,e.lazy)(()=>r.e(\"66202\").then(r.bind(r,78273))),Ie=t=>(0,e.useMemo)(()=>g().createInstance({name:t}),[t]),St=t=>{switch(t){case o.Ev.HorizontalTwo:return\"2v\";case o.Ev.VerticalTwo:return\"2h\";case o.Ev.HorizontalThree:return\"3v\";case o.Ev.VerticalThree:return\"3h\";case o.Ev.LeftOneRightTwo:return\"3s\";case o.Ev.TopOneBottomTwo:return\"1-2\";case o.Ev.Four:return\"4\";case o.Ev.VerticalFour:return\"4h\";case o.Ev.LeftOneRightThree:return\"4s\";case o.Ev.TopOneBottomThree:return\"1-3\";case o.Ev.Five:return\"2-3\";case o.Ev.SixTwoByThree:return\"6\";case o.Ev.SixThreeByTwo:return\"6c\";case o.Ev.Eight:return\"8\";default:return\"s\"}},_e=({tradingViewProps:t,namespace:n,storageName:s=v,containerId:i=\"tradingview\",disablePointerEvents:l=!1})=>{const u=(0,o.y$)(n),d=(0,U.y$)(n),I=(0,K.y)(n),[N,_]=u(o.A8),m=u(o.WK),[y]=u(o.e6),[R,C]=u(o.iw),[Y]=u(o._b),[,h]=I(K.N),[,H]=d(U.yj),[V]=d(U.Cf),[D,M]=(0,e.useState)(!1),z=Ie(n),q=(0,G.Bl)(),Nt=(0,e.useMemo)(()=>P(q),[q]),fe=(0,T.w)(),tt=(0,e.useMemo)(()=>`${n}-${i}`,[i,n]),p=(0,e.useRef)(),Et=(0,e.useRef)(N),Rt=(0,e.useRef)(R),dt=(0,e.useRef)(Y),et=(0,e.useRef)(z),st=(0,e.useRef)(s),Lt=(0,e.useRef)(V),he=(0,e.useRef)(D);(0,e.useEffect)(()=>{Et.current=N},[N]),(0,e.useEffect)(()=>{Rt.current=R},[R]),(0,e.useEffect)(()=>{dt.current=Y},[Y]),(0,e.useEffect)(()=>{et.current=z},[z]),(0,e.useEffect)(()=>{st.current=s},[s]),(0,e.useEffect)(()=>{he.current=D},[D]),(0,e.useEffect)(()=>{Lt.current=V},[V]);const Se=ce({namespace:n,orders:t.limitOrders}),Ne=ue({namespace:n,orders:t.positionOrders}),Re=Zt({namespace:n,orders:t.executionOrders}),Le=se({namespace:n,labelLines:t.labelLines}),{overwritingThemeOnLoaded:Oe,syncThemeTimestamp:nt}=de({namespace:n}),Ot=(0,e.useCallback)(({resolution:c,chartType:L})=>{const F=(0,S.Cz)(c);_(L===3?\"time\":F)},[_]),At=(0,e.useCallback)(async()=>{p.current&&await Wt({storage:et.current,key:st.current,widget:p.current})},[]),mt=(0,e.useCallback)(async c=>zt({storage:et.current,key:st.current}),[]),Tt=(0,e.useCallback)(async c=>{const{chartSave:L}=await xt({storage:et.current,save:c,key:st.current,widget:p.current});await t.initialConfig.onSave?.(L)},[t.initialConfig]),yt=(0,e.useCallback)(async()=>{const c=await mt(dt.current);if(t.initialConfig.onLoadConfig?.(),!c.chartSave)return c;const L=St(dt.current);return c.chartSave.layout===L?c:{...c,chartSave:{...c.chartSave,layout:L}}},[mt,t.initialConfig]),gt=(0,e.useCallback)(c=>{h(c),t.initialConfig.onLoading?.(c)},[h,t.initialConfig]),$=(0,e.useCallback)((c,L=!0)=>{L?c.setChartType(Et.current===\"time\"?2:Rt.current):Et.current===\"time\"&&c.setChartType(2)},[]),Ae=(0,e.useCallback)(c=>{p.current=c,H(c)},[H]),Dt=(0,e.useCallback)(()=>{setTimeout(()=>{if(p.current)try{const c=p.current?.activeChart();$(c)}catch{}},0),M(!0),t.initialConfig.onChartReadyDone?.()},[$,t.initialConfig]),Mt=(0,e.useCallback)(()=>{nt(),t.initialConfig.onSetThemeDone?.()},[nt,t.initialConfig]),vt=(0,e.useCallback)(()=>{nt(),t.initialConfig.onFirstTimeSetThemeDone?.()},[nt,t.initialConfig]),pt=(0,e.useCallback)(c=>{t.initialConfig.onIntervalChanged?.(c)},[t.initialConfig]),Ct=(0,e.useCallback)(async c=>{const{chart:L}=c;if(!L.symbolExt())return;const k=L.chartType();C(k),Ot({resolution:L.resolution(),chartType:k}),await At(),t.initialConfig.onActiveChartChanged?.(c)},[At,Ot,C,t.initialConfig]),Bt=(0,e.useCallback)(c=>{window.open(`https://www.tradingview.com/x/${c}`,\"_blank\")},[]),Ut=(0,e.useCallback)(c=>{t.initialConfig.onAddStudy?.(c)},[t.initialConfig]),Kt=(0,e.useCallback)(c=>{t.initialConfig.onSymbolChanged?.(c)},[t.initialConfig]),Gt=(0,e.useCallback)(c=>{t.initialConfig.onFirstTimeDataLoadedDone?.(c)},[t.initialConfig]),bt=(0,e.useCallback)(c=>{t.initialConfig.onDataLoadedDone?.(c)},[t.initialConfig]),Pt=(0,e.useCallback)(({chart:c,tradingViewSave:L})=>{if(L)return;[{length:7,color:\"rgba(241, 156, 56, 0.7)\"},{length:25,color:\"rgba(234, 61, 247, 0.7)\"},{length:99,color:\"rgba(116, 252, 253, 0.7)\"}].forEach(({length:k,color:It})=>{c.createStudy(\"Moving Average\",!1,!1,{length:k},{\"plot.linewidth\":1,\"plot.color\":It,showLabelsOnPriceScale:!1})})},[]),Yt=(0,e.useCallback)(c=>{(t.initialConfig.onInitChart||Pt)(c);const F=c.chart,k=F.resolution(),It=(0,S.Cz)(k);Lt.current.includes(It)||F.setResolution(\"1D\")},[Pt,t.initialConfig.onInitChart]),Ft=(0,e.useCallback)(()=>{setTimeout(()=>{if(p.current)try{const c=p.current.activeChartIndex(),L=p.current.activeChart(),F=L.chartType();Ct({chart:L,index:c}),C(F),p.current.save(k=>{Tt(k)})}catch{}},100)},[]);(0,e.useEffect)(()=>{if(!(!D||!p.current))try{p.current.activeChart().setChartType(R)}catch{}},[R,D]),(0,e.useEffect)(()=>{if(!(!D||!p.current))try{$(p.current.activeChart())}catch{}},[D,N,$]),(0,e.useEffect)(()=>{if(!(!D||!p.current))try{p.current.setLayout(St(Y))}catch{}},[Y,D]),(0,e.useEffect)(function(){if(D&&p.current)try{const L=p.current.activeChart();L.onChartTypeChanged().subscribe({},()=>{$(L,!1)})}catch{}},[$,D]);const{onTradingViewInitError:Ht,onChartReadyError:Vt}=Xt({storage:z,storageName:s});(0,e.useEffect)(()=>{if(!D)return;const c=l?\"none\":\"auto\";try{const F=document.getElementById(tt)?.querySelector(\"iframe\");if(!F)return;F.style.pointerEvents=c}catch{}},[D,tt,l]);const me=(0,e.useMemo)(()=>({...t.initialConfig,tvConfig:{...t.initialConfig.tvConfig,container:tt,interval:m,locale:Nt},onSave:Tt,onLoadConfig:yt,onLoading:gt,onChartReadyDone:Dt,onChartReadyError:Vt,onTradingViewInitError:Ht,onIntervalChanged:pt,onFirstTimeSetThemeDone:vt,onSetThemeDone:Mt,onFirstTimeDataLoadedDone:Gt,onDataLoadedDone:bt,onInitChart:Yt,onActiveChartChanged:Ct,onSymbolChanged:Kt,onAddStudy:Ut,onScreenshotReady:Bt,onLayoutChanged:Ft}),[tt,m,Nt,Ct,Ut,Dt,Vt,bt,Gt,vt,Yt,pt,Ft,yt,gt,Tt,Bt,Mt,Kt,Ht,t.initialConfig]);return(0,A.jsx)(\"div\",{className:\"h-full\",style:{display:y!==o.tU.TradingView?\"none\":\"block\"},children:(0,A.jsx)(e.Suspense,{fallback:null,children:(0,A.jsx)(Ce,{...t,interval:m,limitOrders:Se,positionOrders:Ne,executionOrders:Re,labelLines:Le,overwritingThemeOnLoaded:Oe,themeConfig:fe,initialConfig:me,onInitialized:Ae})})})}}"; + +/** Preserve native initialization, event subscriptions and storage except snapshot ownership. */ +export function createChartDrawingSaveFactory(__scopeChartDrawingSnapshot = scopeChartDrawingSnapshot) { + return {76535(w,B,r){r.d(B,{A:()=>_e,X:()=>Te});var A=r(31085),e=r(41594),f=r(43917),g=r.n(f),G=r(19020),T=r(13067);const b=null,v="myTradingView";var K=r(78441),U=r(53837),o=r(4260),S=r(80817);const P=t=>{switch(t){case"zh-CN":case"cn":return"zh";case"zh-HK":case"zh-TW":case"zh-TC":case"tw":return"zh_TW";case"ja":return"ja";case"ko":return"ko";case"th":return"th";case"he":return"he_IL";case"ru":return"ru";case"cs":return"cs";case"sv":return"sv";case"vi":case"vn":return"vi";case"tr":return"tr";case"ro":return"ro";case"pt-PT":case"pt-BR":case"pt":return"pt";case"pl":return"pl";case"nl":return"nl_NL";case"it":return"it";case"fr":return"fr";case"es":case"es-LA":return"es";case"de":return"de";case"id":return"id_ID";default:return"en"}};var x=r(46108),ot=r.n(x),Z=r(75857),rt=r.n(Z),at=r(87809),j=r.n(at),X=r(34789),Q=r.n(X),ct=r(19953),it=r.n(ct),lt=r(82438),ut=r.n(lt),a=r(87835),E=r.n(a),O=r(75257);const W=t=>/^(LineTool)/g.test(t),_t=t=>`${t}.customSettings`,ft=t=>`${t}.layout`,J=t=>t.chartId,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}},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},xt=async({storage:t,save:n,key:s,widget:i})=>{if(!i)return{chartSave:n,drawingsSave:{}};const{drawings:l,charts:u}=kt({save:__scopeChartDrawingSnapshot(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}},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},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}},$t=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell;return{...n,direction:n.isBuy?"buy":"sell",arrowStyles:{height:8,spacing:1,color:i},textStyles:{color:s.PrimaryText}}},[t])},jt=[],Zt=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.yC),[l]=s(o._b),u=$t(),d=(0,e.useMemo)(()=>n.map(u),[u,n]);return(0,e.useMemo)(()=>i&&l===o.Ev.Single?d:jt,[i,l,d])};var ht=r(92873);const Xt=({storage:t,storageName:n})=>{const{getI18n:s}=(0,ht.o)("","trade-ui"),i=(0,e.useMemo)(()=>s("trd-chart-tv-initialization-error",{defaultValue:"Something went wrong while initializing TradingView."})||"",[s]),l=(0,e.useMemo)(()=>s("trd-chart-crash-reload",{defaultValue:"Refresh"})||"",[s]),u=(0,e.useMemo)(()=>s("trd-chart-init-error",{defaultValue:"Failed to initialize TradingView due to malformed config, please try to refresh"})||"",[s]),d=(0,e.useCallback)(()=>window.location.reload(),[]),I=(0,e.useCallback)(async()=>{await t.removeItem(n),d()},[d,t,n]),[N,_]=(0,e.useState)(!1),[m,y]=(0,e.useState)(""),[R,C]=(0,e.useState)(""),[Y,h]=(0,e.useState)(async()=>{}),H=(0,e.useCallback)(()=>{_(!0),y(u),C(l),h(I)},[I,l,u]),V=(0,e.useCallback)(()=>{_(!0),y(i),C(l),h(d)},[i,l,d]);return{showError:N,mainText:m,buttonText:R,buttonCallback:Y,onTradingViewInitError:H,onChartReadyError:V}},Qt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell;return{...n,lineStyles:{color:i,length:45,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.PrimaryText,borderColor:i,backgroundColor:i},cancelButtonStyles:{color:s.IconNormal}}},[t])},Jt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.Buy,length:45,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.TextBuy,borderColor:s.Buy,backgroundColor:s.DepthBuyBg},cancelButtonStyles:{color:s.IconNormal}}},[t])},qt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.EmphasizeText,length:100,unit:"percentage",style:0,extended:!1},bodyStyles:{color:s.RedGreenBgText,borderColor:s.EmphasizeText,backgroundColor:s.EmphasizeText},cancelButtonStyles:{color:s.IconNormal}}},[t])},te=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.DisableText,length:100,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.PrimaryText,borderColor:s.InputLine,backgroundColor:s.InputLine},cancelButtonStyles:{color:s.IconNormal,borderColor:s.InputLine,backgroundColor:s.BasicBg}}},[t])},ee=[],se=({namespace:t,labelLines:n})=>{const s=(0,o.y$)(t),[i]=s(o.O),[l]=s(o.S5),[u]=s(o.ks),[d]=s(o.yA),[I]=s(o.pT),[N]=s(o._b),_=Qt(),m=Jt(),y=qt(),R=te(),C=(0,e.useMemo)(()=>i?n.filter(M=>M.type===o.ND.AverageBuyPrice).map(M=>_({...M,isBuy:!0})):[],[n,i,_]),Y=(0,e.useMemo)(()=>l?n.filter(M=>M.type===o.ND.AverageSellPrice).map(M=>_({...M,isBuy:!1})):[],[n,l,_]),h=(0,e.useMemo)(()=>u?n.filter(M=>M.type===o.ND.BreakEvenPrice).map(m):[],[n,u,m]),H=(0,e.useMemo)(()=>d?n.filter(M=>M.type===o.ND.LiquidationPrice).map(y):[],[n,d,y]),V=(0,e.useMemo)(()=>I?n.filter(M=>M.type===o.ND.PriceAlert).map(R):[],[n,I,R]),D=(0,e.useMemo)(()=>[...C,...Y,...h,...H,...V],[C,Y,h,H,V]);return(0,e.useMemo)(()=>N===o.Ev.Single?D:ee,[D,N])},ne=()=>{const{getI18n:t}=(0,ht.o)("","kline-ui");return(0,e.useMemo)(()=>({openOrder:{dragTooltip:t("order-drag-tooltip",{defaultValue:"Drag for price modify"}),modifyTooltip:t("order-modify-tooltip",{defaultValue:"Click for modify"}),cancelTooltip:t("order-cancel-tooltip",{defaultValue:"Click for cancel"})}}),[t])},oe=({position:t,extended:n})=>{switch(t){case"center":return{length:35,unit:"percentage",extended:n};case"right":return{length:24,unit:"pixel",extended:n};case"left":return{length:-24,unit:"pixel",extended:n};case"left-most":return{length:100,unit:"percentage",extended:n};default:return{length:65,unit:"percentage",extended:n}}},re=()=>{const t=ne(),n=(0,T.w)();return(0,e.useCallback)((s,i)=>{const{colors:l}=n,u=s.isBuy?l.Buy:l.Sell;return{...s,tooltip:s.tooltip??t.openOrder.dragTooltip,modifyTooltip:s.modifyTooltip??t.openOrder.modifyTooltip,cancelTooltip:s.cancelTooltip??t.openOrder.cancelTooltip,lineStyles:{...oe(i),color:u,style:2,extended:!1},bodyStyles:{borderColor:l.CardBg,backgroundColor:u,color:l.RedGreenBgText},quantityStyles:{borderColor:u,backgroundColor:l.CardBg,color:u},cancelButtonStyles:{borderColor:u,backgroundColor:l.CardBg,color:u}}},[t.openOrder.dragTooltip,t.openOrder.modifyTooltip,t.openOrder.cancelTooltip,n])},ae=[],ce=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.Nh),[l]=s(o.R3),[u]=s(o.Ec),[d]=s(o._b),[I]=s(o.CV),[N]=s(o.bv),_=re(),m=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>(C===void 0||C===o.aA.LimitOrder)&&i).map(C=>_(C,{position:I,extended:N})),[n,i,_,I,N]),y=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>C===o.aA.BotPreview&&l).map(C=>_(C,{position:"legacy",extended:!1})),[n,l,_]),R=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>C===o.aA.OrderPreview&&u).map(C=>_(C,{position:"left-most",extended:!1})),[n,u,_]);return(0,e.useMemo)(()=>d===o.Ev.Single?[...m,...y,...R]:ae,[m,y,R,d])},ie=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell,l=n.PNL>0?s.Buy:n.PNL<0?s.Sell:i;return{...n,lineStyles:{color:l,length:100,unit:"percentage",style:2,extended:!1},bodyStyles:{borderColor:l,backgroundColor:l,color:s.RedGreenBgText},quantityStyles:{borderColor:i,backgroundColor:s.CardBg,color:i},reverseButtonStyles:{borderColor:i,backgroundColor:s.CardBg,color:i},closeButtonStyles:{borderColor:i,backgroundColor:s.CardBg,color:i}}},[t])},le=[],ue=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.FH),[l]=s(o._b),u=ie(),d=(0,e.useMemo)(()=>n.map(u),[u,n]);return(0,e.useMemo)(()=>i&&l===o.Ev.Single?[...d]:le,[i,l,d])};var Ee=r(6868);const de=({namespace:t})=>{const n=(0,o.y$)(t),[s,i]=n(o.mn),l=(0,Ee.r)(),u=l(_=>_.hasHydrated),d=l(_=>_.lastUpdatedTimestamp),I=(0,e.useMemo)(()=>u&&d!==s,[u,d,s]),N=(0,e.useCallback)(()=>{i(d)},[d,i]);return(0,e.useMemo)(()=>({overwritingThemeOnLoaded:I,syncThemeTimestamp:N}),[I,N])},Te=()=>r.e("66202").then(r.bind(r,78273)),Ce=(0,e.lazy)(()=>r.e("66202").then(r.bind(r,78273))),Ie=t=>(0,e.useMemo)(()=>g().createInstance({name:t}),[t]),St=t=>{switch(t){case o.Ev.HorizontalTwo:return"2v";case o.Ev.VerticalTwo:return"2h";case o.Ev.HorizontalThree:return"3v";case o.Ev.VerticalThree:return"3h";case o.Ev.LeftOneRightTwo:return"3s";case o.Ev.TopOneBottomTwo:return"1-2";case o.Ev.Four:return"4";case o.Ev.VerticalFour:return"4h";case o.Ev.LeftOneRightThree:return"4s";case o.Ev.TopOneBottomThree:return"1-3";case o.Ev.Five:return"2-3";case o.Ev.SixTwoByThree:return"6";case o.Ev.SixThreeByTwo:return"6c";case o.Ev.Eight:return"8";default:return"s"}},_e=({tradingViewProps:t,namespace:n,storageName:s=v,containerId:i="tradingview",disablePointerEvents:l=!1})=>{const u=(0,o.y$)(n),d=(0,U.y$)(n),I=(0,K.y)(n),[N,_]=u(o.A8),m=u(o.WK),[y]=u(o.e6),[R,C]=u(o.iw),[Y]=u(o._b),[,h]=I(K.N),[,H]=d(U.yj),[V]=d(U.Cf),[D,M]=(0,e.useState)(!1),z=Ie(n),q=(0,G.Bl)(),Nt=(0,e.useMemo)(()=>P(q),[q]),fe=(0,T.w)(),tt=(0,e.useMemo)(()=>`${n}-${i}`,[i,n]),p=(0,e.useRef)(),Et=(0,e.useRef)(N),Rt=(0,e.useRef)(R),dt=(0,e.useRef)(Y),et=(0,e.useRef)(z),st=(0,e.useRef)(s),Lt=(0,e.useRef)(V),he=(0,e.useRef)(D);(0,e.useEffect)(()=>{Et.current=N},[N]),(0,e.useEffect)(()=>{Rt.current=R},[R]),(0,e.useEffect)(()=>{dt.current=Y},[Y]),(0,e.useEffect)(()=>{et.current=z},[z]),(0,e.useEffect)(()=>{st.current=s},[s]),(0,e.useEffect)(()=>{he.current=D},[D]),(0,e.useEffect)(()=>{Lt.current=V},[V]);const Se=ce({namespace:n,orders:t.limitOrders}),Ne=ue({namespace:n,orders:t.positionOrders}),Re=Zt({namespace:n,orders:t.executionOrders}),Le=se({namespace:n,labelLines:t.labelLines}),{overwritingThemeOnLoaded:Oe,syncThemeTimestamp:nt}=de({namespace:n}),Ot=(0,e.useCallback)(({resolution:c,chartType:L})=>{const F=(0,S.Cz)(c);_(L===3?"time":F)},[_]),At=(0,e.useCallback)(async()=>{p.current&&await Wt({storage:et.current,key:st.current,widget:p.current})},[]),mt=(0,e.useCallback)(async c=>zt({storage:et.current,key:st.current}),[]),Tt=(0,e.useCallback)(async c=>{const{chartSave:L}=await xt({storage:et.current,save:c,key:st.current,widget:p.current});await t.initialConfig.onSave?.(L)},[t.initialConfig]),yt=(0,e.useCallback)(async()=>{const c=await mt(dt.current);if(t.initialConfig.onLoadConfig?.(),!c.chartSave)return c;const L=St(dt.current);return c.chartSave.layout===L?c:{...c,chartSave:{...c.chartSave,layout:L}}},[mt,t.initialConfig]),gt=(0,e.useCallback)(c=>{h(c),t.initialConfig.onLoading?.(c)},[h,t.initialConfig]),$=(0,e.useCallback)((c,L=!0)=>{L?c.setChartType(Et.current==="time"?2:Rt.current):Et.current==="time"&&c.setChartType(2)},[]),Ae=(0,e.useCallback)(c=>{p.current=c,H(c)},[H]),Dt=(0,e.useCallback)(()=>{setTimeout(()=>{if(p.current)try{const c=p.current?.activeChart();$(c)}catch{}},0),M(!0),t.initialConfig.onChartReadyDone?.()},[$,t.initialConfig]),Mt=(0,e.useCallback)(()=>{nt(),t.initialConfig.onSetThemeDone?.()},[nt,t.initialConfig]),vt=(0,e.useCallback)(()=>{nt(),t.initialConfig.onFirstTimeSetThemeDone?.()},[nt,t.initialConfig]),pt=(0,e.useCallback)(c=>{t.initialConfig.onIntervalChanged?.(c)},[t.initialConfig]),Ct=(0,e.useCallback)(async c=>{const{chart:L}=c;if(!L.symbolExt())return;const k=L.chartType();C(k),Ot({resolution:L.resolution(),chartType:k}),await At(),t.initialConfig.onActiveChartChanged?.(c)},[At,Ot,C,t.initialConfig]),Bt=(0,e.useCallback)(c=>{window.open(`https://www.tradingview.com/x/${c}`,"_blank")},[]),Ut=(0,e.useCallback)(c=>{t.initialConfig.onAddStudy?.(c)},[t.initialConfig]),Kt=(0,e.useCallback)(c=>{t.initialConfig.onSymbolChanged?.(c)},[t.initialConfig]),Gt=(0,e.useCallback)(c=>{t.initialConfig.onFirstTimeDataLoadedDone?.(c)},[t.initialConfig]),bt=(0,e.useCallback)(c=>{t.initialConfig.onDataLoadedDone?.(c)},[t.initialConfig]),Pt=(0,e.useCallback)(({chart:c,tradingViewSave:L})=>{if(L)return;[{length:7,color:"rgba(241, 156, 56, 0.7)"},{length:25,color:"rgba(234, 61, 247, 0.7)"},{length:99,color:"rgba(116, 252, 253, 0.7)"}].forEach(({length:k,color:It})=>{c.createStudy("Moving Average",!1,!1,{length:k},{"plot.linewidth":1,"plot.color":It,showLabelsOnPriceScale:!1})})},[]),Yt=(0,e.useCallback)(c=>{(t.initialConfig.onInitChart||Pt)(c);const F=c.chart,k=F.resolution(),It=(0,S.Cz)(k);Lt.current.includes(It)||F.setResolution("1D")},[Pt,t.initialConfig.onInitChart]),Ft=(0,e.useCallback)(()=>{setTimeout(()=>{if(p.current)try{const c=p.current.activeChartIndex(),L=p.current.activeChart(),F=L.chartType();Ct({chart:L,index:c}),C(F),p.current.save(k=>{Tt(k)})}catch{}},100)},[]);(0,e.useEffect)(()=>{if(!(!D||!p.current))try{p.current.activeChart().setChartType(R)}catch{}},[R,D]),(0,e.useEffect)(()=>{if(!(!D||!p.current))try{$(p.current.activeChart())}catch{}},[D,N,$]),(0,e.useEffect)(()=>{if(!(!D||!p.current))try{p.current.setLayout(St(Y))}catch{}},[Y,D]),(0,e.useEffect)(function(){if(D&&p.current)try{const L=p.current.activeChart();L.onChartTypeChanged().subscribe({},()=>{$(L,!1)})}catch{}},[$,D]);const{onTradingViewInitError:Ht,onChartReadyError:Vt}=Xt({storage:z,storageName:s});(0,e.useEffect)(()=>{if(!D)return;const c=l?"none":"auto";try{const F=document.getElementById(tt)?.querySelector("iframe");if(!F)return;F.style.pointerEvents=c}catch{}},[D,tt,l]);const me=(0,e.useMemo)(()=>({...t.initialConfig,tvConfig:{...t.initialConfig.tvConfig,container:tt,interval:m,locale:Nt},onSave:Tt,onLoadConfig:yt,onLoading:gt,onChartReadyDone:Dt,onChartReadyError:Vt,onTradingViewInitError:Ht,onIntervalChanged:pt,onFirstTimeSetThemeDone:vt,onSetThemeDone:Mt,onFirstTimeDataLoadedDone:Gt,onDataLoadedDone:bt,onInitChart:Yt,onActiveChartChanged:Ct,onSymbolChanged:Kt,onAddStudy:Ut,onScreenshotReady:Bt,onLayoutChanged:Ft}),[tt,m,Nt,Ct,Ut,Dt,Vt,bt,Gt,vt,Yt,pt,Ft,yt,gt,Tt,Bt,Mt,Kt,Ht,t.initialConfig]);return(0,A.jsx)("div",{className:"h-full",style:{display:y!==o.tU.TradingView?"none":"block"},children:(0,A.jsx)(e.Suspense,{fallback:null,children:(0,A.jsx)(Ce,{...t,interval:m,limitOrders:Se,positionOrders:Ne,executionOrders:Re,labelLines:Le,overwritingThemeOnLoaded:Oe,themeConfig:fe,initialConfig:me,onInitialized:Ae})})})}}}[76535]; +} + +/** Unknown host source must remain native rather than accepting an approximate patch. */ +export function replaceChartDrawingSaveFactory(originalFactory) { + if (typeof originalFactory !== 'function' || Function.prototype.toString.call(originalFactory) !== originalFactorySource) { + throw new Error('Drawing save factory source does not match the pinned public module'); + } + return createChartDrawingSaveFactory(); +} diff --git a/src/binance-orderbook-trade/chart-storage/drawing-save-scope.js b/src/binance-orderbook-trade/chart-storage/drawing-save-scope.js new file mode 100644 index 0000000..f6bfecf --- /dev/null +++ b/src/binance-orderbook-trade/chart-storage/drawing-save-scope.js @@ -0,0 +1,28 @@ +/** + * Native loading retains other symbols' drawings in every chart. Saving those + * hidden copies lets another symbol's tab overwrite a newer edit or deletion. + * Filter within each chart before the host deduplicates drawing IDs globally; + * indicator panes inherit their chart's symbol rather than requiring a local + * MainSeries. The native extractor still writes explicit empty active groups. + */ +export function scopeChartDrawingSnapshot(save) { + return { + ...save, + charts: save.charts.map(chart => { + const sources = chart.panes.flatMap(pane => pane.sources); + const symbols = new Set(sources.filter(source => source.type === 'MainSeries') + .map(source => source.state.symbol.toUpperCase())); + if (symbols.size === 0 && sources.some(source => source.type.startsWith('LineTool'))) { + throw new Error('Drawing save requires a MainSeries symbol for its chart'); + } + return { + ...chart, + panes: chart.panes.map(pane => ({ + ...pane, + sources: pane.sources.filter(source => !source.type.startsWith('LineTool') + || symbols.has(source.state.symbol.toUpperCase())), + })), + }; + }), + }; +} diff --git a/src/binance-orderbook-trade/chart-storage/runtime.js b/src/binance-orderbook-trade/chart-storage/runtime.js index 7e73ed9..29cdaaa 100644 --- a/src/binance-orderbook-trade/chart-storage/runtime.js +++ b/src/binance-orderbook-trade/chart-storage/runtime.js @@ -1,87 +1,113 @@ import { observeChartStorageBootstrap } from './bootstrap.js'; import { replaceChartMirrorFactory } from './mirror-module.js'; +import { replaceChartDrawingSaveFactory } from './drawing-save-module.js'; import { createChartMirrorWriter } from './mirror-writer.js'; import { isChartStoragePage } from './scope.js'; const CAPTURE_DEADLINE_MS = 30_000; +const MIRROR_MODULE = '70940'; +const DRAWING_MODULE = '76535'; /** - * 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. + * Mirror optimization is optional and drainable. Drawing ownership protection + * remains installed after stop and gets the same finite startup capture window. */ 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 }; + const drawingScope = { status: 'waiting', reason: null, attempts: 0, matches: 0, executions: 0 }; let observer; let timer; let stopping; + let observationFinished = false; function snapshot() { const stats = writer.getStats(); return { ...state, status: stats.failedBatches > 0 ? 'native_after_failure' : state.status, - phase: writer.getPhase(), writer: stats }; + phase: writer.getPhase(), writer: stats, drawingScope: { ...drawingScope } }; } 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(); }); + if (observer) observer.stopTarget(MIRROR_MODULE, reason); + if (observationFinished) self.removeEventListener('pagehide', onPageHide); return stopping; } - function onPageHide() { void stop('pagehide'); } + function finishObservation() { + observationFinished = true; + clearTimeout(timer); + if (stopping) self.removeEventListener('pagehide', onPageHide); + } + + function onPageHide() { + if (observer) observer.stop('pagehide'); + void stop('pagehide'); + } function dispatch(target, entries, nativeThunk) { if (!isChartStoragePage()) void stop('scope_changed'); return writer.dispatch(target, entries, nativeThunk); } + function replaceFactory(id, original, replace) { + if (!isChartStoragePage()) { + observer.stop('scope_changed'); + void stop('scope_changed'); + return original; + } + const targetState = id === MIRROR_MODULE ? state : drawingScope; + targetState.attempts += 1; + const replacement = replace(original); + targetState.matches += 1; + return function (...args) { + targetState.executions += 1; + return Reflect.apply(replacement, this, args); + }; + } + 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); - }; + targets: { + [MIRROR_MODULE]: original => replaceFactory(MIRROR_MODULE, original, factory => replaceChartMirrorFactory(factory, dispatch)), + [DRAWING_MODULE]: original => replaceFactory(DRAWING_MODULE, original, replaceChartDrawingSaveFactory), }, - onCapture() { - clearTimeout(timer); - state.status = 'active'; + onCapture(id) { + const targetState = id === MIRROR_MODULE ? state : drawingScope; + if (id !== MIRROR_MODULE || !stopping) targetState.status = 'active'; }, + onFailure(id, reason) { + if (id === MIRROR_MODULE) { + void stop(reason === 'execution_failed' ? 'capture_failed' : reason); + } else { + drawingScope.status = reason === 'source_mismatch' ? 'source_mismatch' : 'unavailable'; + drawingScope.reason = reason; + } + }, + onComplete() { finishObservation(); }, }); } catch { - // A late extension cannot safely replace the runtime's cached module. + // A late extension cannot safely replace the runtime's cached modules. + drawingScope.status = 'unavailable'; + drawingScope.reason = 'bootstrap_unavailable'; + finishObservation(); 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); + if (!observationFinished) { + self.addEventListener('pagehide', onPageHide, { once: true }); + timer = setTimeout(() => { + observer.stop('capture_deadline'); + if (state.status === 'waiting') void stop('capture_deadline'); + }, CAPTURE_DEADLINE_MS); + } return Object.freeze({ snapshot, stop }); } diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 74ca1ea..3d5c982 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.218 +// @version 2.7.219 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* diff --git a/test/fixtures/binance-chart-storage/drawing-save-manifest.json b/test/fixtures/binance-chart-storage/drawing-save-manifest.json new file mode 100644 index 0000000..cb36283 --- /dev/null +++ b/test/fixtures/binance-chart-storage/drawing-save-manifest.json @@ -0,0 +1,26 @@ +{ + "source": { + "url": "https://bin.bnbstatic.com/static/chunks/36648.dd8ae7dc.js", + "sha256": "8191b50fb47212425d03146834207645c0e89b8ce623e23a71477194ee9a60ff" + }, + "originalFactory": { + "range": [ + 104, + 15009 + ], + "sha256": "a6371afcfc264f610500bcc427d286cacbdc645b859be14d0a6a7fcf71b84c7a" + }, + "replacement": { + "range": [ + 1851, + 1863 + ], + "original": "kt({save:n})", + "inserted": "kt({save:__scopeChartDrawingSnapshot(n)})" + }, + "outputs": { + "module": "65395422f4d6c80e8882c8891012eccacc3e1bebdaf3d34b7dec4f746b47f43c", + "fixture": "3b5dc2dc2d98775452bde3a9f39964d948473699ff1f7cec8692a979c69ccf9a" + }, + "scope": "Only xt calls kt with a snapshot restricted to each chart's current MainSeries symbols. Other native methods and events are unchanged." +} diff --git a/test/fixtures/binance-chart-storage/drawing-save-scoped.js b/test/fixtures/binance-chart-storage/drawing-save-scoped.js new file mode 100644 index 0000000..f212f03 --- /dev/null +++ b/test/fixtures/binance-chart-storage/drawing-save-scoped.js @@ -0,0 +1,32 @@ +/* Exact native dependencies and save callback, with the same sole production replacement. */ +export function createNativeDrawingSaveFactory() { return {76535(w,B,r){r.d(B,{A:()=>_e,X:()=>Te});var A=r(31085),e=r(41594),f=r(43917),g=r.n(f),G=r(19020),T=r(13067);const b=null,v="myTradingView";var K=r(78441),U=r(53837),o=r(4260),S=r(80817);const P=t=>{switch(t){case"zh-CN":case"cn":return"zh";case"zh-HK":case"zh-TW":case"zh-TC":case"tw":return"zh_TW";case"ja":return"ja";case"ko":return"ko";case"th":return"th";case"he":return"he_IL";case"ru":return"ru";case"cs":return"cs";case"sv":return"sv";case"vi":case"vn":return"vi";case"tr":return"tr";case"ro":return"ro";case"pt-PT":case"pt-BR":case"pt":return"pt";case"pl":return"pl";case"nl":return"nl_NL";case"it":return"it";case"fr":return"fr";case"es":case"es-LA":return"es";case"de":return"de";case"id":return"id_ID";default:return"en"}};var x=r(46108),ot=r.n(x),Z=r(75857),rt=r.n(Z),at=r(87809),j=r.n(at),X=r(34789),Q=r.n(X),ct=r(19953),it=r.n(ct),lt=r(82438),ut=r.n(lt),a=r(87835),E=r.n(a),O=r(75257);const W=t=>/^(LineTool)/g.test(t),_t=t=>`${t}.customSettings`,ft=t=>`${t}.layout`,J=t=>t.chartId,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}},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},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}},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},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}},$t=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell;return{...n,direction:n.isBuy?"buy":"sell",arrowStyles:{height:8,spacing:1,color:i},textStyles:{color:s.PrimaryText}}},[t])},jt=[],Zt=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.yC),[l]=s(o._b),u=$t(),d=(0,e.useMemo)(()=>n.map(u),[u,n]);return(0,e.useMemo)(()=>i&&l===o.Ev.Single?d:jt,[i,l,d])};var ht=r(92873);const Xt=({storage:t,storageName:n})=>{const{getI18n:s}=(0,ht.o)("","trade-ui"),i=(0,e.useMemo)(()=>s("trd-chart-tv-initialization-error",{defaultValue:"Something went wrong while initializing TradingView."})||"",[s]),l=(0,e.useMemo)(()=>s("trd-chart-crash-reload",{defaultValue:"Refresh"})||"",[s]),u=(0,e.useMemo)(()=>s("trd-chart-init-error",{defaultValue:"Failed to initialize TradingView due to malformed config, please try to refresh"})||"",[s]),d=(0,e.useCallback)(()=>window.location.reload(),[]),I=(0,e.useCallback)(async()=>{await t.removeItem(n),d()},[d,t,n]),[N,_]=(0,e.useState)(!1),[m,y]=(0,e.useState)(""),[R,C]=(0,e.useState)(""),[Y,h]=(0,e.useState)(async()=>{}),H=(0,e.useCallback)(()=>{_(!0),y(u),C(l),h(I)},[I,l,u]),V=(0,e.useCallback)(()=>{_(!0),y(i),C(l),h(d)},[i,l,d]);return{showError:N,mainText:m,buttonText:R,buttonCallback:Y,onTradingViewInitError:H,onChartReadyError:V}},Qt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell;return{...n,lineStyles:{color:i,length:45,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.PrimaryText,borderColor:i,backgroundColor:i},cancelButtonStyles:{color:s.IconNormal}}},[t])},Jt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.Buy,length:45,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.TextBuy,borderColor:s.Buy,backgroundColor:s.DepthBuyBg},cancelButtonStyles:{color:s.IconNormal}}},[t])},qt=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.EmphasizeText,length:100,unit:"percentage",style:0,extended:!1},bodyStyles:{color:s.RedGreenBgText,borderColor:s.EmphasizeText,backgroundColor:s.EmphasizeText},cancelButtonStyles:{color:s.IconNormal}}},[t])},te=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t;return{...n,lineStyles:{color:s.DisableText,length:100,unit:"percentage",style:2,extended:!1},bodyStyles:{color:s.PrimaryText,borderColor:s.InputLine,backgroundColor:s.InputLine},cancelButtonStyles:{color:s.IconNormal,borderColor:s.InputLine,backgroundColor:s.BasicBg}}},[t])},ee=[],se=({namespace:t,labelLines:n})=>{const s=(0,o.y$)(t),[i]=s(o.O),[l]=s(o.S5),[u]=s(o.ks),[d]=s(o.yA),[I]=s(o.pT),[N]=s(o._b),_=Qt(),m=Jt(),y=qt(),R=te(),C=(0,e.useMemo)(()=>i?n.filter(M=>M.type===o.ND.AverageBuyPrice).map(M=>_({...M,isBuy:!0})):[],[n,i,_]),Y=(0,e.useMemo)(()=>l?n.filter(M=>M.type===o.ND.AverageSellPrice).map(M=>_({...M,isBuy:!1})):[],[n,l,_]),h=(0,e.useMemo)(()=>u?n.filter(M=>M.type===o.ND.BreakEvenPrice).map(m):[],[n,u,m]),H=(0,e.useMemo)(()=>d?n.filter(M=>M.type===o.ND.LiquidationPrice).map(y):[],[n,d,y]),V=(0,e.useMemo)(()=>I?n.filter(M=>M.type===o.ND.PriceAlert).map(R):[],[n,I,R]),D=(0,e.useMemo)(()=>[...C,...Y,...h,...H,...V],[C,Y,h,H,V]);return(0,e.useMemo)(()=>N===o.Ev.Single?D:ee,[D,N])},ne=()=>{const{getI18n:t}=(0,ht.o)("","kline-ui");return(0,e.useMemo)(()=>({openOrder:{dragTooltip:t("order-drag-tooltip",{defaultValue:"Drag for price modify"}),modifyTooltip:t("order-modify-tooltip",{defaultValue:"Click for modify"}),cancelTooltip:t("order-cancel-tooltip",{defaultValue:"Click for cancel"})}}),[t])},oe=({position:t,extended:n})=>{switch(t){case"center":return{length:35,unit:"percentage",extended:n};case"right":return{length:24,unit:"pixel",extended:n};case"left":return{length:-24,unit:"pixel",extended:n};case"left-most":return{length:100,unit:"percentage",extended:n};default:return{length:65,unit:"percentage",extended:n}}},re=()=>{const t=ne(),n=(0,T.w)();return(0,e.useCallback)((s,i)=>{const{colors:l}=n,u=s.isBuy?l.Buy:l.Sell;return{...s,tooltip:s.tooltip??t.openOrder.dragTooltip,modifyTooltip:s.modifyTooltip??t.openOrder.modifyTooltip,cancelTooltip:s.cancelTooltip??t.openOrder.cancelTooltip,lineStyles:{...oe(i),color:u,style:2,extended:!1},bodyStyles:{borderColor:l.CardBg,backgroundColor:u,color:l.RedGreenBgText},quantityStyles:{borderColor:u,backgroundColor:l.CardBg,color:u},cancelButtonStyles:{borderColor:u,backgroundColor:l.CardBg,color:u}}},[t.openOrder.dragTooltip,t.openOrder.modifyTooltip,t.openOrder.cancelTooltip,n])},ae=[],ce=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.Nh),[l]=s(o.R3),[u]=s(o.Ec),[d]=s(o._b),[I]=s(o.CV),[N]=s(o.bv),_=re(),m=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>(C===void 0||C===o.aA.LimitOrder)&&i).map(C=>_(C,{position:I,extended:N})),[n,i,_,I,N]),y=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>C===o.aA.BotPreview&&l).map(C=>_(C,{position:"legacy",extended:!1})),[n,l,_]),R=(0,e.useMemo)(()=>n.filter(({limitOrderType:C})=>C===o.aA.OrderPreview&&u).map(C=>_(C,{position:"left-most",extended:!1})),[n,u,_]);return(0,e.useMemo)(()=>d===o.Ev.Single?[...m,...y,...R]:ae,[m,y,R,d])},ie=()=>{const t=(0,T.w)();return(0,e.useCallback)(n=>{const{colors:s}=t,i=n.isBuy?s.Buy:s.Sell,l=n.PNL>0?s.Buy:n.PNL<0?s.Sell:i;return{...n,lineStyles:{color:l,length:100,unit:"percentage",style:2,extended:!1},bodyStyles:{borderColor:l,backgroundColor:l,color:s.RedGreenBgText},quantityStyles:{borderColor:i,backgroundColor:s.CardBg,color:i},reverseButtonStyles:{borderColor:i,backgroundColor:s.CardBg,color:i},closeButtonStyles:{borderColor:i,backgroundColor:s.CardBg,color:i}}},[t])},le=[],ue=({namespace:t,orders:n})=>{const s=(0,o.y$)(t),[i]=s(o.FH),[l]=s(o._b),u=ie(),d=(0,e.useMemo)(()=>n.map(u),[u,n]);return(0,e.useMemo)(()=>i&&l===o.Ev.Single?[...d]:le,[i,l,d])};var Ee=r(6868);const de=({namespace:t})=>{const n=(0,o.y$)(t),[s,i]=n(o.mn),l=(0,Ee.r)(),u=l(_=>_.hasHydrated),d=l(_=>_.lastUpdatedTimestamp),I=(0,e.useMemo)(()=>u&&d!==s,[u,d,s]),N=(0,e.useCallback)(()=>{i(d)},[d,i]);return(0,e.useMemo)(()=>({overwritingThemeOnLoaded:I,syncThemeTimestamp:N}),[I,N])},Te=()=>r.e("66202").then(r.bind(r,78273)),Ce=(0,e.lazy)(()=>r.e("66202").then(r.bind(r,78273))),Ie=t=>(0,e.useMemo)(()=>g().createInstance({name:t}),[t]),St=t=>{switch(t){case o.Ev.HorizontalTwo:return"2v";case o.Ev.VerticalTwo:return"2h";case o.Ev.HorizontalThree:return"3v";case o.Ev.VerticalThree:return"3h";case o.Ev.LeftOneRightTwo:return"3s";case o.Ev.TopOneBottomTwo:return"1-2";case o.Ev.Four:return"4";case o.Ev.VerticalFour:return"4h";case o.Ev.LeftOneRightThree:return"4s";case o.Ev.TopOneBottomThree:return"1-3";case o.Ev.Five:return"2-3";case o.Ev.SixTwoByThree:return"6";case o.Ev.SixThreeByTwo:return"6c";case o.Ev.Eight:return"8";default:return"s"}},_e=({tradingViewProps:t,namespace:n,storageName:s=v,containerId:i="tradingview",disablePointerEvents:l=!1})=>{const u=(0,o.y$)(n),d=(0,U.y$)(n),I=(0,K.y)(n),[N,_]=u(o.A8),m=u(o.WK),[y]=u(o.e6),[R,C]=u(o.iw),[Y]=u(o._b),[,h]=I(K.N),[,H]=d(U.yj),[V]=d(U.Cf),[D,M]=(0,e.useState)(!1),z=Ie(n),q=(0,G.Bl)(),Nt=(0,e.useMemo)(()=>P(q),[q]),fe=(0,T.w)(),tt=(0,e.useMemo)(()=>`${n}-${i}`,[i,n]),p=(0,e.useRef)(),Et=(0,e.useRef)(N),Rt=(0,e.useRef)(R),dt=(0,e.useRef)(Y),et=(0,e.useRef)(z),st=(0,e.useRef)(s),Lt=(0,e.useRef)(V),he=(0,e.useRef)(D);(0,e.useEffect)(()=>{Et.current=N},[N]),(0,e.useEffect)(()=>{Rt.current=R},[R]),(0,e.useEffect)(()=>{dt.current=Y},[Y]),(0,e.useEffect)(()=>{et.current=z},[z]),(0,e.useEffect)(()=>{st.current=s},[s]),(0,e.useEffect)(()=>{he.current=D},[D]),(0,e.useEffect)(()=>{Lt.current=V},[V]);const Se=ce({namespace:n,orders:t.limitOrders}),Ne=ue({namespace:n,orders:t.positionOrders}),Re=Zt({namespace:n,orders:t.executionOrders}),Le=se({namespace:n,labelLines:t.labelLines}),{overwritingThemeOnLoaded:Oe,syncThemeTimestamp:nt}=de({namespace:n}),Ot=(0,e.useCallback)(({resolution:c,chartType:L})=>{const F=(0,S.Cz)(c);_(L===3?"time":F)},[_]),At=(0,e.useCallback)(async()=>{p.current&&await Wt({storage:et.current,key:st.current,widget:p.current})},[]),mt=(0,e.useCallback)(async c=>zt({storage:et.current,key:st.current}),[]),Tt=(0,e.useCallback)(async c=>{const{chartSave:L}=await xt({storage:et.current,save:c,key:st.current,widget:p.current});await t.initialConfig.onSave?.(L)},[t.initialConfig]),yt=(0,e.useCallback)(async()=>{const c=await mt(dt.current);if(t.initialConfig.onLoadConfig?.(),!c.chartSave)return c;const L=St(dt.current);return c.chartSave.layout===L?c:{...c,chartSave:{...c.chartSave,layout:L}}},[mt,t.initialConfig]),gt=(0,e.useCallback)(c=>{h(c),t.initialConfig.onLoading?.(c)},[h,t.initialConfig]),$=(0,e.useCallback)((c,L=!0)=>{L?c.setChartType(Et.current==="time"?2:Rt.current):Et.current==="time"&&c.setChartType(2)},[]),Ae=(0,e.useCallback)(c=>{p.current=c,H(c)},[H]),Dt=(0,e.useCallback)(()=>{setTimeout(()=>{if(p.current)try{const c=p.current?.activeChart();$(c)}catch{}},0),M(!0),t.initialConfig.onChartReadyDone?.()},[$,t.initialConfig]),Mt=(0,e.useCallback)(()=>{nt(),t.initialConfig.onSetThemeDone?.()},[nt,t.initialConfig]),vt=(0,e.useCallback)(()=>{nt(),t.initialConfig.onFirstTimeSetThemeDone?.()},[nt,t.initialConfig]),pt=(0,e.useCallback)(c=>{t.initialConfig.onIntervalChanged?.(c)},[t.initialConfig]),Ct=(0,e.useCallback)(async c=>{const{chart:L}=c;if(!L.symbolExt())return;const k=L.chartType();C(k),Ot({resolution:L.resolution(),chartType:k}),await At(),t.initialConfig.onActiveChartChanged?.(c)},[At,Ot,C,t.initialConfig]),Bt=(0,e.useCallback)(c=>{window.open(`https://www.tradingview.com/x/${c}`,"_blank")},[]),Ut=(0,e.useCallback)(c=>{t.initialConfig.onAddStudy?.(c)},[t.initialConfig]),Kt=(0,e.useCallback)(c=>{t.initialConfig.onSymbolChanged?.(c)},[t.initialConfig]),Gt=(0,e.useCallback)(c=>{t.initialConfig.onFirstTimeDataLoadedDone?.(c)},[t.initialConfig]),bt=(0,e.useCallback)(c=>{t.initialConfig.onDataLoadedDone?.(c)},[t.initialConfig]),Pt=(0,e.useCallback)(({chart:c,tradingViewSave:L})=>{if(L)return;[{length:7,color:"rgba(241, 156, 56, 0.7)"},{length:25,color:"rgba(234, 61, 247, 0.7)"},{length:99,color:"rgba(116, 252, 253, 0.7)"}].forEach(({length:k,color:It})=>{c.createStudy("Moving Average",!1,!1,{length:k},{"plot.linewidth":1,"plot.color":It,showLabelsOnPriceScale:!1})})},[]),Yt=(0,e.useCallback)(c=>{(t.initialConfig.onInitChart||Pt)(c);const F=c.chart,k=F.resolution(),It=(0,S.Cz)(k);Lt.current.includes(It)||F.setResolution("1D")},[Pt,t.initialConfig.onInitChart]),Ft=(0,e.useCallback)(()=>{setTimeout(()=>{if(p.current)try{const c=p.current.activeChartIndex(),L=p.current.activeChart(),F=L.chartType();Ct({chart:L,index:c}),C(F),p.current.save(k=>{Tt(k)})}catch{}},100)},[]);(0,e.useEffect)(()=>{if(!(!D||!p.current))try{p.current.activeChart().setChartType(R)}catch{}},[R,D]),(0,e.useEffect)(()=>{if(!(!D||!p.current))try{$(p.current.activeChart())}catch{}},[D,N,$]),(0,e.useEffect)(()=>{if(!(!D||!p.current))try{p.current.setLayout(St(Y))}catch{}},[Y,D]),(0,e.useEffect)(function(){if(D&&p.current)try{const L=p.current.activeChart();L.onChartTypeChanged().subscribe({},()=>{$(L,!1)})}catch{}},[$,D]);const{onTradingViewInitError:Ht,onChartReadyError:Vt}=Xt({storage:z,storageName:s});(0,e.useEffect)(()=>{if(!D)return;const c=l?"none":"auto";try{const F=document.getElementById(tt)?.querySelector("iframe");if(!F)return;F.style.pointerEvents=c}catch{}},[D,tt,l]);const me=(0,e.useMemo)(()=>({...t.initialConfig,tvConfig:{...t.initialConfig.tvConfig,container:tt,interval:m,locale:Nt},onSave:Tt,onLoadConfig:yt,onLoading:gt,onChartReadyDone:Dt,onChartReadyError:Vt,onTradingViewInitError:Ht,onIntervalChanged:pt,onFirstTimeSetThemeDone:vt,onSetThemeDone:Mt,onFirstTimeDataLoadedDone:Gt,onDataLoadedDone:bt,onInitChart:Yt,onActiveChartChanged:Ct,onSymbolChanged:Kt,onAddStudy:Ut,onScreenshotReady:Bt,onLayoutChanged:Ft}),[tt,m,Nt,Ct,Ut,Dt,Vt,bt,Gt,vt,Yt,pt,Ft,yt,gt,Tt,Bt,Mt,Kt,Ht,t.initialConfig]);return(0,A.jsx)("div",{className:"h-full",style:{display:y!==o.tU.TradingView?"none":"block"},children:(0,A.jsx)(e.Suspense,{fallback:null,children:(0,A.jsx)(Ce,{...t,interval:m,limitOrders:Se,positionOrders:Ne,executionOrders:Re,labelLines:Le,overwritingThemeOnLoaded:Oe,themeConfig:fe,initialConfig:me,onInitialized:Ae})})})}}}[76535]; } +export function createDrawingSaveCallbacks(r,__scopeChartDrawingSnapshot) { +var x=r(46108); +var ot=r.n(x); +var Z=r(75857); +var rt=r.n(Z); +var at=r(87809); +var j=r.n(at); +var X=r(34789); +var Q=r.n(X); +var ct=r(19953); +var it=r.n(ct); +var lt=r(82438); +var ut=r.n(lt); +var a=r(87835); +var E=r.n(a); +var O=r(75257); +var W=t=>/^(LineTool)/g.test(t); +var _t=t=>`${t}.customSettings`; +var ft=t=>`${t}.layout`; +var J=t=>t.chartId; +var 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}}; +var 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}; +var xt=async({storage:t,save:n,key:s,widget:i})=>{if(!i)return{chartSave:n,drawingsSave:{}};const{drawings:l,charts:u}=kt({save:__scopeChartDrawingSnapshot(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}}; +var 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}; +var 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}}; +return { save: xt, load: zt, 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)}; +} }; +} diff --git a/test/unit/binance-chart-storage-runtime.test.js b/test/unit/binance-chart-storage-runtime.test.js index 2eddf60..943ca74 100644 --- a/test/unit/binance-chart-storage-runtime.test.js +++ b/test/unit/binance-chart-storage-runtime.test.js @@ -1,11 +1,18 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { getEventListeners } from 'node:events'; +import { readFileSync } from 'node:fs'; +import { runInThisContext } from 'node:vm'; +import { observeChartStorageBootstrap } from '../../src/binance-orderbook-trade/chart-storage/bootstrap.js'; import { startChartStorageOptimizer } from '../../src/binance-orderbook-trade/chart-storage/runtime.js'; import { createNativeMirrorFactory } from '../fixtures/binance-chart-storage/mirror-scoped-callback.js'; +import { createNativeDrawingSaveFactory } from '../fixtures/binance-chart-storage/drawing-save-scoped.js'; const PAGE = 'https://www.binance.com/zh-CN/futures/BTCUSDT'; const QUEUE = 'webpackChunkfutures_trade_ui'; +const HOST_RUNTIME = readFileSync(new URL('../fixtures/binance-chart-storage/webpack-runtime.js', import.meta.url), 'utf8'); +const HOST_PERSISTENCE = readFileSync(new URL('../fixtures/binance-chart-storage/host-persistence.js', import.meta.url), 'utf8'); +const WAITING_DRAWING = { status: 'waiting', reason: null, attempts: 0, matches: 0, executions: 0 }; const EMPTY_WRITER = { acceptedBatches: 0, rejectedBatches: 0, failedBatches: 0, pendingBatches: 0, pendingBytes: 0, peakPendingBatches: 0, peakPendingBytes: 0, @@ -13,10 +20,42 @@ const EMPTY_WRITER = { committedWrites: 0, skippedWrites: 0, nativePassthroughBatches: 0, }; +/** + * Only evaluate the pinned factories: component bodies and lazy chunks stay idle. + * Captured persistence helpers run unchanged. Explicit inert imports cover the UI + * dependencies whose exports the factories do not call until a component renders. + */ +function registerPinnedChartFactories({ mismatchId } = {}) { + const originals = { 70940: createNativeMirrorFactory(), 76535: createNativeDrawingSaveFactory() }; + if (mismatchId) originals[mismatchId] = module => { module.exports = { nativeDrift: mismatchId }; }; + const inactiveIds = [31085, 43917, 19020, 13067, 78441, 53837, 4260, 80817, 92873, 6868, + 81936, 76469, 27571, 87646, 26531, 17409, 94652, 76830, 48651, 79344, 51029, 36307, + 25379, 44745, 15727, 45358, 82071, 85411, 94889, 94547, 80686, 61875, 35630, 12055, + 79515, 80737, 40545, 98107, 51846, 74069, 90664, 36077, 42902, 90170, 63345, 48210, + 51289, 24326, 90922, 34235, 63762, 31542, 17855, 58656, 47354, 87017, 99168, 3742, + 43335, 15540, 20163, 47255, 36488, 48018, 82911, 69578, 53208, 19862, 8874, 26679, + 66547, 78142, 16238, 98087, 55009, 56477, 71730, 36620, 75510, 82508, 91025]; + const modules = Object.fromEntries(inactiveIds.map(id => [id, module => { module.exports = Object.freeze({}); }])); + const lazyLoaders = []; + modules[41594] = module => { + module.exports = { lazy(load) { lazyLoaders.push(load); return { load }; } }; + }; + modules[45250] = module => { + module.exports = { range(start, end) { return Array.from({ length: end - start }, (_, index) => start + index); } }; + }; + modules[17409] = module => { module.exports = { bt: { TRADING: 'TRADING', DELIVERING: 'DELIVERING' } }; }; + runInThisContext(HOST_PERSISTENCE); + let require; + globalThis[QUEUE].push([['pinned-runtime-host'], { ...modules, ...originals }, hostRequire => { require = hostRequire; }]); + runInThisContext(HOST_RUNTIME); + return { require, originals, lazyLoaders }; +} + /** 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 observers = []; 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; @@ -32,6 +71,8 @@ function installPage(t, { url = PAGE, framed = false, queue } = {}) { else delete globalThis[QUEUE]; t.after(async () => { try { + events.dispatchEvent(new Event('pagehide')); + for (const observer of observers) observer.stop(); await Promise.all(sessions.map(session => session.stop())); assert.equal(storageAccesses, 0); assert.equal(getEventListeners(events, 'pagehide').length, 0); @@ -44,6 +85,11 @@ function installPage(t, { url = PAGE, framed = false, queue } = {}) { }); return { events, + observe(configuration) { + const observer = observeChartStorageBootstrap(configuration); + observers.push(observer); + return observer; + }, start() { const session = startChartStorageOptimizer(); sessions.push(session); @@ -95,12 +141,12 @@ for (const url of [ 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 }); + // Then fixed counters show a stopped mirror while drawing capture retains the bounded queue ownership. + assert.deepEqual(waiting, { status: 'waiting', reason: null, attempts: 0, matches: 0, executions: 0, phase: 'active', writer: EMPTY_WRITER, drawingScope: WAITING_DRAWING }); 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); + assert.deepEqual(stopped, { status: 'native', reason: 'manual', attempts: 0, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER, drawingScope: WAITING_DRAWING }); + assert.equal(Object.hasOwn(globalThis, QUEUE), true); + assert.equal(getEventListeners(page.events, 'pagehide').length, 1); }); } @@ -126,7 +172,7 @@ for (const kind of ['populated', 'runtime-owned']) { 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 }); + assert.deepEqual(stopped, { status: 'native', reason: 'bootstrap_unavailable', attempts: 0, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER, drawingScope: { ...WAITING_DRAWING, status: 'unavailable', reason: 'bootstrap_unavailable' } }); }); } @@ -146,9 +192,9 @@ test('user keeps the original factory executable when strict source registration // 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.equal(typeof Object.getOwnPropertyDescriptor(globalThis[QUEUE], 'push').get, 'function'); 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 }); + assert.deepEqual(stopped, { status: 'native', reason: 'source_mismatch', attempts: 1, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER, drawingScope: WAITING_DRAWING }); }); test('user restores the exact pinned factory when stopping before its first execution', async t => { @@ -168,10 +214,10 @@ test('user restores the exact pinned factory when stopping before its first exec // 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.deepEqual(accepted, { status: 'waiting', reason: null, attempts: 1, matches: 1, executions: 0, phase: 'active', writer: EMPTY_WRITER, drawingScope: WAITING_DRAWING }); 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 }); + assert.equal(typeof Object.getOwnPropertyDescriptor(globalThis[QUEUE], 'push').get, 'function'); + assert.deepEqual(stopped, { status: 'native', reason: 'manual', attempts: 1, matches: 1, executions: 0, phase: 'stopped', writer: EMPTY_WRITER, drawingScope: WAITING_DRAWING }); }); test('user retires the capture hook exactly at thirty seconds without leaving a page listener', async t => { @@ -192,7 +238,7 @@ test('user retires the capture hook exactly at thirty seconds without leaving a 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(stopped, { status: 'native', reason: 'capture_deadline', attempts: 0, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER, drawingScope: { ...WAITING_DRAWING, status: 'unavailable', reason: 'capture_deadline' } }); assert.deepEqual(session.snapshot(), stopped); assert.equal(Object.hasOwn(globalThis, QUEUE), false); assert.equal(getEventListeners(page.events, 'pagehide').length, 0); @@ -215,7 +261,7 @@ test('user releases registration and cancels the capture deadline on pagehide', 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(stopped, { status: 'native', reason: 'pagehide', attempts: 1, matches: 1, executions: 0, phase: 'stopped', writer: EMPTY_WRITER, drawingScope: { ...WAITING_DRAWING, status: 'unavailable', reason: 'pagehide' } }); assert.deepEqual(session.snapshot(), stopped); }); @@ -247,7 +293,7 @@ test('user preserves the exact native dependency exception during pinned factory 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 }); + assert.deepEqual(stopped, { status: 'native', reason: 'capture_failed', attempts: 1, matches: 1, executions: 1, phase: 'stopped', writer: EMPTY_WRITER, drawingScope: WAITING_DRAWING }); }); test('user retires a queued capture when navigation leaves the trading scope before registration', async t => { @@ -266,7 +312,7 @@ test('user retires a queued capture when navigation leaves the trading scope bef 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 }); + assert.deepEqual(stopped, { status: 'native', reason: 'scope_changed', attempts: 0, matches: 0, executions: 0, phase: 'stopped', writer: EMPTY_WRITER, drawingScope: { ...WAITING_DRAWING, status: 'unavailable', reason: 'scope_changed' } }); }); test('user receives detached fixed-count snapshots through only two frozen runtime controls', async t => { @@ -285,5 +331,393 @@ test('user receives detached fixed-count snapshots through only two frozen runti // 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 }); + assert.deepEqual(fresh, { status: 'waiting', reason: null, attempts: 1, matches: 1, executions: 0, phase: 'active', writer: EMPTY_WRITER, drawingScope: WAITING_DRAWING }); +}); + +test('user retains drawing capture after stopping mirror optimization before either module registers', async t => { + // Given an early trading page awaits both chart storage factories. + const page = installPage(t); + t.mock.timers.enable({ apis: ['setTimeout'] }); + const session = page.start(); + + // When mirror optimization stops before the bounded drawing capture deadline. + const stopped = await session.stop(); + const ownedWhileWaiting = Object.hasOwn(globalThis, QUEUE); + t.mock.timers.tick(30_000); + + // Then drawing capture stays available until its deadline while the mirror writer stays stopped. + assert.equal(ownedWhileWaiting, true); + assert.equal(stopped.phase, 'stopped'); + assert.equal(stopped.drawingScope.status, 'waiting'); + assert.equal(session.snapshot().drawingScope.reason, 'capture_deadline'); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); +}); + +for (const early of [true, false]) { + for (const order of [['70940', '76535'], ['76535', '70940']]) { + for (const nested of [false, true]) { + test(`user captures both chart modules with registration order ${order.join('-')} early=${early} nested=${nested}`, t => { + // Given the production observer owns one empty queue before the retained real Rspack runtime. + const page = installPage(t); + const captured = []; + const failed = []; + const completed = []; + const originals = { + 70940(module, exports, require) { + module.exports = { kind: 'mirror', child: nested ? require(76535) : null }; + }, + 76535(module) { module.exports = { kind: 'drawing' }; }, + }; + const targets = Object.fromEntries(Object.entries(originals).map(([id, original]) => [id, factory => { + assert.equal(factory, original); + return function (module, exports, require) { + factory.call(this, module, exports, require); + module.exports.protected = true; + }; + }])); + page.observe({ + targets, + onCapture: id => { captured.push(id); }, + onFailure: (id, reason) => { failed.push([id, reason]); }, + onComplete: reason => { completed.push(reason); }, + }); + let require; + + // When both registration orders execute sequentially or through the mirror's drawing dependency. + if (!early) runInThisContext(HOST_RUNTIME); + for (const id of order) { + globalThis[QUEUE].push([[id], { [id]: originals[id] }, hostRequire => { require = hostRequire; }]); + } + if (early) runInThisContext(HOST_RUNTIME); + const executionOrder = nested ? ['70940', '76535'] : order; + const exported = Object.fromEntries(executionOrder.map(id => [id, require(id)])); + const cached = require(70940); + + // Then both effects persist in cached exports while the original factories and native dispatcher are restored. + assert.deepEqual(exported['76535'], { kind: 'drawing', protected: true }); + assert.deepEqual(exported['70940'], { kind: 'mirror', child: nested ? exported['76535'] : null, protected: true }); + assert.equal(cached, exported['70940']); + assert.deepEqual(captured, nested ? ['76535', '70940'] : order); + assert.deepEqual(failed, []); + assert.deepEqual(completed, [null]); + assert.equal(require.m[70940], originals[70940]); + assert.equal(require.m[76535], originals[76535]); + assert.equal(Object.hasOwn(Object.getOwnPropertyDescriptor(globalThis[QUEUE], 'push'), 'value'), true); + assert.equal(globalThis[QUEUE].length, 2); + }); + } + } +} + +for (const failedId of ['70940', '76535']) { + test(`user preserves cached native errors from module ${failedId} while the other target captures`, t => { + // Given two observed factories share the retained Rspack module cache and one native error. + const page = installPage(t); + const nativeFailure = new Error('Native chart module dependency failed'); + const otherId = failedId === '70940' ? '76535' : '70940'; + let attempts = 0; + const originals = { + [failedId]() { attempts += 1; throw nativeFailure; }, + [otherId](module) { module.exports = { retained: 'native-success' }; }, + }; + const captured = []; + const failed = []; + const completed = []; + page.observe({ + targets: Object.fromEntries(Object.entries(originals).map(([id, factory]) => [id, original => { + assert.equal(original, factory); + return original; + }])), + onCapture: id => { captured.push(id); }, + onFailure: (id, reason) => { failed.push([id, reason]); }, + onComplete: reason => { completed.push(reason); }, + }); + let require; + globalThis[QUEUE].push([['native-errors'], originals, hostRequire => { require = hostRequire; }]); + runInThisContext(HOST_RUNTIME); + + // When one factory throws twice through the host cache and the independent target executes. + assert.throws(() => require(failedId), error => error === nativeFailure); + assert.throws(() => require(failedId), error => error === nativeFailure); + const exported = require(otherId); + + // Then native exception identity and cache behavior survive without canceling the successful target. + assert.equal(attempts, 1); + assert.deepEqual(exported, { retained: 'native-success' }); + assert.deepEqual(captured, [otherId]); + assert.deepEqual(failed, [[failedId, 'execution_failed']]); + assert.deepEqual(completed, [null]); + assert.equal(require.m[failedId], originals[failedId]); + assert.equal(require.m[otherId], originals[otherId]); + }); +} + +for (const mismatchId of ['70940', '76535']) { + test(`user retains the matching target when module ${mismatchId} rejects its source pin`, t => { + // Given one complete-source matcher rejects while the other accepts its native factory. + const page = installPage(t); + const otherId = mismatchId === '70940' ? '76535' : '70940'; + const originals = { + [mismatchId](module) { module.exports = { mode: 'native-drift' }; }, + [otherId](module) { module.exports = { mode: 'native-matched' }; }, + }; + const captured = []; + const failed = []; + const completed = []; + page.observe({ + targets: { + [mismatchId]() { throw new Error('Complete source mismatch'); }, + [otherId]: original => function (module, exports, require) { + original.call(this, module, exports, require); + module.exports.mode = 'protected'; + }, + }, + onCapture: id => { captured.push(id); }, + onFailure: (id, reason) => { failed.push([id, reason]); }, + onComplete: reason => { completed.push(reason); }, + }); + let require; + + // When the native runtime registers the drifted module before the matching module. + globalThis[QUEUE].push([['drift'], { [mismatchId]: originals[mismatchId] }]); + globalThis[QUEUE].push([['matched'], { [otherId]: originals[otherId] }, hostRequire => { require = hostRequire; }]); + runInThisContext(HOST_RUNTIME); + const native = require(mismatchId); + const protectedModule = require(otherId); + + // Then only the rejected target remains native and the shared observer finishes after the accepted target. + assert.deepEqual(native, { mode: 'native-drift' }); + assert.deepEqual(protectedModule, { mode: 'protected' }); + assert.deepEqual(captured, [otherId]); + assert.deepEqual(failed, [[mismatchId, 'source_mismatch']]); + assert.deepEqual(completed, [null]); + assert.equal(require.m[mismatchId], originals[mismatchId]); + assert.equal(require.m[otherId], originals[otherId]); + }); +} + +test('user keeps a later queue owner when the original observer is retired', t => { + // Given a registered target waits while the production observer still owns its queue accessor. + const page = installPage(t); + const failures = []; + const completed = []; + const original = module => { module.exports = 'native'; }; + const observer = page.observe({ + targets: { 70940: factory => factory, 76535: factory => factory }, + onCapture: id => { completed.push(id); }, + onFailure: (id, reason) => { failures.push([id, reason]); }, + onComplete: reason => { completed.push(reason); }, + }); + globalThis[QUEUE].push([['pending-native'], { 70940: original }]); + const newOwner = function newPush(chunk) { return Array.prototype.push.call(this, chunk); }; + Object.defineProperty(globalThis[QUEUE], 'push', { value: newOwner, writable: true, configurable: true }); + + // When cleanup encounters a push property explicitly owned by another integration. + observer.stop('stopped'); + + // Then it restores its unexecuted factory without overwriting the later queue owner. + assert.equal(globalThis[QUEUE].push, newOwner); + assert.equal(globalThis[QUEUE][0][1][70940], original); + assert.deepEqual(failures, [['70940', 'stopped'], ['76535', 'stopped']]); + assert.deepEqual(completed, ['ownership_lost']); +}); + +test('user cannot stack a second observer on a pending chart queue', t => { + // Given one observer already owns the early page queue. + const page = installPage(t); + const completed = []; + const configuration = { + targets: { 70940: factory => factory, 76535: factory => factory }, + onCapture: id => { completed.push(id); }, + onFailure: (id, reason) => { completed.push([id, reason]); }, + onComplete: reason => { completed.push(reason); }, + }; + const observer = page.observe(configuration); + const owned = Object.getOwnPropertyDescriptor(globalThis[QUEUE], 'push'); + + // When a second observation request reaches the same still-owned boundary. + const second = () => page.observe(configuration); + + // Then the second request rejects and the first observer alone releases its queue. + assert.throws(second, /before runtime on a native empty queue/); + assert.deepEqual(Object.getOwnPropertyDescriptor(globalThis[QUEUE], 'push'), owned); + observer.stop(); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); + assert.deepEqual(completed, [['70940', 'stopped'], ['76535', 'stopped'], null]); +}); + +test('user evaluates the complete native chart factories without rendering lazy UI or opening storage', t => { + // Given the retained runtime and persistence helpers supply the complete native factories. + installPage(t); + const host = registerPinnedChartFactories(); + + // When the original mirror factory requires the original drawing factory during module initialization. + const mirror = host.require(70940); + const drawing = host.require(76535); + + // Then public component exports and lazy loader registration exist without running any component body. + assert.deepEqual(Object.keys(mirror), ['TradingView', 'default']); + assert.equal(typeof mirror.TradingView, 'function'); + assert.equal(mirror.default, mirror.TradingView); + assert.deepEqual(Object.keys(drawing), ['A', 'X']); + assert.equal(typeof drawing.A, 'function'); + assert.equal(typeof drawing.X, 'function'); + assert.equal(host.lazyLoaders.length, 1); + assert.equal(typeof host.lazyLoaders[0], 'function'); + assert.equal(host.require(45250).range(2, 5).join(','), '2,3,4'); + assert.deepEqual(host.require(17409).bt, { TRADING: 'TRADING', DELIVERING: 'DELIVERING' }); +}); + +for (const stopAt of ['before-registration', 'between-modules', 'after-modules']) { + test(`user retains installed drawing protection when mirror stops ${stopAt}`, async t => { + // Given both complete source pins are available behind the real early runtime entrypoint. + const page = installPage(t); + t.mock.timers.enable({ apis: ['setTimeout'] }); + const session = page.start(); + if (stopAt === 'before-registration') await session.stop(); + const host = registerPinnedChartFactories(); + + // When the caller stops before registration, between executions, or after nested module initialization. + let drawing; + if (stopAt === 'between-modules') { + drawing = host.require(76535); + await session.stop(); + } + const mirror = host.require(70940); + drawing = host.require(76535); + await session.stop(); + const stopped = session.snapshot(); + t.mock.timers.tick(60_000); + + // Then drawing protection stays active in the cached export and all observation timers and ownership are released. + const matched = stopAt === 'before-registration' ? 0 : 1; + const executed = stopAt === 'after-modules' ? 1 : 0; + assert.deepEqual(stopped, { + status: 'native', reason: 'manual', attempts: matched, matches: matched, executions: executed, + phase: 'stopped', writer: EMPTY_WRITER, + drawingScope: { status: 'active', reason: null, attempts: 1, matches: 1, executions: 1 }, + }); + assert.deepEqual(session.snapshot(), stopped); + assert.equal(host.require(76535), drawing); + assert.equal(host.require(70940), mirror); + assert.equal(typeof drawing.A, 'function'); + assert.equal(typeof mirror.TradingView, 'function'); + assert.equal(host.require.m[70940], host.originals[70940]); + assert.equal(host.require.m[76535], host.originals[76535]); + assert.equal(host.lazyLoaders.length, 1); + assert.equal(Object.hasOwn(Object.getOwnPropertyDescriptor(globalThis[QUEUE], 'push'), 'value'), true); + assert.equal(getEventListeners(page.events, 'pagehide').length, 0); + }); +} + +for (const mismatchId of ['70940', '76535']) { + test(`user captures the matching complete factory when the runtime rejects source pin ${mismatchId}`, async t => { + // Given one upstream factory has drifted and the other retains its complete verified source. + const page = installPage(t); + const session = page.start(); + const host = registerPinnedChartFactories({ mismatchId }); + + // When both modules execute through the real Rspack cache after independent source matching. + const mirror = host.require(70940); + const drawing = host.require(76535); + if (mismatchId === '70940') await session.stop(); + const active = session.snapshot(); + + // Then only the mismatched target stays native while the matching target has one completed execution. + if (mismatchId === '70940') { + assert.deepEqual(mirror, { nativeDrift: '70940' }); + assert.equal(typeof drawing.A, 'function'); + assert.deepEqual(active, { + status: 'native', reason: 'source_mismatch', attempts: 1, matches: 0, executions: 0, + phase: 'stopped', writer: EMPTY_WRITER, + drawingScope: { status: 'active', reason: null, attempts: 1, matches: 1, executions: 1 }, + }); + } else { + assert.equal(typeof mirror.TradingView, 'function'); + assert.deepEqual(drawing, { nativeDrift: '76535' }); + assert.deepEqual(active, { + status: 'active', reason: null, attempts: 1, matches: 1, executions: 1, + phase: 'active', writer: EMPTY_WRITER, + drawingScope: { status: 'source_mismatch', reason: 'source_mismatch', attempts: 1, matches: 0, executions: 0 }, + }); + } + assert.equal(host.require.m[mismatchId], host.originals[mismatchId]); + assert.equal(Object.hasOwn(Object.getOwnPropertyDescriptor(globalThis[QUEUE], 'push'), 'value'), true); + }); +} + +test('user keeps the original drawing capture deadline when mirror stops partway through startup', async t => { + // Given startup has already used twenty seconds of its finite registration window. + const page = installPage(t); + t.mock.timers.enable({ apis: ['setTimeout'] }); + const session = page.start(); + t.mock.timers.tick(20_000); + + // When mirror stops and the remaining drawing capture window reaches the original deadline. + await session.stop(); + t.mock.timers.tick(9_999); + const waiting = session.snapshot(); + t.mock.timers.tick(1); + const expired = session.snapshot(); + + // Then manual stop preserves the remaining window without granting another thirty seconds. + assert.deepEqual(waiting.drawingScope, WAITING_DRAWING); + assert.deepEqual(expired.drawingScope, { ...WAITING_DRAWING, status: 'unavailable', reason: 'capture_deadline' }); + assert.equal(expired.reason, 'manual'); + assert.equal(expired.phase, 'stopped'); + assert.equal(Object.hasOwn(globalThis, QUEUE), false); + assert.equal(getEventListeners(page.events, 'pagehide').length, 0); +}); + +test('user keeps installed drawing protection when an unexecuted mirror reaches the capture deadline', async t => { + // Given the drawing factory has executed while the accepted mirror factory still waits. + const page = installPage(t); + t.mock.timers.enable({ apis: ['setTimeout'] }); + const session = page.start(); + const host = registerPinnedChartFactories(); + const drawing = host.require(76535); + + // When the original capture deadline retires the remaining unexecuted mirror target. + t.mock.timers.tick(30_000); + await session.stop(); + const stopped = session.snapshot(); + + // Then only mirror optimization retires and the installed drawing export stays cached and active. + assert.deepEqual(stopped, { + status: 'native', reason: 'capture_deadline', attempts: 1, matches: 1, executions: 0, + phase: 'stopped', writer: EMPTY_WRITER, + drawingScope: { status: 'active', reason: null, attempts: 1, matches: 1, executions: 1 }, + }); + assert.equal(host.require(76535), drawing); + assert.equal(host.require.m[70940], host.originals[70940]); + assert.equal(Object.hasOwn(Object.getOwnPropertyDescriptor(globalThis[QUEUE], 'push'), 'value'), true); + assert.equal(getEventListeners(page.events, 'pagehide').length, 0); +}); + +test('user drains the mirror writer on pagehide after both chart factories have captured', async t => { + // Given both complete factories have executed and interception has restored the native queue. + const page = installPage(t); + t.mock.timers.enable({ apis: ['setTimeout'] }); + const session = page.start(); + const host = registerPinnedChartFactories(); + host.require(70940); + const active = session.snapshot(); + const listening = getEventListeners(page.events, 'pagehide').length; + + // When pagehide arrives after capture has canceled the registration deadline. + page.events.dispatchEvent(new Event('pagehide')); + const stopped = await session.stop(); + t.mock.timers.tick(60_000); + + // Then pagehide still drains the active mirror without withdrawing installed drawing protection. + assert.equal(active.status, 'active'); + assert.equal(active.drawingScope.status, 'active'); + assert.equal(listening, 1); + assert.deepEqual(stopped, { + status: 'native', reason: 'pagehide', attempts: 1, matches: 1, executions: 1, + phase: 'stopped', writer: EMPTY_WRITER, + drawingScope: { status: 'active', reason: null, attempts: 1, matches: 1, executions: 1 }, + }); + assert.deepEqual(session.snapshot(), stopped); + assert.equal(getEventListeners(page.events, 'pagehide').length, 0); });