signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
290 lines • 11.8 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* WASM ServerAPI FFI Bridge
*
* Provides the FFI (Foreign Function Interface) bridge between WASM plugins
* and the Signal K ServerAPI. Enforces capability restrictions and handles
* serialization across the WASM boundary.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createServerAPIBridge = createServerAPIBridge;
exports.createWasmImports = createWasmImports;
exports.callWasmExport = callWasmExport;
/// <reference lib="webworker" />
const debug_1 = __importDefault(require("debug"));
const server_api_1 = require("@signalk/server-api");
const loader_1 = require("./loader");
const wasm_storage_1 = require("./wasm-storage");
const debug = (0, debug_1.default)('signalk:wasm:serverapi');
/**
* Create ServerAPI FFI functions for a WASM plugin
*
* These functions will be imported by the WASM module and provide
* access to Signal K server capabilities based on declared permissions.
*/
function createServerAPIBridge(app, pluginId, configPath) {
const plugin = (0, loader_1.getWasmPlugin)(pluginId);
if (!plugin) {
throw new Error(`Plugin ${pluginId} not found`);
}
const capabilities = plugin.metadata.capabilities;
return {
// Delta Handler API
'delta-handler': {
/**
* Handle delta message from plugin
*
* @param pluginIdParam - Plugin identifier
* @param deltaJson - Delta message as JSON string
* @param version - Signal K version: 1 = v1 (default), 2 = v2
*
* Plugins should use v1 for regular navigation data.
* Use v2 for Course API paths and other v2-specific data.
*/
handleMessage: (pluginIdParam, deltaJson, version = 1) => {
if (!capabilities.dataWrite) {
throw new Error(`Plugin ${pluginId} lacks dataWrite capability`);
}
try {
const delta = JSON.parse(deltaJson);
const skVersion = version === 2 ? server_api_1.SKVersion.v2 : server_api_1.SKVersion.v1;
debug(`Plugin ${pluginId} emitting delta (${skVersion}):`, delta);
// Forward to server's handleMessage with version
if (app.handleMessage) {
app.handleMessage(pluginId, delta, skVersion);
}
else {
debug('Warning: app.handleMessage not available');
}
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
debug(`Error handling delta from ${pluginId}: ${errorMsg}`);
throw error;
}
}
},
// Plugin Config API
'plugin-config': {
/**
* Read plugin configuration
*/
readPluginOptions: () => {
const storagePaths = (0, wasm_storage_1.getPluginStoragePaths)(configPath, pluginId, plugin.packageName);
const config = (0, wasm_storage_1.readPluginConfig)(storagePaths.configFile);
return JSON.stringify(config.configuration || {});
},
/**
* Save plugin configuration
*/
savePluginOptions: (configJson) => {
try {
const configuration = JSON.parse(configJson);
const storagePaths = (0, wasm_storage_1.getPluginStoragePaths)(configPath, pluginId, plugin.packageName);
const config = {
enabled: plugin.enabled,
configuration
};
(0, wasm_storage_1.writePluginConfig)(storagePaths.configFile, config);
plugin.configuration = configuration;
debug(`Plugin ${pluginId} saved configuration`);
return 0; // Success
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
debug(`Error saving config for ${pluginId}: ${errorMsg}`);
return 1; // Error
}
},
/**
* Get data directory path (VFS root from plugin perspective)
*/
getDataDirPath: () => {
// Plugin sees "/" as its VFS root
return '/';
}
},
// Plugin Status API
'plugin-status': {
/**
* Set plugin status message
*/
setPluginStatus: (message) => {
plugin.statusMessage = message;
debug(`Plugin ${pluginId} status: ${message}`);
// Update in app status if available
if (app.setPluginStatus) {
app.setPluginStatus(pluginId, message);
}
},
/**
* Set plugin error message
*/
setPluginError: (message) => {
plugin.errorMessage = message;
plugin.status = 'error';
debug(`Plugin ${pluginId} error: ${message}`);
// Update in app status if available
if (app.setPluginError) {
app.setPluginError(pluginId, message);
}
},
/**
* Debug logging
*/
debug: (message) => {
debug(`[${pluginId}] ${message}`);
},
/**
* Error logging
*/
error: (message) => {
debug(`[${pluginId}] ERROR: ${message}`);
}
},
// Full Model API (Signal K full data model access)
'full-model': {
/**
* Get data from vessel.self path
*/
getSelfPath: (path) => {
if (!capabilities.dataRead) {
throw new Error(`Plugin ${pluginId} lacks dataRead capability`);
}
try {
const value = app.getSelfPath ? app.getSelfPath(path) : undefined;
return value !== undefined ? JSON.stringify(value) : null;
}
catch (error) {
debug(`Error getting self path ${path} for ${pluginId}:`, error);
return null;
}
},
/**
* Get data from any context path
*/
getPath: (path) => {
if (!capabilities.dataRead) {
throw new Error(`Plugin ${pluginId} lacks dataRead capability`);
}
try {
const value = app.getPath ? app.getPath(path) : undefined;
return value !== undefined ? JSON.stringify(value) : null;
}
catch (error) {
debug(`Error getting path ${path} for ${pluginId}:`, error);
return null;
}
}
}
};
}
/**
* Create WASM import object with ServerAPI functions
*
* This generates the WebAssembly imports that will be available to the plugin.
* In Phase 1, we use a simplified approach. Full WIT bindings will be added later.
*/
function createWasmImports(app, pluginId, configPath) {
const bridge = createServerAPIBridge(app, pluginId, configPath);
// Create flat import object for WASM
// Note: This is a simplified version for Phase 1
// Full WIT integration will provide proper type-safe bindings
return {
env: {
// Delta handling
sk_handle_message: (deltaPtr, deltaLen, memory) => {
const deltaJson = readStringFromMemory(memory, deltaPtr, deltaLen);
bridge['delta-handler'].handleMessage(pluginId, deltaJson);
},
// Configuration
sk_read_config: (bufPtr, bufLen, memory) => {
const configJson = bridge['plugin-config'].readPluginOptions();
return writeStringToMemory(memory, bufPtr, bufLen, configJson);
},
sk_save_config: (configPtr, configLen, memory) => {
const configJson = readStringFromMemory(memory, configPtr, configLen);
return bridge['plugin-config'].savePluginOptions(configJson);
},
// Status
sk_set_status: (msgPtr, msgLen, memory) => {
const message = readStringFromMemory(memory, msgPtr, msgLen);
bridge['plugin-status'].setPluginStatus(message);
},
sk_set_error: (msgPtr, msgLen, memory) => {
const message = readStringFromMemory(memory, msgPtr, msgLen);
bridge['plugin-status'].setPluginError(message);
},
sk_debug: (msgPtr, msgLen, memory) => {
const message = readStringFromMemory(memory, msgPtr, msgLen);
bridge['plugin-status'].debug(message);
},
// Data model
sk_get_self_path: (pathPtr, pathLen, bufPtr, bufLen, memory) => {
const path = readStringFromMemory(memory, pathPtr, pathLen);
const value = bridge['full-model'].getSelfPath(path);
if (value === null) {
return 0; // Not found
}
return writeStringToMemory(memory, bufPtr, bufLen, value);
},
sk_get_path: (pathPtr, pathLen, bufPtr, bufLen, memory) => {
const path = readStringFromMemory(memory, pathPtr, pathLen);
const value = bridge['full-model'].getPath(path);
if (value === null) {
return 0; // Not found
}
return writeStringToMemory(memory, bufPtr, bufLen, value);
}
}
};
}
/**
* Read a string from WASM memory
*/
function readStringFromMemory(memory, ptr, len) {
const buffer = new Uint8Array(memory.buffer, ptr, len);
const decoder = new TextDecoder();
return decoder.decode(buffer);
}
/**
* Write a string to WASM memory
* Returns the number of bytes written, or 0 if buffer too small
*/
function writeStringToMemory(memory, ptr, maxLen, str) {
const encoder = new TextEncoder();
const encoded = encoder.encode(str);
if (encoded.length > maxLen) {
debug(`Buffer too small: need ${encoded.length}, have ${maxLen}`);
return 0;
}
const buffer = new Uint8Array(memory.buffer, ptr, maxLen);
buffer.set(encoded);
return encoded.length;
}
/**
* Call a WASM plugin export with error handling
*/
function callWasmExport(pluginId, exportName, ...args) {
const plugin = (0, loader_1.getWasmPlugin)(pluginId);
if (!plugin || !plugin.instance) {
throw new Error(`Plugin ${pluginId} not loaded`);
}
try {
const exportFn = plugin.instance.exports[exportName];
if (typeof exportFn !== 'function') {
throw new Error(`Export ${exportName} not found or not a function`);
}
return exportFn(...args);
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
debug(`Error calling ${exportName} on ${pluginId}: ${errorMsg}`);
throw error;
}
}
//# sourceMappingURL=wasm-serverapi.js.map