| name | notification-persistence |
| description | Stores notifications in localStorage with schema (id, taskId, message, timestamp, read, priority). Provides retrieval methods (getUnread, getAll, markAsRead) and maintains last 50 notifications with automatic cleanup for deleted tasks. |
Notification Persistence Skill
Overview
The notification persistence skill manages the lifecycle of notification data in browser localStorage. It provides CRUD operations, read/unread state management, and automatic cleanup.
When to Apply
Apply this skill:
- When a notification is triggered (persist immediately)
- When displaying notification history
- When user marks notification as read
- When user deletes a task (cleanup notifications)
- When retrieving unread count for badge display
- When app initializes (load existing notifications)
Storage Schema
Notification Object Structure
interface Notification {
id: string;
taskId: string;
message: string;
timestamp: number;
read: boolean;
priority: string;
}
Example Notification
{
id: "550e8400-e29b-41d4-a716-446655440000",
taskId: "task_123",
message: "Task \"Submit quarterly report\" is due in 3 hours and 45 minutes",
timestamp: 1702739700000,
read: false,
priority: "VERY IMPORTANT"
}
Storage Key
const STORAGE_KEY = 'todo-app-notifications';
Core Operations
1. Save Notification
function saveNotification(notification) {
try {
if (!notification.id) {
notification.id = generateUUID();
}
const notifications = getAllNotifications();
notifications.unshift(notification);
const limited = notifications.slice(0, 50);
localStorage.setItem(STORAGE_KEY, JSON.stringify(limited));
return notification;
} catch (error) {
console.error('Failed to save notification:', error);
return null;
}
}
2. Get All Notifications
function getAllNotifications() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return [];
const notifications = JSON.parse(stored);
return notifications.filter(n =>
n.id && n.taskId && n.message && n.timestamp
);
} catch (error) {
console.error('Failed to load notifications:', error);
return [];
}
}
3. Get Unread Notifications
function getUnreadNotifications() {
return getAllNotifications().filter(n => !n.read);
}
4. Mark as Read
function markAsRead(notificationId) {
try {
const notifications = getAllNotifications();
const updated = notifications.map(n =>
n.id === notificationId ? { ...n, read: true } : n
);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return true;
} catch (error) {
console.error('Failed to mark as read:', error);
return false;
}
}
5. Mark All as Read
function markAllAsRead() {
try {
const notifications = getAllNotifications();
const updated = notifications.map(n => ({ ...n, read: true }));
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return true;
} catch (error) {
console.error('Failed to mark all as read:', error);
return false;
}
}
6. Delete Notification
function deleteNotification(notificationId) {
try {
const notifications = getAllNotifications();
const filtered = notifications.filter(n => n.id !== notificationId);
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
return true;
} catch (error) {
console.error('Failed to delete notification:', error);
return false;
}
}
Notification History Retention
50-Notification Limit
Only the most recent 50 notifications are kept:
function enforceLimit() {
const notifications = getAllNotifications();
if (notifications.length > 50) {
const limited = notifications.slice(0, 50);
localStorage.setItem(STORAGE_KEY, JSON.stringify(limited));
}
}
Automatic Cleanup on Save
Task Deletion Cleanup
Remove Associated Notifications
When a task is deleted, remove all its notifications:
function cleanupTaskNotifications(taskId) {
try {
const notifications = getAllNotifications();
const filtered = notifications.filter(n => n.taskId !== taskId);
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
return {
success: true,
removed: notifications.length - filtered.length
};
} catch (error) {
console.error('Failed to cleanup notifications:', error);
return { success: false, removed: 0 };
}
}
Bulk Cleanup
Cleanup notifications for multiple deleted tasks:
function bulkCleanup(taskIds) {
try {
const notifications = getAllNotifications();
const taskIdSet = new Set(taskIds);
const filtered = notifications.filter(n => !taskIdSet.has(n.taskId));
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
return {
success: true,
removed: notifications.length - filtered.length
};
} catch (error) {
console.error('Failed to bulk cleanup:', error);
return { success: false, removed: 0 };
}
}
UUID Generation
function generateUUID() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
Complete Service Implementation
class NotificationPersistenceService {
constructor() {
this.storageKey = 'todo-app-notifications';
this.maxNotifications = 50;
}
save(notification) {
if (!notification.id) {
notification.id = generateUUID();
}
if (!notification.timestamp) {
notification.timestamp = Date.now();
}
if (notification.read === undefined) {
notification.read = false;
}
const notifications = this.getAll();
notifications.unshift(notification);
const limited = notifications.slice(0, this.maxNotifications);
this._write(limited);
return notification;
}
getAll() {
return this._read();
}
getUnread() {
return this._read().filter(n => !n.read);
}
getUnreadCount() {
return this.getUnread().length;
}
markAsRead(id) {
const notifications = this._read();
const updated = notifications.map(n =>
n.id === id ? { ...n, read: true } : n
);
this._write(updated);
}
markAllAsRead() {
const notifications = this._read();
const updated = notifications.map(n => ({ ...n, read: true }));
this._write(updated);
}
delete(id) {
const notifications = this._read();
const filtered = notifications.filter(n => n.id !== id);
this._write(filtered);
}
cleanupTask(taskId) {
const notifications = this._read();
const filtered = notifications.filter(n => n.taskId !== taskId);
const removed = notifications.length - filtered.length;
this._write(filtered);
return removed;
}
clear() {
this._write([]);
}
_read() {
try {
const stored = localStorage.getItem(this.storageKey);
if (!stored) return [];
return JSON.parse(stored);
} catch (error) {
console.error('Failed to read notifications:', error);
return [];
}
}
_write(notifications) {
try {
localStorage.setItem(this.storageKey, JSON.stringify(notifications));
} catch (error) {
console.error('Failed to write notifications:', error);
if (error.name === 'QuotaExceededError') {
console.warn('localStorage quota exceeded, clearing old notifications');
const limited = notifications.slice(0, 25);
localStorage.setItem(this.storageKey, JSON.stringify(limited));
}
}
}
}
export const notificationPersistence = new NotificationPersistenceService();
React Hook Usage
function useNotificationPersistence() {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
const loaded = notificationPersistence.getAll();
setNotifications(loaded);
setUnreadCount(notificationPersistence.getUnreadCount());
}, []);
const save = useCallback((notification) => {
const saved = notificationPersistence.save(notification);
setNotifications(notificationPersistence.getAll());
setUnreadCount(notificationPersistence.getUnreadCount());
return saved;
}, []);
const markAsRead = useCallback((id) => {
notificationPersistence.markAsRead(id);
setNotifications(notificationPersistence.getAll());
setUnreadCount(notificationPersistence.getUnreadCount());
}, []);
const markAllAsRead = useCallback(() => {
notificationPersistence.markAllAsRead();
setNotifications(notificationPersistence.getAll());
setUnreadCount(0);
}, []);
return {
notifications,
unreadCount,
save,
markAsRead,
markAllAsRead
};
}
Error Handling
localStorage Unavailable
function isLocalStorageAvailable() {
try {
const test = '__test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
let memoryStorage = {};
function safeGetItem(key) {
if (isLocalStorageAvailable()) {
return localStorage.getItem(key);
}
return memoryStorage[key] || null;
}
function safeSetItem(key, value) {
if (isLocalStorageAvailable()) {
localStorage.setItem(key, value);
} else {
memoryStorage[key] = value;
}
}
Quota Exceeded
function handleQuotaExceeded() {
const notifications = getAllNotifications();
const reduced = notifications.slice(0, 25);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(reduced));
} catch (error) {
localStorage.removeItem(STORAGE_KEY);
}
}
Testing Examples
const notification = {
taskId: 'task_1',
message: 'Test notification',
priority: 'VERY IMPORTANT'
};
const saved = notificationPersistence.save(notification);
console.log(saved.id);
const all = notificationPersistence.getAll();
console.log(all.length);
notificationPersistence.markAsRead(saved.id);
const unread = notificationPersistence.getUnread();
console.log(unread.length);
notificationPersistence.cleanupTask('task_1');
const remaining = notificationPersistence.getAll();
console.log(remaining.length);
for (let i = 0; i < 60; i++) {
notificationPersistence.save({
taskId: `task_${i}`,
message: `Notification ${i}`,
priority: 'VERY IMPORTANT'
});
}
const limited = notificationPersistence.getAll();
console.log(limited.length);
Integration Points
This skill integrates with:
- Notification Trigger Skill: Persists triggered notifications
- Notification UI Skill: Provides data for display
- Task Management: Cleans up on task deletion
- Notification Experience Agent: Coordinates persistence operations
Performance Considerations
- Read/write operations < 50ms for 50 notifications
- JSON serialization optimized
- Cleanup operations batched
- Memory-efficient (50-notification limit)