signalk-parquet
Version:
Vessel data Parquet file archive with automated value and geospatial triggers. History API compliant with cloud backups and queries.
228 lines • 9.45 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AutoDiscoveryService = void 0;
const minimatch_1 = require("minimatch");
const commands_1 = require("../commands");
const data_handler_1 = require("../data-handler");
class AutoDiscoveryService {
constructor(app, pluginConfig, state, currentPaths) {
this.configuredCount = 0;
this.configurationLock = Promise.resolve();
this.app = app;
this.pluginConfig = pluginConfig;
this.state = state;
this.currentPaths = currentPaths;
}
/**
* Set the initial count of auto-discovered paths from existing config.
* Called during initialization to restore the counter.
*/
setInitialCount(count) {
this.configuredCount = count;
this.app.debug(`[AutoDiscovery] Initialized with ${count} existing auto-discovered paths`);
}
/**
* Get the current count of auto-configured paths
*/
getConfiguredCount() {
return this.configuredCount;
}
/**
* Update references when config/paths change
*/
updateReferences(pluginConfig, currentPaths) {
this.pluginConfig = pluginConfig;
this.currentPaths = currentPaths;
}
/**
* Main entry point: attempt to auto-configure a path if conditions are met.
* Called when a query returns no data for a path.
* Uses a lock to prevent race conditions on concurrent requests.
*/
async maybeAutoConfigurePath(path, context) {
// Serialize all auto-discovery operations to prevent race conditions
return new Promise(resolve => {
this.configurationLock = this.configurationLock
.then(async () => {
const result = await this.doAutoConfigurePath(path, context);
resolve(result);
})
.catch(error => {
// Ensure errors don't break the chain, but still resolve with failure
this.app.error(`[AutoDiscovery] Lock chain error for ${path}: ${error}`);
resolve({
configured: false,
path,
reason: `Lock error: ${error.message}`,
});
});
});
}
/**
* Internal implementation of auto-configure logic.
* Must only be called through the lock in maybeAutoConfigurePath.
*/
async doAutoConfigurePath(path, context) {
this.app.debug(`[AutoDiscovery] doAutoConfigurePath called for ${path} in context ${context}`);
const config = this.pluginConfig.autoDiscovery;
// Check if auto-discovery is enabled
if (!config?.enabled) {
this.app.debug(`[AutoDiscovery] Auto-discovery is disabled in config`);
return {
configured: false,
path,
reason: 'Auto-discovery is disabled',
};
}
this.app.debug(`[AutoDiscovery] Config: enabled=${config.enabled}, requireLiveData=${config.requireLiveData}, maxPaths=${config.maxAutoConfiguredPaths}`);
// Check if path is already configured
const existingPath = this.currentPaths.find(p => p.path === path);
if (existingPath) {
return {
configured: false,
path,
reason: 'Path is already configured',
};
}
// Check max limit
const maxPaths = config.maxAutoConfiguredPaths ?? 100;
if (this.configuredCount >= maxPaths) {
this.app.debug(`[AutoDiscovery] Limit reached (${this.configuredCount}/${maxPaths}), skipping ${path}`);
return {
configured: false,
path,
reason: `Maximum auto-configured paths limit reached (${maxPaths})`,
};
}
// Check exclude patterns
if (config.excludePatterns && config.excludePatterns.length > 0) {
const isExcluded = this.matchesPattern(path, config.excludePatterns);
if (isExcluded) {
this.app.debug(`[AutoDiscovery] Path ${path} matches exclude pattern`);
return {
configured: false,
path,
reason: 'Path matches exclude pattern',
};
}
}
// Check include patterns (if specified, path must match at least one)
if (config.includePatterns && config.includePatterns.length > 0) {
const isIncluded = this.matchesPattern(path, config.includePatterns);
if (!isIncluded) {
this.app.debug(`[AutoDiscovery] Path ${path} does not match any include pattern`);
return {
configured: false,
path,
reason: 'Path does not match include patterns',
};
}
}
// Check if live data exists (if required)
if (config.requireLiveData) {
const hasLiveData = this.checkPathHasLiveData(path, context);
if (!hasLiveData) {
this.app.debug(`[AutoDiscovery] Path ${path} has no live data in SignalK`);
return {
configured: false,
path,
reason: 'Path has no live data in SignalK',
};
}
}
// All checks passed - configure the path
return await this.configurePath(path, context);
}
/**
* Check if a path has live data in SignalK
*/
checkPathHasLiveData(path, context) {
try {
// For vessels.self context, use getSelfPath
if (context === 'vessels.self' ||
context === `vessels.${this.app.selfId}`) {
const value = this.app.getSelfPath(path);
this.app.debug(`[AutoDiscovery] getSelfPath(${path}) returned: ${JSON.stringify(value)}`);
return value !== undefined && value !== null;
}
// For other contexts, try to get the value from the full model
// Note: This may not be available depending on SignalK version
const fullPath = `${context}.${path}`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const model = this.app.getPath?.(fullPath);
this.app.debug(`[AutoDiscovery] getPath(${fullPath}) returned: ${JSON.stringify(model)}`);
return model !== undefined && model !== null;
}
catch (error) {
this.app.debug(`[AutoDiscovery] Error checking live data for ${path}: ${error}`);
return false;
}
}
/**
* Check if a path matches any of the given glob patterns
*/
matchesPattern(path, patterns) {
for (const pattern of patterns) {
if ((0, minimatch_1.minimatch)(path, pattern, { dot: true })) {
return true;
}
}
return false;
}
/**
* Configure a path for recording and update subscriptions
*/
async configurePath(path, context) {
try {
// Create the new path configuration
const newPathConfig = {
path,
name: this.generatePathName(path),
enabled: true,
regimen: undefined, // Always enabled, no regimen dependency
source: undefined, // Accept all sources
context: context,
autoDiscovered: true,
};
// Add to current paths (this.currentPaths is the source of truth)
this.currentPaths.push(newPathConfig);
this.configuredCount++;
// Load current webapp config to get commands, then save with updated paths
// Don't push to webAppConfig.paths - use this.currentPaths as the authoritative source
const webAppConfig = (0, commands_1.loadWebAppConfig)(this.app);
(0, commands_1.saveWebAppConfig)(this.currentPaths, webAppConfig.commands, this.app);
// Update data subscriptions to include the new path
(0, data_handler_1.updateDataSubscriptions)(this.currentPaths, this.state, this.pluginConfig, this.app);
this.app.debug(`[AutoDiscovery] Successfully auto-configured path: ${path}`);
return {
configured: true,
path,
reason: 'Path auto-configured successfully',
};
}
catch (error) {
this.app.error(`[AutoDiscovery] Failed to configure path ${path}: ${error}`);
return {
configured: false,
path,
reason: `Configuration failed: ${error.message}`,
};
}
}
/**
* Generate a human-readable name from a SignalK path
*/
generatePathName(path) {
// Convert "navigation.speedOverGround" to "Navigation Speed Over Ground"
const parts = path.split('.');
const words = parts.map(part => {
// Split camelCase
const spaced = part.replace(/([a-z])([A-Z])/g, '$1 $2');
// Capitalize first letter
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
});
return `[Auto] ${words.join(' ')}`;
}
}
exports.AutoDiscoveryService = AutoDiscoveryService;
//# sourceMappingURL=auto-discovery.js.map