Browse Source
- Remove IndexedDB storage implementation (~90 lines) - Remove 'web' platform from type definitions - Remove web platform registration from plugin - Update storage factory to exclude web platform - Remove web-specific SSR safety checks from vite-plugin - Delete web implementation files (src/web/, www/) BREAKING CHANGE: Web (PWA) platform support removed. Plugin now supports Android, iOS, and Electron platforms only.master
7 changed files with 553 additions and 609 deletions
@ -0,0 +1,330 @@ |
|||||
|
/** |
||||
|
* TimeSafari Storage Adapter |
||||
|
* |
||||
|
* Implements storage adapter pattern for TimeSafari database integration. |
||||
|
* Supports SQLite (native) and fallback storage. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { TimeSafariStorageAdapter } from './timesafari-integration'; |
||||
|
|
||||
|
/** |
||||
|
* TimeSafari Storage Adapter Implementation |
||||
|
* |
||||
|
* Provides unified storage interface across Android (SQLite) and iOS (SQLite) |
||||
|
* platforms with TimeSafari-specific patterns. |
||||
|
*/ |
||||
|
export class TimeSafariStorageAdapterImpl implements TimeSafariStorageAdapter { |
||||
|
private storage: StorageInterface; |
||||
|
private prefix: string; |
||||
|
private ttlManager: TTLManager; |
||||
|
|
||||
|
constructor(storage: StorageInterface, prefix = 'timesafari_notifications') { |
||||
|
this.storage = storage; |
||||
|
this.prefix = prefix; |
||||
|
this.ttlManager = new TTLManager(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Store data with TTL support |
||||
|
* |
||||
|
* @param key - Storage key |
||||
|
* @param value - Value to store |
||||
|
* @param ttlSeconds - Optional TTL in seconds |
||||
|
*/ |
||||
|
async store(key: string, value: unknown, ttlSeconds?: number): Promise<void> { |
||||
|
try { |
||||
|
const prefixedKey = this.getPrefixedKey(key); |
||||
|
const storageValue = { |
||||
|
data: value, |
||||
|
timestamp: Date.now(), |
||||
|
ttl: ttlSeconds ? Date.now() + (ttlSeconds * 1000) : null |
||||
|
}; |
||||
|
|
||||
|
await this.storage.setItem(prefixedKey, JSON.stringify(storageValue)); |
||||
|
|
||||
|
// Register TTL if provided
|
||||
|
if (ttlSeconds) { |
||||
|
this.ttlManager.registerTTL(prefixedKey, ttlSeconds); |
||||
|
} |
||||
|
|
||||
|
// Successfully stored key
|
||||
|
} catch (error) { |
||||
|
throw new Error(`Failed to store key: ${key}`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Retrieve data with TTL validation |
||||
|
* |
||||
|
* @param key - Storage key |
||||
|
* @returns Stored value or null if not found/expired |
||||
|
*/ |
||||
|
async retrieve(key: string): Promise<unknown> { |
||||
|
try { |
||||
|
const prefixedKey = this.getPrefixedKey(key); |
||||
|
const stored = await this.storage.getItem(prefixedKey); |
||||
|
|
||||
|
if (!stored) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const storageValue = JSON.parse(stored); |
||||
|
|
||||
|
// Check TTL
|
||||
|
if (storageValue.ttl && Date.now() > storageValue.ttl) { |
||||
|
// Key expired, cleaning up
|
||||
|
await this.remove(key); // Clean up expired data
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Successfully retrieved key
|
||||
|
return storageValue.data; |
||||
|
} catch (error) { |
||||
|
// Failed to retrieve key, returning null
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Remove data from storage |
||||
|
* |
||||
|
* @param key - Storage key |
||||
|
*/ |
||||
|
async remove(key: string): Promise<void> { |
||||
|
try { |
||||
|
const prefixedKey = this.getPrefixedKey(key); |
||||
|
await this.storage.removeItem(prefixedKey); |
||||
|
this.ttlManager.unregisterTTL(prefixedKey); |
||||
|
|
||||
|
// Successfully removed key
|
||||
|
} catch (error) { |
||||
|
throw new Error(`Failed to remove key: ${key}`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clear all data with prefix |
||||
|
*/ |
||||
|
async clear(): Promise<void> { |
||||
|
try { |
||||
|
const keys = await this.storage.getAllKeys(); |
||||
|
const prefixedKeys = keys.filter(key => key.startsWith(this.prefix)); |
||||
|
|
||||
|
for (const key of prefixedKeys) { |
||||
|
await this.storage.removeItem(key); |
||||
|
this.ttlManager.unregisterTTL(key); |
||||
|
} |
||||
|
|
||||
|
// Successfully cleared storage
|
||||
|
} catch (error) { |
||||
|
throw new Error('Failed to clear storage'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get prefixed key |
||||
|
* |
||||
|
* @param key - Original key |
||||
|
* @returns Prefixed key |
||||
|
*/ |
||||
|
private getPrefixedKey(key: string): string { |
||||
|
return `${this.prefix}_${key}`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clean up expired data |
||||
|
*/ |
||||
|
async cleanupExpired(): Promise<void> { |
||||
|
try { |
||||
|
const keys = await this.storage.getAllKeys(); |
||||
|
const prefixedKeys = keys.filter(key => key.startsWith(this.prefix)); |
||||
|
|
||||
|
for (const key of prefixedKeys) { |
||||
|
const stored = await this.storage.getItem(key); |
||||
|
if (stored) { |
||||
|
const storageValue = JSON.parse(stored); |
||||
|
if (storageValue.ttl && Date.now() > storageValue.ttl) { |
||||
|
await this.storage.removeItem(key); |
||||
|
this.ttlManager.unregisterTTL(key); |
||||
|
// Cleaned up expired key
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch (error) { |
||||
|
// Failed to cleanup expired data, continuing
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get storage statistics |
||||
|
* |
||||
|
* @returns Storage statistics |
||||
|
*/ |
||||
|
async getStats(): Promise<StorageStats> { |
||||
|
try { |
||||
|
const keys = await this.storage.getAllKeys(); |
||||
|
const prefixedKeys = keys.filter(key => key.startsWith(this.prefix)); |
||||
|
|
||||
|
let totalSize = 0; |
||||
|
let expiredCount = 0; |
||||
|
let validCount = 0; |
||||
|
|
||||
|
for (const key of prefixedKeys) { |
||||
|
const stored = await this.storage.getItem(key); |
||||
|
if (stored) { |
||||
|
totalSize += stored.length; |
||||
|
const storageValue = JSON.parse(stored); |
||||
|
if (storageValue.ttl && Date.now() > storageValue.ttl) { |
||||
|
expiredCount++; |
||||
|
} else { |
||||
|
validCount++; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
totalKeys: prefixedKeys.length, |
||||
|
validKeys: validCount, |
||||
|
expiredKeys: expiredCount, |
||||
|
totalSizeBytes: totalSize, |
||||
|
prefix: this.prefix |
||||
|
}; |
||||
|
} catch (error) { |
||||
|
// Failed to get stats, returning default
|
||||
|
return { |
||||
|
totalKeys: 0, |
||||
|
validKeys: 0, |
||||
|
expiredKeys: 0, |
||||
|
totalSizeBytes: 0, |
||||
|
prefix: this.prefix |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Storage Interface |
||||
|
*/ |
||||
|
export interface StorageInterface { |
||||
|
getItem(key: string): Promise<string | null>; |
||||
|
setItem(key: string, value: string): Promise<void>; |
||||
|
removeItem(key: string): Promise<void>; |
||||
|
getAllKeys(): Promise<string[]>; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TTL Manager for managing time-to-live data |
||||
|
*/ |
||||
|
class TTLManager { |
||||
|
private ttlMap: Map<string, NodeJS.Timeout> = new Map(); |
||||
|
|
||||
|
/** |
||||
|
* Register TTL for a key |
||||
|
* |
||||
|
* @param key - Storage key |
||||
|
* @param ttlSeconds - TTL in seconds |
||||
|
*/ |
||||
|
registerTTL(key: string, ttlSeconds: number): void { |
||||
|
// Clear existing TTL if any
|
||||
|
this.unregisterTTL(key); |
||||
|
|
||||
|
// Set new TTL
|
||||
|
const timeout = setTimeout(() => { |
||||
|
// TTL expired for key
|
||||
|
this.ttlMap.delete(key); |
||||
|
}, ttlSeconds * 1000); |
||||
|
|
||||
|
this.ttlMap.set(key, timeout); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Unregister TTL for a key |
||||
|
* |
||||
|
* @param key - Storage key |
||||
|
*/ |
||||
|
unregisterTTL(key: string): void { |
||||
|
const timeout = this.ttlMap.get(key); |
||||
|
if (timeout) { |
||||
|
clearTimeout(timeout); |
||||
|
this.ttlMap.delete(key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clear all TTLs |
||||
|
*/ |
||||
|
clearAll(): void { |
||||
|
for (const timeout of this.ttlMap.values()) { |
||||
|
clearTimeout(timeout); |
||||
|
} |
||||
|
this.ttlMap.clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Storage Statistics |
||||
|
*/ |
||||
|
export interface StorageStats { |
||||
|
totalKeys: number; |
||||
|
validKeys: number; |
||||
|
expiredKeys: number; |
||||
|
totalSizeBytes: number; |
||||
|
prefix: string; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* LocalStorage Storage Implementation for Web (fallback) |
||||
|
*/ |
||||
|
export class LocalStorageStorage implements StorageInterface { |
||||
|
async getItem(key: string): Promise<string | null> { |
||||
|
return localStorage.getItem(key); |
||||
|
} |
||||
|
|
||||
|
async setItem(key: string, value: string): Promise<void> { |
||||
|
localStorage.setItem(key, value); |
||||
|
} |
||||
|
|
||||
|
async removeItem(key: string): Promise<void> { |
||||
|
localStorage.removeItem(key); |
||||
|
} |
||||
|
|
||||
|
async getAllKeys(): Promise<string[]> { |
||||
|
const keys: string[] = []; |
||||
|
for (let i = 0; i < localStorage.length; i++) { |
||||
|
const key = localStorage.key(i); |
||||
|
if (key) keys.push(key); |
||||
|
} |
||||
|
return keys; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Storage Factory |
||||
|
*/ |
||||
|
export class StorageFactory { |
||||
|
/** |
||||
|
* Create appropriate storage implementation |
||||
|
* |
||||
|
* @param platform - Platform type |
||||
|
* @returns Storage implementation |
||||
|
*/ |
||||
|
static createStorage(platform: 'android' | 'ios' | 'electron'): StorageInterface { |
||||
|
switch (platform) { |
||||
|
case 'android': |
||||
|
case 'ios': |
||||
|
// For native platforms, this would integrate with SQLite
|
||||
|
// For now, return localStorage as placeholder
|
||||
|
return new LocalStorageStorage(); |
||||
|
case 'electron': |
||||
|
// Electron can use either SQLite or localStorage
|
||||
|
// For now, return localStorage as placeholder
|
||||
|
return new LocalStorageStorage(); |
||||
|
default: |
||||
|
throw new Error(`Unsupported platform: ${platform}`); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,207 @@ |
|||||
|
/** |
||||
|
* Vite Plugin for TimeSafari Daily Notification Plugin |
||||
|
* |
||||
|
* Provides Vite-specific optimizations and integrations for the TimeSafari PWA. |
||||
|
* Handles tree-shaking, SSR safety, and platform-specific builds. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import type { Plugin } from 'vite'; |
||||
|
|
||||
|
export interface TimeSafariPluginOptions { |
||||
|
/** |
||||
|
* Enable SSR safety checks |
||||
|
*/ |
||||
|
ssrSafe?: boolean; |
||||
|
|
||||
|
/** |
||||
|
* Enable tree-shaking optimizations |
||||
|
*/ |
||||
|
treeShaking?: boolean; |
||||
|
|
||||
|
/** |
||||
|
* Platform-specific builds |
||||
|
*/ |
||||
|
platforms?: ('android' | 'ios' | 'electron')[]; |
||||
|
|
||||
|
/** |
||||
|
* Bundle size budget in KB |
||||
|
*/ |
||||
|
bundleSizeBudget?: number; |
||||
|
|
||||
|
/** |
||||
|
* Enable development mode optimizations |
||||
|
*/ |
||||
|
devMode?: boolean; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TimeSafari Vite Plugin |
||||
|
* |
||||
|
* Provides optimizations and integrations for TimeSafari PWA compatibility. |
||||
|
*/ |
||||
|
export function timeSafariPlugin(options: TimeSafariPluginOptions = {}): Plugin { |
||||
|
const { |
||||
|
ssrSafe = true, |
||||
|
treeShaking = true, |
||||
|
platforms = ['android', 'ios'], |
||||
|
bundleSizeBudget = 35, |
||||
|
devMode = false |
||||
|
} = options; |
||||
|
|
||||
|
return { |
||||
|
name: 'timesafari-daily-notification', |
||||
|
|
||||
|
// Plugin configuration
|
||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
config(config, { command }): any { |
||||
|
const isDev = command === 'serve'; |
||||
|
|
||||
|
return { |
||||
|
// Build optimizations
|
||||
|
build: { |
||||
|
...config.build, |
||||
|
// Enable tree-shaking
|
||||
|
rollupOptions: { |
||||
|
...config.build?.rollupOptions, |
||||
|
treeshake: treeShaking ? { |
||||
|
moduleSideEffects: false, |
||||
|
propertyReadSideEffects: false, |
||||
|
unknownGlobalSideEffects: false |
||||
|
} : false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// Define constants
|
||||
|
define: { |
||||
|
...config.define, |
||||
|
__TIMESAFARI_SSR_SAFE__: ssrSafe, |
||||
|
__TIMESAFARI_PLATFORMS__: JSON.stringify(platforms), |
||||
|
__TIMESAFARI_BUNDLE_BUDGET__: bundleSizeBudget, |
||||
|
__TIMESAFARI_DEV_MODE__: devMode || isDev |
||||
|
} |
||||
|
}; |
||||
|
}, |
||||
|
|
||||
|
// Transform code for SSR safety
|
||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
transform(code, _id): any { |
||||
|
if (!ssrSafe) return null; |
||||
|
|
||||
|
// Check for SSR-unsafe code patterns
|
||||
|
const ssrUnsafePatterns = [ |
||||
|
/window\./g, |
||||
|
/document\./g, |
||||
|
/navigator\./g, |
||||
|
/localStorage\./g, |
||||
|
/sessionStorage\./g, |
||||
|
]; |
||||
|
|
||||
|
let hasUnsafeCode = false; |
||||
|
for (const pattern of ssrUnsafePatterns) { |
||||
|
if (pattern.test(code)) { |
||||
|
hasUnsafeCode = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (hasUnsafeCode) { |
||||
|
// Wrap unsafe code in platform checks
|
||||
|
const wrappedCode = ` |
||||
|
// SSR-safe wrapper for TimeSafari Daily Notification Plugin
|
||||
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') { |
||||
|
${code} |
||||
|
} else { |
||||
|
// SSR fallback - return mock implementation
|
||||
|
// Running in SSR environment, using fallback implementation
|
||||
|
} |
||||
|
`;
|
||||
|
|
||||
|
return { |
||||
|
code: wrappedCode, |
||||
|
map: null |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
}, |
||||
|
|
||||
|
// Generate platform-specific builds
|
||||
|
generateBundle(_options, bundle): void { |
||||
|
// Remove any web-specific code (not applicable in native-first architecture)
|
||||
|
Object.keys(bundle).forEach(fileName => { |
||||
|
if (fileName.includes('web') || fileName.includes('browser')) { |
||||
|
delete bundle[fileName]; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (!platforms.includes('android')) { |
||||
|
// Remove Android-specific code
|
||||
|
Object.keys(bundle).forEach(fileName => { |
||||
|
if (fileName.includes('android')) { |
||||
|
delete bundle[fileName]; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (!platforms.includes('ios')) { |
||||
|
// Remove iOS-specific code
|
||||
|
Object.keys(bundle).forEach(fileName => { |
||||
|
if (fileName.includes('ios')) { |
||||
|
delete bundle[fileName]; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// Bundle size analysis
|
||||
|
writeBundle(_options, bundle): void { |
||||
|
if (devMode) return; |
||||
|
|
||||
|
let totalSize = 0; |
||||
|
const fileSizes: Record<string, number> = {}; |
||||
|
|
||||
|
Object.entries(bundle).forEach(([fileName, chunk]) => { |
||||
|
if (chunk.type === 'chunk') { |
||||
|
const size = Buffer.byteLength(chunk.code, 'utf8'); |
||||
|
fileSizes[fileName] = size; |
||||
|
totalSize += size; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const totalSizeKB = totalSize / 1024; |
||||
|
|
||||
|
if (totalSizeKB > bundleSizeBudget) { |
||||
|
// Bundle size exceeds budget
|
||||
|
|
||||
|
// Log largest files for debugging (available in fileSizes)
|
||||
|
} else { |
||||
|
// Bundle size within budget
|
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// Development server middleware
|
||||
|
configureServer(server): void { |
||||
|
if (!devMode) return; |
||||
|
|
||||
|
// Add development-specific middleware
|
||||
|
server.middlewares.use('/timesafari-plugin', (_req, res, _next) => { |
||||
|
res.setHeader('Content-Type', 'application/json'); |
||||
|
res.end(JSON.stringify({ |
||||
|
name: 'TimeSafari Daily Notification Plugin', |
||||
|
version: process.env.npm_package_version || '1.0.0', |
||||
|
platforms: platforms, |
||||
|
ssrSafe: ssrSafe, |
||||
|
treeShaking: treeShaking |
||||
|
})); |
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Default export for easy usage |
||||
|
*/ |
||||
|
export default timeSafariPlugin; |
@ -1,601 +0,0 @@ |
|||||
/** |
|
||||
* DailyNotification Web Implementation |
|
||||
* |
|
||||
* Web platform implementation with proper mock functionality |
|
||||
* Aligned with updated interface definitions |
|
||||
* |
|
||||
* @author Matthew Raymer |
|
||||
* @version 2.0.0 |
|
||||
*/ |
|
||||
|
|
||||
import { |
|
||||
ContentFetchConfig, |
|
||||
UserNotificationConfig, |
|
||||
DualScheduleConfiguration, |
|
||||
DualScheduleStatus, |
|
||||
ContentFetchResult, |
|
||||
DailyReminderOptions, |
|
||||
DailyReminderInfo |
|
||||
} from '../definitions'; |
|
||||
|
|
||||
import { DailyNotificationPlugin, NotificationOptions, NotificationResponse, NotificationStatus, NotificationSettings, BatteryStatus, PowerState, PermissionStatus } from '../definitions'; |
|
||||
|
|
||||
export class DailyNotificationWeb implements DailyNotificationPlugin { |
|
||||
private notifications: Map<string, NotificationResponse> = new Map(); |
|
||||
private settings: NotificationSettings = { |
|
||||
sound: true, |
|
||||
priority: 'default', |
|
||||
timezone: 'UTC' |
|
||||
}; |
|
||||
private scheduledNotifications: Set<string> = new Set(); |
|
||||
private activeDid?: string; // Stored for future use in web implementation
|
|
||||
|
|
||||
getCurrentActiveDid(): string | undefined { |
|
||||
return this.activeDid; |
|
||||
} |
|
||||
|
|
||||
async configure(_options: Record<string, unknown>): Promise<void> { |
|
||||
// Web implementation placeholder
|
|
||||
// Configuration applied for web platform
|
|
||||
} |
|
||||
|
|
||||
async maintainRollingWindow(): Promise<void> { |
|
||||
// Rolling window maintenance for web platform
|
|
||||
} |
|
||||
|
|
||||
async getRollingWindowStats(): Promise<{ |
|
||||
stats: string; |
|
||||
maintenanceNeeded: boolean; |
|
||||
timeUntilNextMaintenance: number; |
|
||||
}> { |
|
||||
// Get rolling window stats for web platform
|
|
||||
return { |
|
||||
stats: 'Web platform - rolling window not applicable', |
|
||||
maintenanceNeeded: false, |
|
||||
timeUntilNextMaintenance: 0 |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
async getExactAlarmStatus(): Promise<{ |
|
||||
supported: boolean; |
|
||||
enabled: boolean; |
|
||||
canSchedule: boolean; |
|
||||
fallbackWindow: string; |
|
||||
}> { |
|
||||
// Get exact alarm status for web platform
|
|
||||
return { |
|
||||
supported: false, |
|
||||
enabled: false, |
|
||||
canSchedule: false, |
|
||||
fallbackWindow: 'Not applicable on web' |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
async requestExactAlarmPermission(): Promise<void> { |
|
||||
// Request exact alarm permission called on web platform
|
|
||||
} |
|
||||
|
|
||||
async openExactAlarmSettings(): Promise<void> { |
|
||||
// Open exact alarm settings called on web platform
|
|
||||
} |
|
||||
|
|
||||
async getRebootRecoveryStatus(): Promise<{ |
|
||||
inProgress: boolean; |
|
||||
lastRecoveryTime: number; |
|
||||
timeSinceLastRecovery: number; |
|
||||
recoveryNeeded: boolean; |
|
||||
}> { |
|
||||
// Get reboot recovery status called on web platform
|
|
||||
return { |
|
||||
inProgress: false, |
|
||||
lastRecoveryTime: 0, |
|
||||
timeSinceLastRecovery: 0, |
|
||||
recoveryNeeded: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Schedule a daily notification |
|
||||
*/ |
|
||||
async scheduleDailyNotification(options: NotificationOptions): Promise<void> { |
|
||||
// Validate required parameters
|
|
||||
if (!options.time) { |
|
||||
throw new Error('Time parameter is required'); |
|
||||
} |
|
||||
|
|
||||
// Create notification content
|
|
||||
const notification: NotificationResponse = { |
|
||||
id: this.generateId(), |
|
||||
title: options.title || 'Daily Update', |
|
||||
body: options.body || 'Your daily notification is ready', |
|
||||
timestamp: Date.now(), |
|
||||
url: options.url |
|
||||
}; |
|
||||
|
|
||||
// Store notification
|
|
||||
this.notifications.set(notification.id, notification); |
|
||||
this.scheduledNotifications.add(notification.id); |
|
||||
|
|
||||
// Schedule the notification using browser APIs if available
|
|
||||
if ('Notification' in window && 'serviceWorker' in navigator) { |
|
||||
await this.scheduleBrowserNotification(notification, options); |
|
||||
} |
|
||||
|
|
||||
// Web notification scheduled
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get the last notification |
|
||||
*/ |
|
||||
async getLastNotification(): Promise<NotificationResponse | null> { |
|
||||
const notifications = Array.from(this.notifications.values()); |
|
||||
if (notifications.length === 0) { |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
// Return the most recent notification
|
|
||||
return notifications.sort((a, b) => b.timestamp - a.timestamp)[0]; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Cancel all notifications |
|
||||
*/ |
|
||||
async cancelAllNotifications(): Promise<void> { |
|
||||
this.scheduledNotifications.clear(); |
|
||||
this.notifications.clear(); |
|
||||
|
|
||||
// Cancel browser notifications if available
|
|
||||
if ('Notification' in window) { |
|
||||
Notification.requestPermission().then(permission => { |
|
||||
if (permission === 'granted') { |
|
||||
// Clear any existing browser notifications
|
|
||||
// Browser notifications cleared
|
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get notification status |
|
||||
*/ |
|
||||
async getNotificationStatus(): Promise<NotificationStatus> { |
|
||||
return { |
|
||||
isEnabled: 'Notification' in window, |
|
||||
isScheduled: this.scheduledNotifications.size > 0, |
|
||||
lastNotificationTime: this.getLastNotificationTime(), |
|
||||
nextNotificationTime: this.getNextNotificationTime(), |
|
||||
pending: this.scheduledNotifications.size, |
|
||||
settings: this.settings, |
|
||||
error: undefined |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Update notification settings |
|
||||
*/ |
|
||||
async updateSettings(settings: NotificationSettings): Promise<void> { |
|
||||
this.settings = { ...this.settings, ...settings }; |
|
||||
// Settings updated
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get battery status (mock implementation for web) |
|
||||
*/ |
|
||||
async getBatteryStatus(): Promise<BatteryStatus> { |
|
||||
// Mock implementation for web
|
|
||||
return { |
|
||||
level: 100, |
|
||||
isCharging: false, |
|
||||
powerState: 0, |
|
||||
isOptimizationExempt: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Request battery optimization exemption (mock for web) |
|
||||
*/ |
|
||||
async requestBatteryOptimizationExemption(): Promise<void> { |
|
||||
// Battery optimization exemption requested (web mock)
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Set adaptive scheduling (mock for web) |
|
||||
*/ |
|
||||
async setAdaptiveScheduling(_options: { enabled: boolean }): Promise<void> { |
|
||||
// Adaptive scheduling set
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get power state (mock for web) |
|
||||
*/ |
|
||||
async getPowerState(): Promise<PowerState> { |
|
||||
return { |
|
||||
powerState: 0, |
|
||||
isOptimizationExempt: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Check permissions (web implementation) |
|
||||
*/ |
|
||||
async checkPermissions(): Promise<PermissionStatus> { |
|
||||
if (!('Notification' in window)) { |
|
||||
return { |
|
||||
notifications: 'denied', |
|
||||
alert: false, |
|
||||
badge: false, |
|
||||
sound: false, |
|
||||
lockScreen: false, |
|
||||
carPlay: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
const permission = Notification.permission; |
|
||||
return { |
|
||||
status: permission, |
|
||||
granted: permission === 'granted', |
|
||||
notifications: permission === 'granted' ? 'granted' : |
|
||||
permission === 'denied' ? 'denied' : 'prompt', |
|
||||
alert: permission === 'granted', |
|
||||
badge: permission === 'granted', |
|
||||
sound: permission === 'granted', |
|
||||
lockScreen: permission === 'granted', |
|
||||
carPlay: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Request permissions (web implementation) |
|
||||
*/ |
|
||||
async requestPermissions(): Promise<PermissionStatus> { |
|
||||
if (!('Notification' in window)) { |
|
||||
throw new Error('Notifications not supported in this browser'); |
|
||||
} |
|
||||
|
|
||||
await Notification.requestPermission(); |
|
||||
return this.checkPermissions(); |
|
||||
} |
|
||||
|
|
||||
// Dual Scheduling Methods Implementation
|
|
||||
|
|
||||
/** |
|
||||
* Schedule content fetch (web implementation) |
|
||||
*/ |
|
||||
async scheduleContentFetch(_config: ContentFetchConfig): Promise<void> { |
|
||||
// Content fetch scheduled (web mock implementation)
|
|
||||
// Mock implementation - in real app would use Service Worker
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Schedule user notification (web implementation) |
|
||||
*/ |
|
||||
async scheduleUserNotification(_config: UserNotificationConfig): Promise<void> { |
|
||||
// User notification scheduled (web mock implementation)
|
|
||||
// Mock implementation - in real app would use browser notifications
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Schedule dual notification (web implementation) |
|
||||
*/ |
|
||||
async scheduleDualNotification(_config: DualScheduleConfiguration): Promise<void> { |
|
||||
// Dual notification scheduled (web mock implementation)
|
|
||||
// Mock implementation combining content fetch and user notification
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get dual schedule status (web implementation) |
|
||||
*/ |
|
||||
async getDualScheduleStatus(): Promise<DualScheduleStatus> { |
|
||||
return { |
|
||||
contentFetch: { |
|
||||
isEnabled: false, |
|
||||
isScheduled: false, |
|
||||
pendingFetches: 0 |
|
||||
}, |
|
||||
userNotification: { |
|
||||
isEnabled: false, |
|
||||
isScheduled: false, |
|
||||
pendingNotifications: 0 |
|
||||
}, |
|
||||
relationship: { |
|
||||
isLinked: false, |
|
||||
contentAvailable: false |
|
||||
}, |
|
||||
overall: { |
|
||||
isActive: false, |
|
||||
lastActivity: Date.now(), |
|
||||
errorCount: 0, |
|
||||
successRate: 1.0 |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Update dual schedule configuration (web implementation) |
|
||||
*/ |
|
||||
async updateDualScheduleConfig(_config: DualScheduleConfiguration): Promise<void> { |
|
||||
// Dual schedule config updated (web mock implementation)
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Cancel dual schedule (web implementation) |
|
||||
*/ |
|
||||
async cancelDualSchedule(): Promise<void> { |
|
||||
// Dual schedule cancelled (web mock implementation)
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Pause dual schedule (web implementation) |
|
||||
*/ |
|
||||
async pauseDualSchedule(): Promise<void> { |
|
||||
// Dual schedule paused (web mock implementation)
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Resume dual schedule (web implementation) |
|
||||
*/ |
|
||||
async resumeDualSchedule(): Promise<void> { |
|
||||
// Dual schedule resumed (web mock implementation)
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get content cache (web implementation) |
|
||||
*/ |
|
||||
async getContentCache(): Promise<Record<string, unknown>> { |
|
||||
return {}; // Mock empty cache
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Clear content cache (web implementation) |
|
||||
*/ |
|
||||
async clearContentCache(): Promise<void> { |
|
||||
// Content cache cleared (web mock implementation)
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get content history (web implementation) |
|
||||
*/ |
|
||||
async getContentHistory(): Promise<ContentFetchResult[]> { |
|
||||
return []; // Mock empty history
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Register callback (web implementation) |
|
||||
*/ |
|
||||
async registerCallback(_name: string, _callback: (...args: unknown[]) => void): Promise<void> { |
|
||||
// Callback registered (web mock implementation)
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Unregister callback (web implementation) |
|
||||
*/ |
|
||||
async unregisterCallback(_name: string): Promise<void> { |
|
||||
// Callback unregistered (web mock implementation)
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get registered callbacks (web implementation) |
|
||||
*/ |
|
||||
async getRegisteredCallbacks(): Promise<string[]> { |
|
||||
return []; // Mock empty callback list
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Schedule browser notification using native APIs |
|
||||
*/ |
|
||||
private async scheduleBrowserNotification(notification: NotificationResponse, options: NotificationOptions): Promise<void> { |
|
||||
if (!('Notification' in window)) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const permission = await Notification.requestPermission(); |
|
||||
if (permission !== 'granted') { |
|
||||
console.warn('Notification permission not granted'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// Calculate next notification time
|
|
||||
if (!options.time) { |
|
||||
throw new Error('Time parameter is required for scheduling'); |
|
||||
} |
|
||||
const nextTime = this.calculateNextNotificationTime(options.time); |
|
||||
const delay = nextTime.getTime() - Date.now(); |
|
||||
|
|
||||
if (delay > 0) { |
|
||||
setTimeout(() => { |
|
||||
this.showBrowserNotification(notification, options); |
|
||||
}, delay); |
|
||||
} else { |
|
||||
// Show immediately if time has passed
|
|
||||
this.showBrowserNotification(notification, options); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Show browser notification |
|
||||
*/ |
|
||||
private showBrowserNotification(notification: NotificationResponse, options: NotificationOptions): void { |
|
||||
if (!('Notification' in window)) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const browserNotification = new Notification(notification.title, { |
|
||||
body: notification.body, |
|
||||
icon: '/favicon.ico', |
|
||||
tag: notification.id, |
|
||||
requireInteraction: false, |
|
||||
silent: !options.sound |
|
||||
}); |
|
||||
|
|
||||
// Handle notification click
|
|
||||
browserNotification.onclick = (): void => { |
|
||||
if (notification.url) { |
|
||||
window.open(notification.url, '_blank'); |
|
||||
} |
|
||||
browserNotification.close(); |
|
||||
}; |
|
||||
|
|
||||
// Auto-close after 10 seconds
|
|
||||
setTimeout(() => { |
|
||||
browserNotification.close(); |
|
||||
}, 10000); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Calculate next notification time |
|
||||
*/ |
|
||||
private calculateNextNotificationTime(timeString: string): Date { |
|
||||
const [hours, minutes] = timeString.split(':').map(Number); |
|
||||
const now = new Date(); |
|
||||
const next = new Date(now); |
|
||||
|
|
||||
next.setHours(hours, minutes, 0, 0); |
|
||||
|
|
||||
// If time has passed today, schedule for tomorrow
|
|
||||
if (next <= now) { |
|
||||
next.setDate(next.getDate() + 1); |
|
||||
} |
|
||||
|
|
||||
return next; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Generate unique ID |
|
||||
*/ |
|
||||
private generateId(): string { |
|
||||
return `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get last notification time |
|
||||
*/ |
|
||||
private async getLastNotificationTime(): Promise<number> { |
|
||||
const last = await this.getLastNotification(); |
|
||||
return last ? last.timestamp : 0; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get next notification time |
|
||||
*/ |
|
||||
private getNextNotificationTime(): number { |
|
||||
// For web, return 24 hours from now as placeholder
|
|
||||
return Date.now() + (24 * 60 * 60 * 1000); |
|
||||
} |
|
||||
|
|
||||
// Phase 1: ActiveDid Management Methods Implementation
|
|
||||
async setActiveDidFromHost(activeDid: string): Promise<void> { |
|
||||
try { |
|
||||
// Setting activeDid from host
|
|
||||
|
|
||||
// Store activeDid for future use
|
|
||||
this.activeDid = activeDid; |
|
||||
|
|
||||
// ActiveDid set successfully
|
|
||||
|
|
||||
} catch (error) { |
|
||||
console.error('DNP-WEB-INDEX: Error setting activeDid from host:', error); |
|
||||
throw error; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onActiveDidChange(callback: (newActiveDid: string) => Promise<void>): void { |
|
||||
try { |
|
||||
// Setting up activeDid change listener
|
|
||||
|
|
||||
// Set up event listener for activeDidChanged events
|
|
||||
document.addEventListener('activeDidChanged', async (event: Event) => { |
|
||||
try { |
|
||||
const eventDetail = (event as CustomEvent).detail; |
|
||||
if (eventDetail && eventDetail.activeDid) { |
|
||||
// ActiveDid changed to new value
|
|
||||
|
|
||||
// Clear current cached content
|
|
||||
await this.clearCacheForNewIdentity(); |
|
||||
|
|
||||
// Update authentication for new identity
|
|
||||
await this.refreshAuthenticationForNewIdentity(eventDetail.activeDid); |
|
||||
|
|
||||
// Call the provided callback
|
|
||||
await callback(eventDetail.activeDid); |
|
||||
|
|
||||
// ActiveDid changed processed
|
|
||||
} |
|
||||
} catch (error) { |
|
||||
console.error('DNP-WEB-INDEX: Error processing activeDid change:', error); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// ActiveDid change listener configured
|
|
||||
|
|
||||
} catch (error) { |
|
||||
console.error('DNP-WEB-INDEX: Error setting up activeDid change listener:', error); |
|
||||
throw error; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async refreshAuthenticationForNewIdentity(activeDid: string): Promise<void> { |
|
||||
try { |
|
||||
// Refreshing authentication for activeDid
|
|
||||
|
|
||||
// Update current activeDid
|
|
||||
this.activeDid = activeDid; |
|
||||
|
|
||||
// Authentication refreshed successfully
|
|
||||
|
|
||||
} catch (error) { |
|
||||
console.error('DNP-WEB-INDEX: Error refreshing authentication:', error); |
|
||||
throw error; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async clearCacheForNewIdentity(): Promise<void> { |
|
||||
try { |
|
||||
// Clearing cache for new identity
|
|
||||
|
|
||||
// Clear content cache
|
|
||||
await this.clearContentCache(); |
|
||||
|
|
||||
// Cache cleared successfully
|
|
||||
|
|
||||
} catch (error) { |
|
||||
console.error('DNP-WEB-INDEX: Error clearing cache for new identity:', error); |
|
||||
throw error; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async updateBackgroundTaskIdentity(activeDid: string): Promise<void> { |
|
||||
try { |
|
||||
// Updating background task identity
|
|
||||
|
|
||||
// Update current activeDid
|
|
||||
this.activeDid = activeDid; |
|
||||
|
|
||||
// Background task identity updated successfully
|
|
||||
|
|
||||
} catch (error) { |
|
||||
console.error('DNP-WEB-INDEX: Error updating background task identity:', error); |
|
||||
throw error; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Static Daily Reminder Methods
|
|
||||
async scheduleDailyReminder(_options: DailyReminderOptions): Promise<void> { |
|
||||
// Schedule daily reminder called (web mock implementation)
|
|
||||
// Mock implementation for web
|
|
||||
} |
|
||||
|
|
||||
async cancelDailyReminder(_reminderId: string): Promise<void> { |
|
||||
// Cancel daily reminder called (web mock implementation)
|
|
||||
// Mock implementation for web
|
|
||||
} |
|
||||
|
|
||||
async getScheduledReminders(): Promise<DailyReminderInfo[]> { |
|
||||
// Get scheduled reminders called (web mock implementation)
|
|
||||
return []; // Mock empty array for web
|
|
||||
} |
|
||||
|
|
||||
async updateDailyReminder(_reminderId: string, _options: DailyReminderOptions): Promise<void> { |
|
||||
// Update daily reminder called (web mock implementation)
|
|
||||
// Mock implementation for web
|
|
||||
} |
|
||||
} |
|
@ -0,0 +1,3 @@ |
|||||
|
1b8e9ec8f48f956e8c971b5c5451603a53b524b082ca6be768cca3cd03dfa54f |
||||
|
# Generated on 2025-10-08T03:25:29.479Z |
||||
|
# Files: dist/esm/**/*.d.ts |
Loading…
Reference in new issue