feat: add types to storage api

This commit is contained in:
SethBurkart123
2025-03-18 20:45:25 +11:00
parent be54816d83
commit 74e92ddb53
4 changed files with 58 additions and 37 deletions
@@ -9,7 +9,12 @@ interface NotificationCollectorSettings extends PluginSettings {
}; };
} }
const notificationCollectorPlugin: Plugin<NotificationCollectorSettings> = { interface NotificationCollectorStorage {
lastNotificationCount: number;
lastCheckedTime: string;
}
const notificationCollectorPlugin: Plugin<NotificationCollectorSettings, NotificationCollectorStorage> = {
id: 'notificationCollector', id: 'notificationCollector',
name: 'Notification Collector', name: 'Notification Collector',
description: 'Collects and displays SEQTA notifications', description: 'Collects and displays SEQTA notifications',
@@ -26,8 +31,19 @@ const notificationCollectorPlugin: Plugin<NotificationCollectorSettings> = {
run: async (api) => { run: async (api) => {
let pollInterval: number | null = null; let pollInterval: number | null = null;
// Store last notification count in storage
if (!api.storage.lastNotificationCount) {
api.storage.lastNotificationCount = 0;
}
const checkNotifications = async () => { const checkNotifications = async () => {
try { try {
const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement;
if (api.storage.lastNotificationCount !== 0) {
alertDiv.textContent = api.storage.lastNotificationCount.toString();
}
const response = await fetch(`${location.origin}/seqta/student/heartbeat?`, { const response = await fetch(`${location.origin}/seqta/student/heartbeat?`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -40,10 +56,14 @@ const notificationCollectorPlugin: Plugin<NotificationCollectorSettings> = {
}); });
const data = await response.json(); const data = await response.json();
const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement;
// Store notification count for history
const notificationCount = data.payload.notifications.length;
api.storage.lastNotificationCount = notificationCount;
api.storage.lastCheckedTime = new Date().toISOString();
if (alertDiv) { if (alertDiv) {
alertDiv.textContent = data.payload.notifications.length.toString(); alertDiv.textContent = notificationCount.toString();
} else { } else {
console.info("[BetterSEQTA+] No notifications currently"); console.info("[BetterSEQTA+] No notifications currently");
} }
+11 -22
View File
@@ -112,7 +112,7 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
return proxy; return proxy;
} }
function createStorageAPI(pluginId: string): StorageAPI { function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in keyof T]: T[K] } {
const prefix = `plugin.${pluginId}.storage.`; const prefix = `plugin.${pluginId}.storage.`;
const cache: Record<string, any> = {}; const cache: Record<string, any> = {};
const listeners = new Map<string, Set<(value: any) => void>>(); const listeners = new Map<string, Set<(value: any) => void>>();
@@ -152,28 +152,17 @@ function createStorageAPI(pluginId: string): StorageAPI {
// Create the proxy for direct property access // Create the proxy for direct property access
return new Proxy(cache, { return new Proxy(cache, {
get(target, prop: string) { get(target, prop: string) {
if (prop === 'get') {
return async <T>(key: string) => {
return target[key] as T || null;
};
}
if (prop === 'set') {
return async <T>(key: string, value: T) => {
target[key] = value;
await browser.storage.local.set({ [prefix + key]: value });
};
}
if (prop === 'onChange') { if (prop === 'onChange') {
return (key: string, callback: (value: any) => void) => { return (key: keyof T, callback: (value: T[keyof T]) => void) => {
if (!listeners.has(key)) { if (!listeners.has(key as string)) {
listeners.set(key, new Set()); listeners.set(key as string, new Set());
} }
listeners.get(key)!.add(callback); listeners.get(key as string)!.add(callback);
}; };
} }
if (prop === 'offChange') { if (prop === 'offChange') {
return (key: string, callback: (value: any) => void) => { return (key: keyof T, callback: (value: T[keyof T]) => void) => {
listeners.get(key)?.delete(callback); listeners.get(key as string)?.delete(callback);
}; };
} }
if (prop === 'loaded') { if (prop === 'loaded') {
@@ -184,7 +173,7 @@ function createStorageAPI(pluginId: string): StorageAPI {
return target[prop]; return target[prop];
}, },
set(target, prop: string, value: any) { set(target, prop: string, value: any) {
if (['get', 'set', 'onChange', 'offChange', 'loaded'].includes(prop)) { if (['onChange', 'offChange', 'loaded'].includes(prop)) {
return false; return false;
} }
@@ -197,7 +186,7 @@ function createStorageAPI(pluginId: string): StorageAPI {
return true; return true;
} }
}) as StorageAPI; }) as StorageAPI<T> & { [K in keyof T]: T[K] };
} }
function createEventsAPI(pluginId: string): EventsAPI { function createEventsAPI(pluginId: string): EventsAPI {
@@ -219,11 +208,11 @@ function createEventsAPI(pluginId: string): EventsAPI {
}; };
} }
export function createPluginAPI<T extends PluginSettings>(plugin: Plugin<T>): PluginAPI<T> { export function createPluginAPI<T extends PluginSettings, S = any>(plugin: Plugin<T, S>): PluginAPI<T, S> {
return { return {
seqta: createSEQTAAPI(), seqta: createSEQTAAPI(),
settings: createSettingsAPI(plugin), settings: createSettingsAPI(plugin),
storage: createStorageAPI(plugin.id), storage: createStorageAPI<S>(plugin.id),
events: createEventsAPI(plugin.id), events: createEventsAPI(plugin.id),
}; };
} }
+2 -2
View File
@@ -3,7 +3,7 @@ import { createPluginAPI } from './createAPI';
export class PluginManager { export class PluginManager {
private static instance: PluginManager; private static instance: PluginManager;
private plugins: Map<string, Plugin<any>> = new Map(); private plugins: Map<string, Plugin<any, any>> = new Map();
private runningPlugins: Map<string, boolean> = new Map(); private runningPlugins: Map<string, boolean> = new Map();
private eventBacklog: Map<string, any[]> = new Map(); private eventBacklog: Map<string, any[]> = new Map();
private cleanupFunctions: Map<string, () => void> = new Map(); private cleanupFunctions: Map<string, () => void> = new Map();
@@ -45,7 +45,7 @@ export class PluginManager {
} }
} }
public registerPlugin<T extends PluginSettings>(plugin: Plugin<T>): void { public registerPlugin<T extends PluginSettings, S>(plugin: Plugin<T, S>): void {
if (this.plugins.has(plugin.id)) { if (this.plugins.has(plugin.id)) {
throw new Error(`Plugin with id "${plugin.id}" is already registered`); throw new Error(`Plugin with id "${plugin.id}" is already registered`);
} }
+22 -10
View File
@@ -57,13 +57,25 @@ export interface SEQTAAPI {
onPageChange: (callback: (page: string) => void) => void; onPageChange: (callback: (page: string) => void) => void;
} }
export interface StorageAPI { export interface StorageAPI<T = any> {
get: <T>(key: string) => Promise<T | null>; /**
set: <T>(key: string, value: T) => Promise<void>; * Register a callback to be called when a storage value changes
onChange: (key: string, callback: (value: any) => void) => void; */
offChange: (key: string, callback: (value: any) => void) => void; onChange: <K extends keyof T>(key: K, callback: (value: T[K]) => void) => void;
/**
* Remove a previously registered callback
*/
offChange: <K extends keyof T>(key: K, callback: (value: T[K]) => void) => void;
/**
* Promise that resolves when storage values are loaded
*/
loaded: Promise<void>; loaded: Promise<void>;
[key: string]: any; }
export type TypedStorageAPI<T> = StorageAPI<T> & {
[K in keyof T]: T[K];
} }
export interface EventsAPI { export interface EventsAPI {
@@ -71,18 +83,18 @@ export interface EventsAPI {
emit: (event: string, ...args: any[]) => void; emit: (event: string, ...args: any[]) => void;
} }
export interface PluginAPI<T extends PluginSettings> { export interface PluginAPI<T extends PluginSettings, S = any> {
seqta: SEQTAAPI; seqta: SEQTAAPI;
settings: SettingsAPI<T>; settings: SettingsAPI<T>;
storage: StorageAPI; storage: TypedStorageAPI<S>;
events: EventsAPI; events: EventsAPI;
} }
export interface Plugin<T extends PluginSettings = PluginSettings> { export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
id: string; id: string;
name: string; name: string;
description: string; description: string;
version: string; version: string;
settings: T; settings: T;
run: (api: PluginAPI<T>) => void | Promise<void> | (() => void) | Promise<() => void>; run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<() => void>;
} }