feat(typescript): Update ContentCache interface for Contract v1
- Update ContentCache interface to match Contract v1 exactly - Add Contract v1 fields: fetchTime, mediaUrl, title, body, metadata - Mark legacy fields as deprecated (fetchedAt, payload, meta) - Add src/utils/contract-helpers.ts with contract validation utilities BREAKING CHANGE: ContentCache interface now uses Contract v1 field names. Legacy fields marked as deprecated for backward compatibility.
This commit is contained in:
@@ -345,16 +345,43 @@ export interface CreateScheduleInput {
|
|||||||
* Content cache entry with TTL
|
* Content cache entry with TTL
|
||||||
* Stores prefetched content for offline-first display
|
* Stores prefetched content for offline-first display
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Content cache entry (Contract v1)
|
||||||
|
*
|
||||||
|
* This interface matches Contract v1 NotificationContent structure.
|
||||||
|
* Field names align with Contract v1 to ensure cross-platform compatibility.
|
||||||
|
*
|
||||||
|
* @contract v1 - DO NOT MODIFY without Contract Change Proposal (CCP)
|
||||||
|
* @see contracts/NotificationContract.v1.ts
|
||||||
|
*/
|
||||||
export interface ContentCache {
|
export interface ContentCache {
|
||||||
/** Unique cache identifier */
|
/** Unique cache identifier */
|
||||||
id: string;
|
id: string;
|
||||||
/** Timestamp when content was fetched (milliseconds since epoch) */
|
/** Notification title (extracted from payload) */
|
||||||
fetchedAt: number;
|
title: string;
|
||||||
|
/** Notification body text (optional, extracted from payload) */
|
||||||
|
body?: string;
|
||||||
|
/** When notification should be displayed (epoch ms, optional) */
|
||||||
|
scheduledTime?: number;
|
||||||
|
/** When content was fetched (epoch ms, Contract v1 - was fetchedAt) */
|
||||||
|
fetchTime: number;
|
||||||
|
/** Optional image URL for rich notifications (Contract v1 - was url) */
|
||||||
|
mediaUrl?: string;
|
||||||
/** Time-to-live in seconds */
|
/** Time-to-live in seconds */
|
||||||
ttlSeconds: number;
|
ttlSeconds?: number;
|
||||||
/** Content payload (JSON string or base64 encoded) */
|
/** Deduplication key (for idempotency) */
|
||||||
payload: string;
|
dedupeKey?: string;
|
||||||
/** Optional metadata */
|
/** Notification priority level */
|
||||||
|
priority?: 'min' | 'low' | 'default' | 'high' | 'max';
|
||||||
|
/** Additional metadata (structured, was payload string) */
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
|
||||||
|
// Legacy fields (deprecated, may be present for backward compatibility)
|
||||||
|
/** @deprecated Use fetchTime instead */
|
||||||
|
fetchedAt?: number;
|
||||||
|
/** @deprecated Use title, body, metadata instead */
|
||||||
|
payload?: string;
|
||||||
|
/** @deprecated Use metadata instead */
|
||||||
meta?: string;
|
meta?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
101
src/utils/contract-helpers.ts
Normal file
101
src/utils/contract-helpers.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* contract-helpers.ts
|
||||||
|
*
|
||||||
|
* Helper utilities for Contract v1 validation and time conversion
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
* @contract v1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NotificationContent } from '../contracts/NotificationContract.v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current time in epoch milliseconds
|
||||||
|
*
|
||||||
|
* @returns Current time in epoch ms (Contract v1)
|
||||||
|
*/
|
||||||
|
export function nowMs(): number {
|
||||||
|
return Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate NotificationContent against Contract v1
|
||||||
|
*
|
||||||
|
* @param content NotificationContent to validate
|
||||||
|
* @param contractVersion Contract version (default: "v1")
|
||||||
|
* @returns true if valid, throws error if invalid
|
||||||
|
*/
|
||||||
|
export function assertContract(
|
||||||
|
content: NotificationContent,
|
||||||
|
contractVersion: string = 'v1'
|
||||||
|
): boolean {
|
||||||
|
if (contractVersion !== 'v1') {
|
||||||
|
throw new Error(`Unsupported contract version: ${contractVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required fields
|
||||||
|
if (!content.id || typeof content.id !== 'string') {
|
||||||
|
throw new Error('Contract v1 violation: id must be a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.title || typeof content.title !== 'string') {
|
||||||
|
throw new Error('Contract v1 violation: title must be a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof content.fetchTime !== 'number' || content.fetchTime <= 0) {
|
||||||
|
throw new Error('Contract v1 violation: fetchTime must be a positive number (epoch ms)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional fields type checks
|
||||||
|
if (content.body !== undefined && typeof content.body !== 'string') {
|
||||||
|
throw new Error('Contract v1 violation: body must be a string if provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.scheduledTime !== undefined) {
|
||||||
|
if (typeof content.scheduledTime !== 'number' || content.scheduledTime <= 0) {
|
||||||
|
throw new Error('Contract v1 violation: scheduledTime must be a positive number (epoch ms) if provided');
|
||||||
|
}
|
||||||
|
if (content.scheduledTime < content.fetchTime) {
|
||||||
|
throw new Error('Contract v1 violation: scheduledTime must be >= fetchTime');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.mediaUrl !== undefined && typeof content.mediaUrl !== 'string') {
|
||||||
|
throw new Error('Contract v1 violation: mediaUrl must be a string if provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.ttlSeconds !== undefined) {
|
||||||
|
if (typeof content.ttlSeconds !== 'number' || content.ttlSeconds <= 0) {
|
||||||
|
throw new Error('Contract v1 violation: ttlSeconds must be a positive number if provided');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.priority !== undefined) {
|
||||||
|
const validPriorities = ['min', 'low', 'default', 'high', 'max'];
|
||||||
|
if (!validPriorities.includes(content.priority)) {
|
||||||
|
throw new Error(`Contract v1 violation: priority must be one of: ${validPriorities.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Contract v1 hash from bundled file
|
||||||
|
*
|
||||||
|
* @returns Contract hash string
|
||||||
|
*/
|
||||||
|
export async function getContractHash(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/contracts/notification.v1.hash');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load contract hash: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return (await response.text()).trim();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not load contract hash:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user