/** * Copyright IBM Corp. 2021, 2026 * SPDX-License-Identifier: BUSL-1.1 */ import Store from 'api/services/store'; import { service } from '@ember/service'; import { TrackedObject } from 'tracked-built-ins'; import { dropTask, timeout } from 'ember-concurrency'; export default class extends Store { @service flashMessages; @service intl; @service router; /** * Overrides store.query to detect a rate-limit warning and surface a * flash notification to the user. The partial result is still returned * normally so the UI renders whatever data was fetched. * @override */ async query(type, query, options) { const result = await super.query(type, query, options); if (result?.meta?.rateLimitWarning) { const { retryAfter } = result.meta.rateLimitWarning; this._countdownTask.perform(retryAfter); } return result; } /** * Creates a sticky flash notification and counts down each second until the * rate-limit window expires and drops any new ones that come in. */ _countdownTask = dropTask(async (retryAfter) => { // Use a TrackedObject for `button` so that mutations to `button.disabled` // are tracked and the template re-renders when the countdown finishes. const button = new TrackedObject({ label: this.intl.t('actions.retry'), disabled: true, action: (flash) => { flash.destroyMessage(); this.router.refresh(); }, }); const flash = this.flashMessages .warning( this.intl.t('errors.429.description-partial', { seconds: retryAfter }), { color: 'warning', title: this.intl.t('errors.429.title'), sticky: true, dismiss: (flash) => { this._countdownTask.cancelAll(); flash.destroyMessage(); }, button, }, ) .getFlashObject(); let remaining = retryAfter; while (remaining > 0) { await timeout(1000); remaining -= 1; flash.message = remaining > 0 ? this.intl.t('errors.429.description-partial', { seconds: remaining, }) : this.intl.t('errors.429.description-ready'); } button.disabled = false; }); }