diff --git a/package.json b/package.json index 26c50b13a..ae301c9d1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "serve": "vite preview", "test:ci": "vitest --coverage --run", "test:unit": "vitest", - "typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false" + "typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", + "build:lib": "BUILD_MODE=library vite build" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.0", diff --git a/src/libs/external-api/api.ts b/src/libs/external-api/api.ts new file mode 100644 index 000000000..36b1a2bca --- /dev/null +++ b/src/libs/external-api/api.ts @@ -0,0 +1,43 @@ +import { CallbackRateLimiter } from './callback-rate-limiter' + +/** + * Current version of the Cockpit Widget API + */ +export const COCKPIT_WIDGET_API_VERSION = '0.0.0' + +/** + * Listens to updates for a specific datalake variable. + * This function sets up a message listener that receives updates from the parent window + * and forwards them to the callback function, respecting the specified rate limit. + * @param {string} variable - The name of the datalake variable to listen to + * @param {Function} callback - The function to call when the variable is updated + * @param {number} maxRateHz - The maximum rate (in Hz) at which updates should be received. Default is 10 Hz + * @example + * ```typescript + * // Listen to updates at 5Hz + * listenToDatalakeVariable('temperature', (value) => { + * console.log('Temperature:', value); + * }, 5); + * ``` + */ +export function listenToDatalakeVariable(variable: string, callback: (data: any) => void, maxRateHz = 10): void { + // Convert Hz to minimum interval in milliseconds + const minIntervalMs = 1000 / maxRateHz + const rateLimiter = new CallbackRateLimiter(minIntervalMs) + + const message = { + type: 'cockpit:listenToDatalakeVariables', + variable: variable, + maxRateHz: maxRateHz, + } + window.parent.postMessage(message, '*') + + window.addEventListener('message', function handler(event) { + if (event.data.type === 'cockpit:datalakeVariable' && event.data.variable === variable) { + // Only call callback if we haven't exceeded the rate limit + if (rateLimiter.canCall(variable)) { + callback(event.data.value) + } + } + }) +} diff --git a/src/libs/external-api/callback-rate-limiter.ts b/src/libs/external-api/callback-rate-limiter.ts new file mode 100644 index 000000000..cae7aaa9c --- /dev/null +++ b/src/libs/external-api/callback-rate-limiter.ts @@ -0,0 +1,30 @@ +/** + * A simple rate limiter for callbacks that ensures a minimum time interval between calls + */ +export class CallbackRateLimiter { + private lastCallTimes = new Map() + + /** + * Creates a new CallbackRateLimiter + * @param {number} minIntervalMs - The minimum time (in milliseconds) that must pass between calls + */ + constructor(private minIntervalMs: number) {} + + /** + * Checks if enough time has passed to allow another call + * @param {string} key - Unique identifier for the callback being rate limited + * @returns {boolean} true if enough time has passed since the last call, false otherwise + */ + public canCall(key: string): boolean { + const now = Date.now() + const lastCall = this.lastCallTimes.get(key) || 0 + const timeSinceLastCall = now - lastCall + + if (timeSinceLastCall >= this.minIntervalMs) { + this.lastCallTimes.set(key, now) + return true + } + + return false + } +} diff --git a/vite.config.ts b/vite.config.ts index 3bbdf9ef0..c9a989000 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,8 +10,10 @@ import { getVersion } from './src/libs/non-browser-utils' // Check if we're running in Electron mode or building the application const isElectron = process.env.ELECTRON === 'true' const isBuilding = process.argv.includes('build') +const isLibrary = process.env.BUILD_MODE === 'library' -export default defineConfig({ +// Base configuration that will be merged +const baseConfig = { plugins: [ (isElectron || isBuilding) && electron([ @@ -70,4 +72,48 @@ export default defineConfig({ server: { host: '0.0.0.0', }, +} + +// Library-specific configuration +const libraryConfig = { + build: { + lib: { + entry: path.resolve(__dirname, 'src/libs/external-api/api.ts'), + name: 'CockpitAPI', + formats: ['es', 'umd', 'iife'], + fileName: (format: string) => { + switch (format) { + case 'iife': + return 'cockpit-external-api.browser.js' + case 'umd': + return 'cockpit-external-api.umd.js' + default: + return `cockpit-external-api.${format}.js` + } + }, + }, + rollupOptions: { + external: ['vue', 'vuetify'], + output: { + globals: { + vue: 'Vue', + vuetify: 'Vuetify', + }, + }, + }, + outDir: 'dist/lib', // Separate output directory for library builds + minify: false, // Disable minification for now + }, +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default defineConfig((_configEnv) => { + if (isLibrary) { + // For library builds, merge the base config with library-specific settings + return { + ...baseConfig, + ...libraryConfig, + } as any + } + return baseConfig as any })