import { DestroyRef, ENVIRONMENT_INITIALIZER, Injectable, inject, } from '@angular/core'; import { CanDeactivateFn } from '@angular/router'; /** * A source of debounced, not-yet-flushed writes (autosave). The two autosave owners in * this app have different lifetimes — root singleton stores (`BriefStore`, * `OrgTemplateStore`) and per-wizard `createDraftSync` controllers living inside child * organisms — so both register here instead of the guard/unload handler needing to know * which page or store owns the pending write. */ export interface PendingSave { /** True while a debounced edit hasn't been written to the backend yet. */ hasPendingSave(): boolean; /** Flush that pending write now and await it. No-op when nothing is pending. */ flushPending(): Promise; } /** Registry of every active autosave owner. The `CanDeactivate` guard and the `beforeunload` handler flush through this — one seam, both callers. */ @Injectable({ providedIn: 'root' }) export class PendingSaves { private readonly owners = new Set(); /** Register an owner; returns an unregister function. */ register(owner: PendingSave): () => void { this.owners.add(owner); return () => this.owners.delete(owner); } hasPending(): boolean { return [...this.owners].some((o) => o.hasPendingSave()); } /** Flush every owner that has a pending write, awaiting all. Best-effort: a rejected flush is swallowed (a failed autosave surfaces its own error state; navigation must not be blocked by it). */ async flushAll(): Promise { await Promise.allSettled( [...this.owners].filter((o) => o.hasPendingSave()).map((o) => o.flushPending()), ); } } /** Register the current injection context's owner for the life of its `DestroyRef`. Call from a constructor or field initializer (root store, or `createDraftSync`). */ export function registerPendingSave(owner: PendingSave): void { const unregister = inject(PendingSaves).register(owner); inject(DestroyRef).onDestroy(unregister); } /** `CanDeactivate` guard: flush any pending debounced write before an in-app route change, then allow navigation. Awaitable, so the write lands before the page tears down (which would otherwise drop a sub-debounce edit). We never block leaving — the flush is a guarantee of effort, not a gate. */ export const flushPendingGuard: CanDeactivateFn = () => { const pending = inject(PendingSaves); return pending.hasPending() ? pending.flushAll().then(() => true) : true; }; /** Wire a `beforeunload` handler that guards the last-mile save on a hard tab-close/reload. ponytail: the HTTP seam is Angular `HttpClient` (no `keepalive`/`sendBeacon`), so an async flush can't be guaranteed to finish as the page tears down — we fire it best-effort AND trigger the browser's native "unsaved changes" prompt, which lets the ~600ms debounce land if the user stays. Upgrade path: a `sendBeacon`/keepalive last-mile if this ever needs to be guaranteed. */ export function provideUnloadFlush() { return { provide: ENVIRONMENT_INITIALIZER, multi: true, useValue: () => { const pending = inject(PendingSaves); window.addEventListener('beforeunload', (e) => { if (!pending.hasPending()) return; void pending.flushAll(); e.preventDefault(); e.returnValue = ''; }); }, }; }