From 6a7e994f079b592ba851afc80f451d65f55b696d Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 5 Aug 2026 02:40:20 +0800 Subject: [PATCH] fix(modal): cancel stale purchase flights on scope change Co-Authored-By: Claude Fable 5 --- assets/js/vip-purchase-coordinator.js | 88 +++++++++++++++++++++------ 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/assets/js/vip-purchase-coordinator.js b/assets/js/vip-purchase-coordinator.js index 18bbd4c0..90810066 100644 --- a/assets/js/vip-purchase-coordinator.js +++ b/assets/js/vip-purchase-coordinator.js @@ -17,6 +17,7 @@ var createFlights = Object.create(null); var polls = Object.create(null); var lastScope = ""; + var scopeGeneration = 0; var SUCCESS_STATUSES = { paid: true, delivered: true, @@ -40,6 +41,11 @@ } function clearFlights() { + scopeGeneration += 1; + Object.keys(createFlights).forEach(function (key) { + var flight = createFlights[key]; + if (flight && typeof flight.cancel === "function") flight.cancel(); + }); createFlights = Object.create(null); Object.keys(polls).forEach(function (pollKey) { var active = polls[pollKey]; @@ -87,6 +93,9 @@ function callback(options, name) { var args = Array.prototype.slice.call(arguments, 2); if (!options || typeof options[name] !== "function") return; + if (options.scope && userScope() !== options.scope) return; + if (options.generation !== undefined && scopeGeneration !== options.generation) return; + if (typeof options.isActive === "function" && !options.isActive()) return; try { options[name].apply(null, args); } catch (error) { @@ -105,21 +114,36 @@ } var key = userScope() + ":" + String(packageId) + ":" + method; - if (createFlights[key]) return createFlights[key]; + if (createFlights[key]) return createFlights[key].promise; - var promise = request("/pay/vip", { - method: "POST", - body: JSON.stringify({ - package_id: packageId, - payment_method: method, + var requestController = !options.signal && window.AbortController + ? new window.AbortController() + : null; + var flight = { + promise: request("/pay/vip", { + method: "POST", + body: JSON.stringify({ + package_id: packageId, + payment_method: method, + }), + signal: options.signal || (requestController ? requestController.signal : undefined), }), - signal: options.signal, + cancel: function () { + if (typeof options.cancel === "function") { + try { options.cancel(); } catch (error) {} + } + if (requestController) { + try { requestController.abort(); } catch (error) {} + } + }, + }; + createFlights[key] = flight; + flight.promise.then(function () { + if (createFlights[key] === flight) delete createFlights[key]; + }, function () { + if (createFlights[key] === flight) delete createFlights[key]; }); - createFlights[key] = promise; - promise.then(function () { - if (createFlights[key] === promise) delete createFlights[key]; - }); - return promise; + return flight.promise; } function exchange(options) { @@ -149,7 +173,7 @@ } function poll(orderNumber, options) { - options = options || {}; + options = Object.assign({}, options || {}); orderNumber = String(orderNumber || ""); if (!orderNumber) { return { @@ -158,6 +182,9 @@ }; } var pollScope = userScope(); + var pollGeneration = scopeGeneration; + if (!options.scope) options.scope = pollScope; + if (options.generation === undefined) options.generation = pollGeneration; var pollKey = pollScope + ":" + orderNumber; if (polls[pollKey]) return polls[pollKey].handle; @@ -201,6 +228,11 @@ } function terminal(data, response, status) { + if ((options.scope && userScope() !== options.scope) + || (options.generation !== undefined && scopeGeneration !== options.generation)) { + finish({ terminal: false, aborted: true, status: "scope_changed" }); + return; + } var success = !!(data && data.paid) || !!SUCCESS_STATUSES[status]; invalidate(options.tags || ["vip", "orders"]); callback(options, "onTerminal", { @@ -232,8 +264,8 @@ function run() { state.timer = null; if (state.done || state.aborted) return; - // Detect an account transition even when no auth event reached this page. - if (userScope() !== pollScope || state.done || state.aborted) return; + // Detect account transitions and explicit auth resets even when no event reached this page. + if (userScope() !== pollScope || scopeGeneration !== pollGeneration || state.done || state.aborted) return; if (document.hidden) { state.waitingForVisible = true; return; @@ -307,9 +339,23 @@ var activePoll = null; var stopped = false; var controller = window.AbortController ? new window.AbortController() : null; + var startScope = userScope(); + var startGeneration = scopeGeneration; if (controller) options.signal = controller.signal; + options.scope = startScope; + options.generation = startGeneration; + options.isActive = function () { + return !stopped && scopeGeneration === startGeneration && userScope() === startScope; + }; + options.cancel = function () { + if (controller) { + try { controller.abort(); } catch (error) {} + } + }; var promise = purchase(options).then(function (response) { - if (stopped) return { ok: false, aborted: true, status: "aborted" }; + if (!options.isActive()) { + return { ok: false, aborted: true, status: stopped ? "aborted" : "scope_changed" }; + } var data = response.data || response.json || {}; if (!response.ok) { callback(options, "onError", response); @@ -322,6 +368,9 @@ callback(options, "onError", missing); return missing; } + if (!options.isActive()) { + return { ok: false, aborted: true, status: "scope_changed" }; + } activePoll = poll(data.order_number, options); activePoll.promise.then(function (result) { if (result.success) callback(options, "onSuccess", result); @@ -334,6 +383,9 @@ poll: activePoll, }; } + if (!options.isActive()) { + return { ok: false, aborted: true, status: "scope_changed" }; + } invalidate(options.tags || ["vip", "orders"]); callback(options, "onSuccess", { terminal: true, success: true, data: data, response: response }); return { ok: true, pending: false, data: data, response: response }; @@ -343,9 +395,7 @@ promise: promise, abort: function () { stopped = true; - if (controller) { - try { controller.abort(); } catch (error) {} - } + options.cancel(); if (activePoll && typeof activePoll.abort === "function") activePoll.abort(); }, };