notification-persistence
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.
$ Installieren
git clone https://github.com/Syedaashnaghazanfar/full-stack-todo-app /tmp/full-stack-todo-app && cp -r /tmp/full-stack-todo-app/.claude/skills/notification-persistence ~/.claude/skills/full-stack-todo-app// tip: Run this command in your terminal to install the skill
SKILL.md
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; // UUID
taskId: string; // Associated task ID
message: string; // Notification message text
timestamp: number; // Unix timestamp (milliseconds)
read: boolean; // Read/unread state
priority: string; // "VERY IMPORTANT"
}
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, // Unix timestamp
read: false,
priority: "VERY IMPORTANT"
}
Storage Key
const STORAGE_KEY = 'todo-app-notifications';
Core Operations
1. Save Notification
function saveNotification(notification) {
try {
// Generate UUID if not provided
if (!notification.id) {
notification.id = generateUUID();
}
// Get existing notifications
const notifications = getAllNotifications();
// Add new notification
notifications.unshift(notification); // Add to beginning
// Keep only last 50
const limited = notifications.slice(0, 50);
// Save to localStorage
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);
// Ensure valid structure
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
// Limit is enforced automatically in saveNotification()
// Oldest notifications are automatically removed
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() {
// Use crypto.randomUUID if available
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback implementation
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 new notification
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;
}
// Get all notifications
getAll() {
return this._read();
}
// Get unread notifications
getUnread() {
return this._read().filter(n => !n.read);
}
// Get unread count
getUnreadCount() {
return this.getUnread().length;
}
// Mark notification as read
markAsRead(id) {
const notifications = this._read();
const updated = notifications.map(n =>
n.id === id ? { ...n, read: true } : n
);
this._write(updated);
}
// Mark all as read
markAllAsRead() {
const notifications = this._read();
const updated = notifications.map(n => ({ ...n, read: true }));
this._write(updated);
}
// Delete notification
delete(id) {
const notifications = this._read();
const filtered = notifications.filter(n => n.id !== id);
this._write(filtered);
}
// Cleanup notifications for deleted task
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 all notifications
clear() {
this._write([]);
}
// Internal read method
_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 [];
}
}
// Internal write method
_write(notifications) {
try {
localStorage.setItem(this.storageKey, JSON.stringify(notifications));
} catch (error) {
console.error('Failed to write notifications:', error);
// Handle quota exceeded
if (error.name === 'QuotaExceededError') {
console.warn('localStorage quota exceeded, clearing old notifications');
const limited = notifications.slice(0, 25); // Keep only 25
localStorage.setItem(this.storageKey, JSON.stringify(limited));
}
}
}
}
// Singleton instance
export const notificationPersistence = new NotificationPersistenceService();
React Hook Usage
function useNotificationPersistence() {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
// Load notifications on mount
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;
}
}
// Fallback to in-memory storage
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() {
// Reduce to 25 notifications
const notifications = getAllNotifications();
const reduced = notifications.slice(0, 25);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(reduced));
} catch (error) {
// If still failing, clear completely
localStorage.removeItem(STORAGE_KEY);
}
}
Testing Examples
// Test 1: Save notification
const notification = {
taskId: 'task_1',
message: 'Test notification',
priority: 'VERY IMPORTANT'
};
const saved = notificationPersistence.save(notification);
console.log(saved.id); // UUID generated
// Test 2: Get all
const all = notificationPersistence.getAll();
console.log(all.length); // 1
// Test 3: Mark as read
notificationPersistence.markAsRead(saved.id);
const unread = notificationPersistence.getUnread();
console.log(unread.length); // 0
// Test 4: Cleanup task
notificationPersistence.cleanupTask('task_1');
const remaining = notificationPersistence.getAll();
console.log(remaining.length); // 0
// Test 5: 50-notification limit
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); // 50 (not 60)
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)
Repository

Syedaashnaghazanfar
Author
Syedaashnaghazanfar/full-stack-todo-app/.claude/skills/notification-persistence
1
Stars
0
Forks
Updated55m ago
Added1w ago