npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lucidaquarian/alert-queue

v1.0.0

Published

Serialized alert queue with source deduplication for Ionic, Capacitor, and Cordova

Readme

@lucidaquarian/alert-queue

Serialized alert queue with source deduplication for Ionic, Capacitor, and Cordova.

A lightweight, framework-agnostic queue and lifecycle manager for alert dialogs. It guarantees that only one alert is visible at a time, serializes pending alerts by priority, and deduplicates by source so the same feature never stacks duplicate dialogs.

It is not a UI library — rendering is fully delegated to the platform (@ionic/core's alertController, Capacitor's Dialog, or Cordova's navigator.notification). The plugin only owns the queue, the state machine, and the lifecycle.

  • ✅ Framework-agnostic core (Ionic Angular / React / Vue, Capacitor, Cordova)
  • ✅ Priority queue + single-alert mutex
  • ✅ Source deduplication (drop-new / replace)
  • ✅ Per-alert and global lifecycle hooks
  • ✅ Auto timeouts, programmatic dismissal, source-scoped dismissal
  • ✅ Pluggable rendering — bring your own popup UI via a tiny adapter
  • ✅ Full TypeScript types; ESM + CJS + IIFE builds
  • ✅ Zero runtime dependencies

Installation

npm install @lucidaquarian/alert-queue

Requirements: Node 16+ at runtime (zero runtime dependencies). The dev toolchain (Vitest) needs Node 18+; a dependency-free Node 16 runtime smoke test (npm run smoke) runs in CI to guarantee the published bundle works on Node 16.

Then install whichever platform package you actually use (all are optional peer dependencies — install only what you need):

# Ionic (any framework)
npm install @ionic/core

# Capacitor native dialogs
npm install @capacitor/core @capacitor/dialog

# Cordova native dialogs
cordova plugin add cordova-plugin-dialogs

# Angular DI integration (optional)
npm install @angular/core

Quick start

import { AlertQueue } from '@lucidaquarian/alert-queue';

const result = await AlertQueue.show({
  title: 'Session expired',
  message: 'Please log in again.',
  buttons: [
    { text: 'Cancel', role: 'cancel' },
    { text: 'Log in', role: 'confirm' },
  ],
});

if (result.status === 'confirmed') {
  router.navigate(['/login']);
}

The platform is auto-detected on first use — no configuration required.

Convenience helpers

For the common cases, skip the verbose show({...}):

await AlertQueue.alert('Saved', 'Your changes are live.');

const ok = await AlertQueue.confirm('Delete file?', 'This cannot be undone.');
if (ok) { /* ... */ }

const name = await AlertQueue.prompt('Your name?', undefined, { placeholder: 'Ada' });
if (name !== null) { /* entered text (may be '') */ }

All three accept an options object with any AlertConfig field (sourceId, priority, timeout, cooldown, hooks, …) plus okText / cancelText (and placeholder / defaultValue / inputType for prompt). The same helpers are available on the Angular AlertQueueService.


Screenshots

Real Ionic dialogs rendered by the library (captured by the Playwright e2e suite in e2e/):

| Two-button alert | Destructive multi-button alert | |---|---| | Session expired alert | Delete project alert |

The plugin manages the queue and lifecycle; the visual styling comes from the platform (here, @ionic/core in Material mode).


Platform support

| Capability | Ionic (@ionic/core) | Capacitor (@capacitor/dialog) | Cordova (cordova-plugin-dialogs) | |---|:---:|:---:|:---:| | Show / queue / priority / dedup | ✅ | ✅ | ✅ | | Multi-button alerts | ✅ | ⚠️ confirm + cancel only | ✅ | | Per-button handler fires on tap | ✅ | ✅ | ✅ | | handler returns false to keep open | ✅ | ❌ (OS-closed) | ❌ (OS-closed) | | Inputs / prompts | ✅ all input types | ⚠️ single text input | ⚠️ single text input | | cssClass, backdrop dismiss | ✅ | ❌ (OS-styled) | ❌ (OS-styled) | | confirmed / cancelled results | ✅ | ✅ | ✅ | | dismissed (backdrop / programmatic) | ✅ | ❌ | ⚠️ back-button only | | timeout auto-dismiss | ✅ | ❌ | ❌ | | dismiss() / dismissAll() / dismissSource() (active alert) | ✅ | ❌ | ❌ | | replace policy on the active alert | ✅ | ❌ | ❌ |

Why the ❌s on native? Capacitor's Dialog and Cordova's navigator.notification are OS-managed and expose no programmatic dismiss API. So anything that needs to remove an already-visible dialog — timeout, replace on the active alert, and the dismiss*() methods — cannot affect a native dialog that is already on screen. Queued (not-yet-shown) alerts are still fully controllable on every platform. AlertHandle.canDismiss reflects this at runtime.


API reference

All methods are static on AlertQueue.

| Method | Signature | Description | |---|---|---| | configure | (options: GlobalConfig) => void | Set global defaults + hooks. Merges across calls (later wins). | | init | () => void | Auto-detect platform & cache the adapter. Called lazily on first show(). | | show | (config: AlertConfig) => Promise<AlertResult> | Enqueue an alert; resolves when it finally settles. | | alert | (title, message?, options?) => Promise<AlertResult> | Shortcut: single-OK alert. See Convenience helpers. | | confirm | (title, message?, options?) => Promise<boolean> | Shortcut: Cancel/OK; resolves true when confirmed. | | prompt | (title, message?, options?) => Promise<string \| null> | Shortcut: single input; resolves the value or null. | | dismiss | (id?: string) => Promise<void> | Dismiss a specific queued/active alert, or the current one. | | dismissAll | () => Promise<void> | Clear the queue and dismiss the active alert. | | dismissSource | (sourceId: string) => Promise<void> | Dismiss every alert from a source. | | hasPendingSource | (sourceId: string) => boolean | Whether a source is currently active or queued. | | onDismiss | (handler: (r: AlertResult) => void) => () => void | Subscribe to every dismissal. Returns an unsubscribe fn. | | useAdapter | (adapter: IAlertAdapter) => void | Replace the renderer with your own popup UI. See Custom UI. | | reset | () => void | Testing: clear all state. |

AlertConfig

| Field | Type | Default | Notes | |---|---|---|---| | title | string | — | Required. | | message | string | — | | | buttons | AlertButton[] | [{ text: 'OK', role: 'confirm' }] | | | inputs | AlertInput[] | — | Prompt-style input fields. Ionic: all types; Capacitor/Cordova: single text input. | | priority | number | 0 | Higher is shown sooner. | | timeout | number | — | Auto-dismiss after N ms (Ionic only). | | dismissible | boolean | true | Backdrop tap closes (Ionic only). | | sourceId | string | — | Enables deduplication. | | sourcePolicy | 'drop-new' \| 'replace' | 'drop-new' | Behaviour when the source is already active. | | cooldown | number | — | After dismissal, suppress new same-sourceId alerts for N ms. | | cssClass | string \| string[] | — | Ionic only. | | id | string | auto UUID | | | beforeShow / afterShow | (config) => void \| Promise | — | Per-alert hooks. | | beforeDismiss / afterDismiss | (result) => void \| Promise | — | Per-alert hooks. | | onDeduplicated | (existingId) => void | — | Fires when suppressed by drop-new. |

AlertResult

interface AlertResult {
  status: 'confirmed' | 'cancelled' | 'timeout' | 'dismissed' | 'deduplicated';
  action?: string;     // tapped button text, when known
  role?: string;       // tapped button role, when known
  existingId?: string; // only when status === 'deduplicated'
  values?: Record<string, unknown>; // entered input values, when the alert had `inputs`
}

Inputs & prompts

Add inputs to collect input. Entered values come back on AlertResult.values, keyed by each input's name:

const r = await AlertQueue.show({
  title: 'Rename project',
  inputs: [
    { name: 'title', type: 'text', placeholder: 'New name', value: currentName },
  ],
  buttons: [
    { text: 'Cancel', role: 'cancel' },
    { text: 'Save', role: 'confirm' },
  ],
});

if (r.status === 'confirmed') {
  await rename(r.values?.title as string);
}

Ionic supports every input type (text, number, password, textarea, checkbox, radio, …). Capacitor and Cordova only have a native single-text prompt, so they use the first input and ignore the rest (see the support matrix).


Source deduplication

Each alert may carry a sourceId. While an alert from that source is active or queued, the gate is closed for that source.

| Scenario | Behaviour | |---|---| | First alert from source X | Admitted; source registered. | | Second alert from X (drop-new, default) | Suppressed → resolves { status: 'deduplicated', existingId }. | | Second alert from X (replace) | The existing alert is dismissed; the new one is shown. | | Alert from X dismissed (any way) | Source freed; the gate re-opens. | | Alert with no sourceId | No dedup; queued normally. |

Cooldown — after a sourced alert is dismissed, cooldown keeps its gate closed for a while so flaky errors can't immediately re-nag:

// A dismissed "network-error" won't reappear for 30s, even after it closes.
await AlertQueue.show({
  title: 'Network error',
  sourceId: 'network-error',
  cooldown: 30_000,
});

Suppressed alerts (active-source dedup or cooldown) resolve { status: 'deduplicated' }. Set a fleet-wide default with configure({ defaultCooldown }).

// Fires repeatedly, but only one dialog ever appears:
await AlertQueue.show({
  title: 'Payment failed',
  message: 'Please check your card details.',
  sourceId: 'payment-error',
  priority: 5,
  onDeduplicated: (existingId) => console.log(`suppressed; active: ${existingId}`),
});

// Always show the latest instead:
await AlertQueue.show({
  title: 'Network error',
  message: `Retry attempt ${attempt}`,
  sourceId: 'network-error',
  sourcePolicy: 'replace',
});

if (AlertQueue.hasPendingSource('checkout-flow')) {
  await AlertQueue.dismissSource('checkout-flow');
}

Button actions

Run code when a specific button is tapped in one of two ways.

Per-button handler — fires on tap, on all platforms:

await AlertQueue.show({
  title: 'Delete file?',
  buttons: [
    { text: 'Cancel', role: 'cancel', handler: () => console.log('cancelled') },
    { text: 'Delete', role: 'destructive', handler: async () => { await deleteFile(); } },
  ],
});

Returning false from a handler keeps the dialog open (Ionic only — native dialogs are already closed by the time their result arrives).

Branch on the awaited result — platform-agnostic; inspect which button won:

const r = await AlertQueue.show({ title: 'Save changes?', buttons: [
  { text: 'Discard', role: 'cancel' },
  { text: 'Save', role: 'confirm' },
]});
// r.status -> 'confirmed' | 'cancelled' | 'dismissed' | 'timeout' | ...
// r.role   -> 'confirm' | 'cancel' | 'destructive'
// r.action -> tapped button text, e.g. 'Save'

Lifecycle hooks

For each alert that runs the full flow, hooks fire in this order — global first, then per-alert:

beforeShow → [present] → afterShow → [user acts / timeout] → beforeDismiss → afterDismiss → onDismiss / onQueueChange
  • afterShow fires once the dialog is actually visible (the adapter resolves present() on the visible signal, not on dismissal).
  • If beforeShow throws, the alert is cancelled, its show() promise rejects, and the queue advances to the next alert.
  • afterShow / beforeDismiss / afterDismiss are observe-only and error-isolated — a throwing hook can never deadlock the queue.

⚠️ Note: beforeDismiss cannot block a dismissal. By the time it runs the dialog has already closed (native dialogs especially are gone the moment the user acts). Use it for cleanup/telemetry, not veto logic.

AlertQueue.configure({
  defaultDismissible: true,
  defaultSourcePolicy: 'drop-new',
  maxQueue: 20,             // cap pending alerts; overflow: 'drop-new' (default) or 'drop-oldest'
  beforeShow: (config) => analytics.track('alert_shown', { title: config.title }),
  afterDismiss: (result) => analytics.track('alert_dismissed', { status: result.status }),
  onQueueChange: (length) => badgeService.update(length),
});

Custom UI — bring your own popup

The library is UI-agnostic: the queue, deduplication, priority, timeouts, and lifecycle hooks all live in a core that talks to a small adapter interface. The built-in adapters render with Ionic / Capacitor / Cordova, but you can plug in any popup UI (your own modal component, a design-system dialog, a toast, etc.) by implementing IAlertAdapter and registering it with useAdapter().

interface IAlertAdapter {
  // Resolve once YOUR popup is visible. Deliver the outcome via onResult.
  present(config: PreparedAlert, onResult: AlertResultCallback): Promise<AlertHandle>;
}

interface AlertHandle {
  id: string;
  canDismiss: boolean;       // can you close it programmatically?
  dismiss(): Promise<void>;  // close it (used by timeout / replace / dismiss*())
}

PreparedAlert is your AlertConfig with defaults already applied and an id assigned — read title, message, buttons, dismissible, cssClass, etc.

Three rules to honor

  1. Resolve present() when the popup is on screen (so afterShow and timeouts fire at the right moment).
  2. Call onResult exactly once with the outcome (confirmed / cancelled / dismissed / ...).
  3. dismiss() should close the popup and fire onResult so timeout, the replace policy, and dismiss*() work. If your UI can't be closed programmatically, set canDismiss: false and the queue adapts.

Example

import {
  AlertQueue,
  type IAlertAdapter,
  type AlertHandle,
  type PreparedAlert,
} from '@lucidaquarian/alert-queue';

const myAdapter: IAlertAdapter = {
  async present(config: PreparedAlert, onResult): Promise<AlertHandle> {
    const buttons = config.buttons ?? [{ text: 'OK', role: 'confirm' }];

    const modal = MyModal.open({
      title: config.title,
      message: config.message,
      buttons,
      onButton: (btn) =>
        onResult({
          status: btn.role === 'cancel' ? 'cancelled' : 'confirmed',
          action: btn.text,
          role: btn.role,
        }),
      onBackdrop: () => onResult({ status: 'dismissed' }),
    });

    // present() resolves when the modal is visible:
    return {
      id: config.id,
      canDismiss: true,
      dismiss: async () => {
        modal.close();
        onResult({ status: 'dismissed' });
      },
    };
  },
};

// Register BEFORE the first show() — this bypasses platform auto-detection.
AlertQueue.useAdapter(myAdapter);

// From here, every AlertQueue.show(...) renders your popup, fully queued,
// deduplicated, and lifecycle-managed.
await AlertQueue.show({ title: 'Saved', message: 'Your changes are live.' });

Usage with Angular

Import from the @lucidaquarian/alert-queue/angular subpath so non-Angular consumers never pull in @angular/core.

Standalone (Angular 15+):

import { bootstrapApplication } from '@angular/platform-browser';
import { provideAlertQueue } from '@lucidaquarian/alert-queue/angular';

bootstrapApplication(AppComponent, {
  providers: [
    provideAlertQueue({
      defaultDismissible: true,
      onQueueChange: (n) => console.log(`Queue length: ${n}`),
    }),
  ],
});

NgModule:

import { AlertQueueModule } from '@lucidaquarian/alert-queue/angular';

@NgModule({
  imports: [AlertQueueModule.forRoot({ defaultPriority: 0 })],
})
export class AppModule {}

Inject the service:

import { AlertQueueService } from '@lucidaquarian/alert-queue/angular';

@Component({ /* ... */ })
export class MyComponent {
  constructor(private alerts: AlertQueueService) {}

  async confirm() {
    const r = await this.alerts.show({ title: 'Delete?', buttons: [
      { text: 'Cancel', role: 'cancel' },
      { text: 'Delete', role: 'destructive' },
    ]});
    if (r.status === 'confirmed') { /* ... */ }
  }
}

Usage with Capacitor (no Angular)

import { AlertQueue } from '@lucidaquarian/alert-queue';
// Ensure @capacitor/core and @capacitor/dialog are installed.

await AlertQueue.show({
  title: 'Update available',
  message: 'A new version is ready.',
  buttons: [
    { text: 'Later', role: 'cancel' },
    { text: 'Update', role: 'confirm' },
  ],
});

On Capacitor web, isNativePlatform() is false, so the Ionic adapter is used (requires @ionic/core). On native iOS/Android the Capacitor Dialog plugin is used.


Usage with Cordova

Include the IIFE bundle and the dialogs plugin:

<script src="cordova.js"></script>
<script src="node_modules/@lucidaquarian/alert-queue/dist/umd/alert-queue.global.js"></script>
<script>
  document.addEventListener('deviceready', async () => {
    const result = await AlertQueueLib.AlertQueue.show({
      title: 'Confirm',
      message: 'Proceed?',
      buttons: [
        { text: 'No', role: 'cancel' },
        { text: 'Yes', role: 'confirm' },
      ],
    });
    console.log(result.status);
  });
</script>

Migration from ad-hoc AlertController

// Before — alerts can stack, duplicates appear, no serialization:
const alert = await this.alertController.create({
  header: 'Payment failed',
  message: 'Try again.',
  buttons: ['OK'],
});
await alert.present();

// After — queued, deduplicated, awaited:
const result = await AlertQueue.show({
  title: 'Payment failed',
  message: 'Try again.',
  sourceId: 'payment-error',
});

Map header → title, backdropDismiss → dismissible, and add a sourceId to gain deduplication for free.


Builds

| Output | Path | For | |---|---|---| | ESM | dist/index.mjs | Bundlers, Ionic/Angular/Capacitor | | CJS | dist/index.cjs | Node / CommonJS | | IIFE | dist/umd/alert-queue.global.js | Cordova / <script> (global AlertQueueLib) | | Types | dist/index.d.ts, dist/angular/index.d.ts | TypeScript |


Contributing

npm install
npm run typecheck       # tsc --noEmit
npm test                # vitest — 90 unit + integration tests
npm run test:coverage   # vitest + v8 coverage report (~93%)
npm run test:e2e        # Playwright — renders real Ionic dialogs in Chromium
npm run build           # tsup -> dist/

CI runs typecheck, test, build, and the Playwright e2e suite on every push and PR across Node 18 / 20 / 22. A full verification snapshot lives in docs/TEST-REPORT.md.

PRs welcome. Please keep the core framework-agnostic and add tests for new behaviour.


License

MIT © thelucidaquarian