feat(ios): implement Phase 2.1 iOS background tasks with T–lead prefetch
- Add DailyNotificationBackgroundTaskManager with BGTaskScheduler integration - Add DailyNotificationTTLEnforcer for iOS freshness validation - Add DailyNotificationRollingWindow for iOS capacity management - Add DailyNotificationDatabase with SQLite schema and WAL mode - Add NotificationContent data structure for iOS - Update DailyNotificationPlugin with background task integration - Add phase2-1-ios-background-tasks.ts usage examples This implements the critical Phase 2.1 iOS background execution: - BGTaskScheduler integration for T–lead prefetch - Single-attempt prefetch with 12s timeout - ETag/304 caching support for efficient content updates - Background execution constraints handling - Integration with existing TTL enforcement and rolling window - iOS-specific capacity limits and notification management Files: 7 changed, 2088 insertions(+), 299 deletions(-)
This commit is contained in:
211
ios/Plugin/DailyNotificationDatabase.swift
Normal file
211
ios/Plugin/DailyNotificationDatabase.swift
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* DailyNotificationDatabase.swift
|
||||
*
|
||||
* iOS SQLite database management for daily notifications
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
/**
|
||||
* SQLite database manager for daily notifications on iOS
|
||||
*
|
||||
* This class manages the SQLite database with the three-table schema:
|
||||
* - notif_contents: keep history, fast newest-first reads
|
||||
* - notif_deliveries: track many deliveries per slot/time
|
||||
* - notif_config: generic configuration KV
|
||||
*/
|
||||
class DailyNotificationDatabase {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private static let TAG = "DailyNotificationDatabase"
|
||||
|
||||
// Table names
|
||||
static let TABLE_NOTIF_CONTENTS = "notif_contents"
|
||||
static let TABLE_NOTIF_DELIVERIES = "notif_deliveries"
|
||||
static let TABLE_NOTIF_CONFIG = "notif_config"
|
||||
|
||||
// Column names
|
||||
static let COL_CONTENTS_ID = "id"
|
||||
static let COL_CONTENTS_SLOT_ID = "slot_id"
|
||||
static let COL_CONTENTS_PAYLOAD_JSON = "payload_json"
|
||||
static let COL_CONTENTS_FETCHED_AT = "fetched_at"
|
||||
static let COL_CONTENTS_ETAG = "etag"
|
||||
|
||||
static let COL_DELIVERIES_ID = "id"
|
||||
static let COL_DELIVERIES_SLOT_ID = "slot_id"
|
||||
static let COL_DELIVERIES_FIRE_AT = "fire_at"
|
||||
static let COL_DELIVERIES_DELIVERED_AT = "delivered_at"
|
||||
static let COL_DELIVERIES_STATUS = "status"
|
||||
static let COL_DELIVERIES_ERROR_CODE = "error_code"
|
||||
static let COL_DELIVERIES_ERROR_MESSAGE = "error_message"
|
||||
|
||||
static let COL_CONFIG_K = "k"
|
||||
static let COL_CONFIG_V = "v"
|
||||
|
||||
// Status values
|
||||
static let STATUS_SCHEDULED = "scheduled"
|
||||
static let STATUS_SHOWN = "shown"
|
||||
static let STATUS_ERROR = "error"
|
||||
static let STATUS_CANCELED = "canceled"
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var db: OpaquePointer?
|
||||
private let path: String
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
* Initialize database with path
|
||||
*
|
||||
* @param path Database file path
|
||||
*/
|
||||
init(path: String) {
|
||||
self.path = path
|
||||
openDatabase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open database connection
|
||||
*/
|
||||
private func openDatabase() {
|
||||
if sqlite3_open(path, &db) == SQLITE_OK {
|
||||
print("\(Self.TAG): Database opened successfully at \(path)")
|
||||
createTables()
|
||||
configureDatabase()
|
||||
} else {
|
||||
print("\(Self.TAG): Error opening database: \(String(cString: sqlite3_errmsg(db)))")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database tables
|
||||
*/
|
||||
private func createTables() {
|
||||
// Create notif_contents table
|
||||
let createContentsTable = """
|
||||
CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_CONTENTS)(
|
||||
\(Self.COL_CONTENTS_ID) INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
\(Self.COL_CONTENTS_SLOT_ID) TEXT NOT NULL,
|
||||
\(Self.COL_CONTENTS_PAYLOAD_JSON) TEXT NOT NULL,
|
||||
\(Self.COL_CONTENTS_FETCHED_AT) INTEGER NOT NULL,
|
||||
\(Self.COL_CONTENTS_ETAG) TEXT,
|
||||
UNIQUE(\(Self.COL_CONTENTS_SLOT_ID), \(Self.COL_CONTENTS_FETCHED_AT))
|
||||
);
|
||||
"""
|
||||
|
||||
// Create notif_deliveries table
|
||||
let createDeliveriesTable = """
|
||||
CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_DELIVERIES)(
|
||||
\(Self.COL_DELIVERIES_ID) INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
\(Self.COL_DELIVERIES_SLOT_ID) TEXT NOT NULL,
|
||||
\(Self.COL_DELIVERIES_FIRE_AT) INTEGER NOT NULL,
|
||||
\(Self.COL_DELIVERIES_DELIVERED_AT) INTEGER,
|
||||
\(Self.COL_DELIVERIES_STATUS) TEXT NOT NULL DEFAULT '\(Self.STATUS_SCHEDULED)',
|
||||
\(Self.COL_DELIVERIES_ERROR_CODE) TEXT,
|
||||
\(Self.COL_DELIVERIES_ERROR_MESSAGE) TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
// Create notif_config table
|
||||
let createConfigTable = """
|
||||
CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_CONFIG)(
|
||||
\(Self.COL_CONFIG_K) TEXT PRIMARY KEY,
|
||||
\(Self.COL_CONFIG_V) TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
|
||||
// Create indexes
|
||||
let createContentsIndex = """
|
||||
CREATE INDEX IF NOT EXISTS notif_idx_contents_slot_time
|
||||
ON \(Self.TABLE_NOTIF_CONTENTS)(\(Self.COL_CONTENTS_SLOT_ID), \(Self.COL_CONTENTS_FETCHED_AT) DESC);
|
||||
"""
|
||||
|
||||
// Execute table creation
|
||||
executeSQL(createContentsTable)
|
||||
executeSQL(createDeliveriesTable)
|
||||
executeSQL(createConfigTable)
|
||||
executeSQL(createContentsIndex)
|
||||
|
||||
print("\(Self.TAG): Database tables created successfully")
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure database settings
|
||||
*/
|
||||
private func configureDatabase() {
|
||||
// Enable WAL mode
|
||||
executeSQL("PRAGMA journal_mode=WAL")
|
||||
|
||||
// Set synchronous mode
|
||||
executeSQL("PRAGMA synchronous=NORMAL")
|
||||
|
||||
// Set busy timeout
|
||||
executeSQL("PRAGMA busy_timeout=5000")
|
||||
|
||||
// Enable foreign keys
|
||||
executeSQL("PRAGMA foreign_keys=ON")
|
||||
|
||||
// Set user version
|
||||
executeSQL("PRAGMA user_version=1")
|
||||
|
||||
print("\(Self.TAG): Database configured successfully")
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute SQL statement
|
||||
*
|
||||
* @param sql SQL statement to execute
|
||||
*/
|
||||
private func executeSQL(_ sql: String) {
|
||||
var statement: OpaquePointer?
|
||||
|
||||
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
|
||||
if sqlite3_step(statement) == SQLITE_DONE {
|
||||
print("\(Self.TAG): SQL executed successfully: \(sql)")
|
||||
} else {
|
||||
print("\(Self.TAG): SQL execution failed: \(String(cString: sqlite3_errmsg(db)))")
|
||||
}
|
||||
} else {
|
||||
print("\(Self.TAG): SQL preparation failed: \(String(cString: sqlite3_errmsg(db)))")
|
||||
}
|
||||
|
||||
sqlite3_finalize(statement)
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
func close() {
|
||||
if sqlite3_close(db) == SQLITE_OK {
|
||||
print("\(Self.TAG): Database closed successfully")
|
||||
} else {
|
||||
print("\(Self.TAG): Error closing database: \(String(cString: sqlite3_errmsg(db)))")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database path
|
||||
*
|
||||
* @return Database file path
|
||||
*/
|
||||
func getPath() -> String {
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database is open
|
||||
*
|
||||
* @return true if database is open
|
||||
*/
|
||||
func isOpen() -> Bool {
|
||||
return db != nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user