{"_id":"@abineshsolairaj/alert-queue","name":"@abineshsolairaj/alert-queue","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@abineshsolairaj/alert-queue","version":"1.0.0","description":"Serialized alert queue with source deduplication for Ionic, Capacitor, and Cordova","license":"MIT","author":{"name":"thelucidaquarian"},"repository":{"type":"git","url":"git+https://github.com/thelucidaquarian/ionic-alert-queue.git"},"homepage":"https://github.com/thelucidaquarian/ionic-alert-queue#readme","bugs":{"url":"https://github.com/thelucidaquarian/ionic-alert-queue/issues"},"keywords":["ionic","alert","queue","capacitor","cordova","dialog","deduplication","alert-manager","ionic-alert"],"type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.ts","unpkg":"./dist/umd/alert-queue.global.js","jsdelivr":"./dist/umd/alert-queue.global.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"},"./angular":{"types":"./dist/angular/index.d.ts","import":"./dist/angular/index.mjs","require":"./dist/angular/index.cjs"},"./package.json":"./package.json"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","test:e2e":"playwright test","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"peerDependencies":{"@angular/core":">=14.0.0","@capacitor/core":">=5.0.0","@capacitor/dialog":">=5.0.0","@ionic/core":">=7.0.0"},"peerDependenciesMeta":{"@angular/core":{"optional":true},"@capacitor/core":{"optional":true},"@capacitor/dialog":{"optional":true},"@ionic/core":{"optional":true}},"devDependencies":{"@angular/core":"^17.3.0","@capacitor/core":"^6.2.1","@capacitor/dialog":"^6.0.3","@ionic/core":"^8.8.11","@playwright/test":"^1.61.1","@swc/core":"^1.15.43","@vitest/coverage-v8":"^1.6.1","rxjs":"^7.8.0","tslib":"^2.6.0","tsup":"^8.0.0","typescript":"^5.4.0","vitest":"^1.5.0","zone.js":"^0.14.0"},"engines":{"node":">=18"},"publishConfig":{"access":"public"},"gitHead":"01ca353f9a0fa73bba7d918817ee26d7ef03472a","_id":"@abineshsolairaj/alert-queue@1.0.0","_nodeVersion":"23.10.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-84QWuC11ziOsBs0mVU+YXTIXigvwH2n0493efAr2VVHHANBjK6mc+zRSAGnxnOGKP+C6eyd/f86mTA0CVKGslw==","shasum":"b50b4883b5bd4321a68dc7fd1a431e89fccdbdc1","tarball":"https://registry.npmjs.org/@abineshsolairaj/alert-queue/-/alert-queue-1.0.0.tgz","fileCount":18,"unpackedSize":468090,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDbuBbMN45VFN5TwULZap8Xp+FsKNyT+V5fJmPKaG0y5AiB4u3RlZMPGLP7J1m3iKodnxA4ZroYSatM9qmGCcwSdjA=="}]},"_npmUser":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"directories":{},"maintainers":[{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/alert-queue_1.0.0_1782268870926_0.9919827151749352"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-24T02:41:10.728Z","1.0.0":"2026-06-24T02:41:11.082Z","modified":"2026-06-24T02:41:11.353Z"},"maintainers":[{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"}],"description":"Serialized alert queue with source deduplication for Ionic, Capacitor, and Cordova","homepage":"https://github.com/thelucidaquarian/ionic-alert-queue#readme","keywords":["ionic","alert","queue","capacitor","cordova","dialog","deduplication","alert-manager","ionic-alert"],"repository":{"type":"git","url":"git+https://github.com/thelucidaquarian/ionic-alert-queue.git"},"author":{"name":"thelucidaquarian"},"bugs":{"url":"https://github.com/thelucidaquarian/ionic-alert-queue/issues"},"license":"MIT","readme":"# @abineshsolairaj/alert-queue\n\n> Serialized alert queue with source deduplication for Ionic, Capacitor, and Cordova.\n\nA lightweight, framework-agnostic **queue and lifecycle manager** for alert\ndialogs. It guarantees that **only one alert is visible at a time**, serializes\npending alerts by priority, and **deduplicates by source** so the same feature\nnever stacks duplicate dialogs.\n\nIt is *not* a UI library — rendering is fully delegated to the platform\n(`@ionic/core`'s `alertController`, Capacitor's `Dialog`, or Cordova's\n`navigator.notification`). The plugin only owns the queue, the state machine,\nand the lifecycle.\n\n- ✅ Framework-agnostic core (Ionic Angular / React / Vue, Capacitor, Cordova)\n- ✅ Priority queue + single-alert mutex\n- ✅ Source deduplication (`drop-new` / `replace`)\n- ✅ Per-alert and global lifecycle hooks\n- ✅ Auto timeouts, programmatic dismissal, source-scoped dismissal\n- ✅ Pluggable rendering — [bring your own popup UI](#custom-ui--bring-your-own-popup) via a tiny adapter\n- ✅ Full TypeScript types; ESM + CJS + IIFE builds\n- ✅ Zero runtime dependencies\n\n---\n\n## Installation\n\n```bash\nnpm install @abineshsolairaj/alert-queue\n```\n\nThen install whichever platform package you actually use (all are **optional**\npeer dependencies — install only what you need):\n\n```bash\n# Ionic (any framework)\nnpm install @ionic/core\n\n# Capacitor native dialogs\nnpm install @capacitor/core @capacitor/dialog\n\n# Cordova native dialogs\ncordova plugin add cordova-plugin-dialogs\n\n# Angular DI integration (optional)\nnpm install @angular/core\n```\n\n---\n\n## Quick start\n\n```ts\nimport { AlertQueue } from '@abineshsolairaj/alert-queue';\n\nconst result = await AlertQueue.show({\n  title: 'Session expired',\n  message: 'Please log in again.',\n  buttons: [\n    { text: 'Cancel', role: 'cancel' },\n    { text: 'Log in', role: 'confirm' },\n  ],\n});\n\nif (result.status === 'confirmed') {\n  router.navigate(['/login']);\n}\n```\n\nThe platform is auto-detected on first use — no configuration required.\n\n---\n\n## Screenshots\n\nReal Ionic dialogs rendered by the library (captured by the Playwright e2e\nsuite in `e2e/`):\n\n| Two-button alert | Destructive multi-button alert |\n|---|---|\n| ![Session expired alert](docs/screenshots/alert-basic.png) | ![Delete project alert](docs/screenshots/alert-destructive.png) |\n\n> The plugin manages the queue and lifecycle; the visual styling comes from the\n> platform (here, `@ionic/core` in Material mode).\n\n---\n\n## Platform support\n\n| Capability | Ionic (`@ionic/core`) | Capacitor (`@capacitor/dialog`) | Cordova (`cordova-plugin-dialogs`) |\n|---|:---:|:---:|:---:|\n| Show / queue / priority / dedup | ✅ | ✅ | ✅ |\n| Multi-button alerts | ✅ | ⚠️ confirm + cancel only | ✅ |\n| `cssClass`, backdrop dismiss | ✅ | ❌ (OS-styled) | ❌ (OS-styled) |\n| `confirmed` / `cancelled` results | ✅ | ✅ | ✅ |\n| `dismissed` (backdrop / programmatic) | ✅ | ❌ | ⚠️ back-button only |\n| **`timeout` auto-dismiss** | ✅ | ❌ | ❌ |\n| **`dismiss()` / `dismissAll()` / `dismissSource()` (active alert)** | ✅ | ❌ | ❌ |\n| **`replace` policy on the *active* alert** | ✅ | ❌ | ❌ |\n\n> **Why the ❌s on native?** Capacitor's `Dialog` and Cordova's\n> `navigator.notification` are **OS-managed** and expose no programmatic dismiss\n> API. So anything that needs to *remove an already-visible dialog* — `timeout`,\n> `replace` on the active alert, and the `dismiss*()` methods — cannot affect a\n> native dialog that is already on screen. Queued (not-yet-shown) alerts are\n> still fully controllable on every platform. `AlertHandle.canDismiss` reflects\n> this at runtime.\n\n---\n\n## API reference\n\nAll methods are static on `AlertQueue`.\n\n| Method | Signature | Description |\n|---|---|---|\n| `configure` | `(options: GlobalConfig) => void` | Set global defaults + hooks. Merges across calls (later wins). |\n| `init` | `() => void` | Auto-detect platform & cache the adapter. Called lazily on first `show()`. |\n| `show` | `(config: AlertConfig) => Promise<AlertResult>` | Enqueue an alert; resolves when it finally settles. |\n| `dismiss` | `(id?: string) => Promise<void>` | Dismiss a specific queued/active alert, or the current one. |\n| `dismissAll` | `() => Promise<void>` | Clear the queue and dismiss the active alert. |\n| `dismissSource` | `(sourceId: string) => Promise<void>` | Dismiss every alert from a source. |\n| `hasPendingSource` | `(sourceId: string) => boolean` | Whether a source is currently active or queued. |\n| `onDismiss` | `(handler: (r: AlertResult) => void) => () => void` | Subscribe to every dismissal. Returns an unsubscribe fn. |\n| `useAdapter` | `(adapter: IAlertAdapter) => void` | Replace the renderer with your own popup UI. See [Custom UI](#custom-ui--bring-your-own-popup). |\n| `reset` | `() => void` | *Testing*: clear all state. |\n\n### `AlertConfig`\n\n| Field | Type | Default | Notes |\n|---|---|---|---|\n| `title` | `string` | — | Required. |\n| `message` | `string` | — | |\n| `buttons` | `AlertButton[]` | `[{ text: 'OK', role: 'confirm' }]` | |\n| `priority` | `number` | `0` | Higher is shown sooner. |\n| `timeout` | `number` | — | Auto-dismiss after N ms (Ionic only). |\n| `dismissible` | `boolean` | `true` | Backdrop tap closes (Ionic only). |\n| `sourceId` | `string` | — | Enables deduplication. |\n| `sourcePolicy` | `'drop-new' \\| 'replace'` | `'drop-new'` | Behaviour when the source is already active. |\n| `cssClass` | `string \\| string[]` | — | Ionic only. |\n| `id` | `string` | auto UUID | |\n| `beforeShow` / `afterShow` | `(config) => void \\| Promise` | — | Per-alert hooks. |\n| `beforeDismiss` / `afterDismiss` | `(result) => void \\| Promise` | — | Per-alert hooks. |\n| `onDeduplicated` | `(existingId) => void` | — | Fires when suppressed by `drop-new`. |\n\n### `AlertResult`\n\n```ts\ninterface AlertResult {\n  status: 'confirmed' | 'cancelled' | 'timeout' | 'dismissed' | 'deduplicated';\n  action?: string;     // tapped button text, when known\n  role?: string;       // tapped button role, when known\n  existingId?: string; // only when status === 'deduplicated'\n}\n```\n\n---\n\n## Source deduplication\n\nEach alert may carry a `sourceId`. While an alert from that source is active or\nqueued, the gate is closed for that source.\n\n| Scenario | Behaviour |\n|---|---|\n| First alert from source `X` | Admitted; source registered. |\n| Second alert from `X` (`drop-new`, default) | Suppressed → resolves `{ status: 'deduplicated', existingId }`. |\n| Second alert from `X` (`replace`) | The existing alert is dismissed; the new one is shown. |\n| Alert from `X` dismissed (any way) | Source freed; the gate re-opens. |\n| Alert with no `sourceId` | No dedup; queued normally. |\n\n```ts\n// Fires repeatedly, but only one dialog ever appears:\nawait AlertQueue.show({\n  title: 'Payment failed',\n  message: 'Please check your card details.',\n  sourceId: 'payment-error',\n  priority: 5,\n  onDeduplicated: (existingId) => console.log(`suppressed; active: ${existingId}`),\n});\n\n// Always show the latest instead:\nawait AlertQueue.show({\n  title: 'Network error',\n  message: `Retry attempt ${attempt}`,\n  sourceId: 'network-error',\n  sourcePolicy: 'replace',\n});\n\nif (AlertQueue.hasPendingSource('checkout-flow')) {\n  await AlertQueue.dismissSource('checkout-flow');\n}\n```\n\n---\n\n## Lifecycle hooks\n\nFor each alert that runs the full flow, hooks fire in this order — **global\nfirst, then per-alert**:\n\n```\nbeforeShow → [present] → afterShow → [user acts / timeout] → beforeDismiss → afterDismiss → onDismiss / onQueueChange\n```\n\n- `afterShow` fires once the dialog is **actually visible** (the adapter resolves\n  `present()` on the visible signal, not on dismissal).\n- If `beforeShow` **throws**, the alert is cancelled, its `show()` promise\n  rejects, and the queue advances to the next alert.\n- `afterShow` / `beforeDismiss` / `afterDismiss` are **observe-only** and\n  error-isolated — a throwing hook can never deadlock the queue.\n\n> ⚠️ Note: `beforeDismiss` cannot *block* a dismissal. By the time it runs the\n> dialog has already closed (native dialogs especially are gone the moment the\n> user acts). Use it for cleanup/telemetry, not veto logic.\n\n```ts\nAlertQueue.configure({\n  defaultDismissible: true,\n  defaultSourcePolicy: 'drop-new',\n  beforeShow: (config) => analytics.track('alert_shown', { title: config.title }),\n  afterDismiss: (result) => analytics.track('alert_dismissed', { status: result.status }),\n  onQueueChange: (length) => badgeService.update(length),\n});\n```\n\n---\n\n## Custom UI — bring your own popup\n\nThe library is **UI-agnostic**: the queue, deduplication, priority, timeouts,\nand lifecycle hooks all live in a core that talks to a small adapter interface.\nThe built-in adapters render with Ionic / Capacitor / Cordova, but you can plug\nin **any** popup UI (your own modal component, a design-system dialog, a toast,\netc.) by implementing `IAlertAdapter` and registering it with `useAdapter()`.\n\n```ts\ninterface IAlertAdapter {\n  // Resolve once YOUR popup is visible. Deliver the outcome via onResult.\n  present(config: PreparedAlert, onResult: AlertResultCallback): Promise<AlertHandle>;\n}\n\ninterface AlertHandle {\n  id: string;\n  canDismiss: boolean;       // can you close it programmatically?\n  dismiss(): Promise<void>;  // close it (used by timeout / replace / dismiss*())\n}\n```\n\n`PreparedAlert` is your `AlertConfig` with defaults already applied and an `id`\nassigned — read `title`, `message`, `buttons`, `dismissible`, `cssClass`, etc.\n\n### Three rules to honor\n1. **Resolve `present()` when the popup is on screen** (so `afterShow` and\n   timeouts fire at the right moment).\n2. **Call `onResult` exactly once** with the outcome (`confirmed` / `cancelled`\n   / `dismissed` / ...).\n3. **`dismiss()` should close the popup and fire `onResult`** so `timeout`, the\n   `replace` policy, and `dismiss*()` work. If your UI can't be closed\n   programmatically, set `canDismiss: false` and the queue adapts.\n\n### Example\n\n```ts\nimport {\n  AlertQueue,\n  type IAlertAdapter,\n  type AlertHandle,\n  type PreparedAlert,\n} from '@abineshsolairaj/alert-queue';\n\nconst myAdapter: IAlertAdapter = {\n  async present(config: PreparedAlert, onResult): Promise<AlertHandle> {\n    const buttons = config.buttons ?? [{ text: 'OK', role: 'confirm' }];\n\n    const modal = MyModal.open({\n      title: config.title,\n      message: config.message,\n      buttons,\n      onButton: (btn) =>\n        onResult({\n          status: btn.role === 'cancel' ? 'cancelled' : 'confirmed',\n          action: btn.text,\n          role: btn.role,\n        }),\n      onBackdrop: () => onResult({ status: 'dismissed' }),\n    });\n\n    // present() resolves when the modal is visible:\n    return {\n      id: config.id,\n      canDismiss: true,\n      dismiss: async () => {\n        modal.close();\n        onResult({ status: 'dismissed' });\n      },\n    };\n  },\n};\n\n// Register BEFORE the first show() — this bypasses platform auto-detection.\nAlertQueue.useAdapter(myAdapter);\n\n// From here, every AlertQueue.show(...) renders your popup, fully queued,\n// deduplicated, and lifecycle-managed.\nawait AlertQueue.show({ title: 'Saved', message: 'Your changes are live.' });\n```\n\n---\n\n## Usage with Angular\n\nImport from the `@abineshsolairaj/alert-queue/angular` subpath so non-Angular consumers\nnever pull in `@angular/core`.\n\n**Standalone (Angular 15+):**\n\n```ts\nimport { bootstrapApplication } from '@angular/platform-browser';\nimport { provideAlertQueue } from '@abineshsolairaj/alert-queue/angular';\n\nbootstrapApplication(AppComponent, {\n  providers: [\n    provideAlertQueue({\n      defaultDismissible: true,\n      onQueueChange: (n) => console.log(`Queue length: ${n}`),\n    }),\n  ],\n});\n```\n\n**NgModule:**\n\n```ts\nimport { AlertQueueModule } from '@abineshsolairaj/alert-queue/angular';\n\n@NgModule({\n  imports: [AlertQueueModule.forRoot({ defaultPriority: 0 })],\n})\nexport class AppModule {}\n```\n\n**Inject the service:**\n\n```ts\nimport { AlertQueueService } from '@abineshsolairaj/alert-queue/angular';\n\n@Component({ /* ... */ })\nexport class MyComponent {\n  constructor(private alerts: AlertQueueService) {}\n\n  async confirm() {\n    const r = await this.alerts.show({ title: 'Delete?', buttons: [\n      { text: 'Cancel', role: 'cancel' },\n      { text: 'Delete', role: 'destructive' },\n    ]});\n    if (r.status === 'confirmed') { /* ... */ }\n  }\n}\n```\n\n---\n\n## Usage with Capacitor (no Angular)\n\n```ts\nimport { AlertQueue } from '@abineshsolairaj/alert-queue';\n// Ensure @capacitor/core and @capacitor/dialog are installed.\n\nawait AlertQueue.show({\n  title: 'Update available',\n  message: 'A new version is ready.',\n  buttons: [\n    { text: 'Later', role: 'cancel' },\n    { text: 'Update', role: 'confirm' },\n  ],\n});\n```\n\nOn Capacitor **web**, `isNativePlatform()` is `false`, so the Ionic adapter is\nused (requires `@ionic/core`). On native iOS/Android the Capacitor `Dialog`\nplugin is used.\n\n---\n\n## Usage with Cordova\n\nInclude the IIFE bundle and the dialogs plugin:\n\n```html\n<script src=\"cordova.js\"></script>\n<script src=\"node_modules/@abineshsolairaj/alert-queue/dist/umd/alert-queue.global.js\"></script>\n<script>\n  document.addEventListener('deviceready', async () => {\n    const result = await AlertQueueLib.AlertQueue.show({\n      title: 'Confirm',\n      message: 'Proceed?',\n      buttons: [\n        { text: 'No', role: 'cancel' },\n        { text: 'Yes', role: 'confirm' },\n      ],\n    });\n    console.log(result.status);\n  });\n</script>\n```\n\n---\n\n## Migration from ad-hoc `AlertController`\n\n```ts\n// Before — alerts can stack, duplicates appear, no serialization:\nconst alert = await this.alertController.create({\n  header: 'Payment failed',\n  message: 'Try again.',\n  buttons: ['OK'],\n});\nawait alert.present();\n\n// After — queued, deduplicated, awaited:\nconst result = await AlertQueue.show({\n  title: 'Payment failed',\n  message: 'Try again.',\n  sourceId: 'payment-error',\n});\n```\n\nMap `header → title`, `backdropDismiss → dismissible`, and add a `sourceId`\nto gain deduplication for free.\n\n---\n\n## Builds\n\n| Output | Path | For |\n|---|---|---|\n| ESM | `dist/index.mjs` | Bundlers, Ionic/Angular/Capacitor |\n| CJS | `dist/index.cjs` | Node / CommonJS |\n| IIFE | `dist/umd/alert-queue.global.js` | Cordova / `<script>` (global `AlertQueueLib`) |\n| Types | `dist/index.d.ts`, `dist/angular/index.d.ts` | TypeScript |\n\n---\n\n## Contributing\n\n```bash\nnpm install\nnpm run typecheck       # tsc --noEmit\nnpm test                # vitest (58 tests)\nnpm run test:coverage   # vitest + v8 coverage report (~92%)\nnpm run build           # tsup -> dist/\n```\n\nCI runs `typecheck`, `test`, and `build` on every push and PR across Node\n18 / 20 / 22.\n\nPRs welcome. Please keep the core framework-agnostic and add tests for new\nbehaviour.\n\n---\n\n## License\n\n[MIT](./LICENSE) © thelucidaquarian\n","readmeFilename":"README.md","_rev":"1-0031957642c429bfba57b9d0b4f590a1"}