Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,791 @@
/**
* @ruvector/edge-net Distribution Manager
*
* Handles model distribution across multiple sources:
* - Google Cloud Storage (GCS)
* - IPFS (via web3.storage or nft.storage)
* - CDN with fallback support
*
* Features:
* - Integrity verification (SHA256)
* - Progress tracking for large files
* - Automatic source failover
*
* @module @ruvector/edge-net/models/distribution
*/
import { EventEmitter } from 'events';
import { createHash, randomBytes } from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
import https from 'https';
import http from 'http';
import { URL } from 'url';
// ============================================
// CONSTANTS
// ============================================
const DEFAULT_GCS_BUCKET = 'ruvector-models';
const DEFAULT_CDN_BASE = 'https://models.ruvector.dev';
const DEFAULT_IPFS_GATEWAY = 'https://w3s.link/ipfs';
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks for streaming
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1000;
// ============================================
// SOURCE TYPES
// ============================================
/**
* Source priority order (lower = higher priority)
*/
export const SOURCE_PRIORITY = {
cdn: 1,
gcs: 2,
ipfs: 3,
fallback: 99,
};
/**
* Source URL patterns
*/
export const SOURCE_PATTERNS = {
gcs: /^gs:\/\/([^/]+)\/(.+)$/,
ipfs: /^ipfs:\/\/(.+)$/,
http: /^https?:\/\/.+$/,
};
// ============================================
// PROGRESS TRACKER
// ============================================
/**
* Progress tracker for file transfers
*/
export class ProgressTracker extends EventEmitter {
constructor(totalBytes = 0) {
super();
this.totalBytes = totalBytes;
this.bytesTransferred = 0;
this.startTime = Date.now();
this.lastUpdateTime = Date.now();
this.lastBytesTransferred = 0;
}
/**
* Update progress
* @param {number} bytes - Bytes transferred in this chunk
*/
update(bytes) {
this.bytesTransferred += bytes;
const now = Date.now();
// Calculate speed (bytes per second)
const timeDelta = (now - this.lastUpdateTime) / 1000;
const bytesDelta = this.bytesTransferred - this.lastBytesTransferred;
const speed = timeDelta > 0 ? bytesDelta / timeDelta : 0;
// Calculate ETA
const remaining = this.totalBytes - this.bytesTransferred;
const eta = speed > 0 ? remaining / speed : 0;
const progress = {
bytesTransferred: this.bytesTransferred,
totalBytes: this.totalBytes,
percent: this.totalBytes > 0
? Math.round((this.bytesTransferred / this.totalBytes) * 100)
: 0,
speed: Math.round(speed),
speedMBps: Math.round(speed / (1024 * 1024) * 100) / 100,
eta: Math.round(eta),
elapsed: Math.round((now - this.startTime) / 1000),
};
this.lastUpdateTime = now;
this.lastBytesTransferred = this.bytesTransferred;
this.emit('progress', progress);
if (this.bytesTransferred >= this.totalBytes) {
this.emit('complete', progress);
}
}
/**
* Mark as complete
*/
complete() {
this.bytesTransferred = this.totalBytes;
const elapsed = (Date.now() - this.startTime) / 1000;
this.emit('complete', {
bytesTransferred: this.bytesTransferred,
totalBytes: this.totalBytes,
percent: 100,
elapsed: Math.round(elapsed),
averageSpeed: Math.round(this.totalBytes / elapsed),
});
}
/**
* Mark as failed
* @param {Error} error - Failure error
*/
fail(error) {
this.emit('error', {
error,
bytesTransferred: this.bytesTransferred,
totalBytes: this.totalBytes,
});
}
}
// ============================================
// DISTRIBUTION MANAGER
// ============================================
/**
* DistributionManager - Manages model uploads and downloads
*/
export class DistributionManager extends EventEmitter {
/**
* Create a new DistributionManager
* @param {object} options - Configuration options
*/
constructor(options = {}) {
super();
this.id = `dist-${randomBytes(6).toString('hex')}`;
// GCS configuration
this.gcsConfig = {
bucket: options.gcsBucket || DEFAULT_GCS_BUCKET,
projectId: options.gcsProjectId || process.env.GCS_PROJECT_ID,
keyFilePath: options.gcsKeyFile || process.env.GOOGLE_APPLICATION_CREDENTIALS,
};
// IPFS configuration
this.ipfsConfig = {
gateway: options.ipfsGateway || DEFAULT_IPFS_GATEWAY,
web3StorageToken: options.web3StorageToken || process.env.WEB3_STORAGE_TOKEN,
nftStorageToken: options.nftStorageToken || process.env.NFT_STORAGE_TOKEN,
};
// CDN configuration
this.cdnConfig = {
baseUrl: options.cdnBaseUrl || DEFAULT_CDN_BASE,
fallbackUrls: options.cdnFallbacks || [],
};
// Download cache (in-flight downloads)
this.activeDownloads = new Map();
// Stats
this.stats = {
uploads: 0,
downloads: 0,
bytesUploaded: 0,
bytesDownloaded: 0,
failures: 0,
};
}
// ============================================
// URL GENERATION
// ============================================
/**
* Generate CDN URL for a model
* @param {string} modelName - Model name
* @param {string} version - Model version
* @param {string} filename - Filename
* @returns {string}
*/
getCdnUrl(modelName, version, filename = null) {
const file = filename || `${modelName}.onnx`;
return `${this.cdnConfig.baseUrl}/${modelName}/${version}/${file}`;
}
/**
* Generate GCS URL for a model
* @param {string} modelName - Model name
* @param {string} version - Model version
* @param {string} filename - Filename
* @returns {string}
*/
getGcsUrl(modelName, version, filename = null) {
const file = filename || `${modelName}.onnx`;
return `gs://${this.gcsConfig.bucket}/${modelName}/${version}/${file}`;
}
/**
* Generate IPFS URL from CID
* @param {string} cid - IPFS Content ID
* @returns {string}
*/
getIpfsUrl(cid) {
return `ipfs://${cid}`;
}
/**
* Generate HTTP gateway URL for IPFS
* @param {string} cid - IPFS Content ID
* @returns {string}
*/
getIpfsGatewayUrl(cid) {
// Handle both ipfs:// URLs and raw CIDs
const cleanCid = cid.replace(/^ipfs:\/\//, '');
return `${this.ipfsConfig.gateway}/${cleanCid}`;
}
/**
* Generate all source URLs for a model
* @param {object} sources - Source configuration from metadata
* @param {string} modelName - Model name
* @param {string} version - Version
* @returns {object[]} Sorted list of sources with URLs
*/
generateSourceUrls(sources, modelName, version) {
const urls = [];
// CDN (highest priority)
if (sources.cdn) {
urls.push({
type: 'cdn',
url: sources.cdn,
priority: SOURCE_PRIORITY.cdn,
});
} else {
// Auto-generate CDN URL
urls.push({
type: 'cdn',
url: this.getCdnUrl(modelName, version),
priority: SOURCE_PRIORITY.cdn,
});
}
// GCS
if (sources.gcs) {
const gcsMatch = sources.gcs.match(SOURCE_PATTERNS.gcs);
if (gcsMatch) {
// Convert gs:// to HTTPS URL
const [, bucket, path] = gcsMatch;
urls.push({
type: 'gcs',
url: `https://storage.googleapis.com/${bucket}/${path}`,
originalUrl: sources.gcs,
priority: SOURCE_PRIORITY.gcs,
});
}
}
// IPFS
if (sources.ipfs) {
urls.push({
type: 'ipfs',
url: this.getIpfsGatewayUrl(sources.ipfs),
originalUrl: sources.ipfs,
priority: SOURCE_PRIORITY.ipfs,
});
}
// Fallback URLs
for (const fallback of this.cdnConfig.fallbackUrls) {
urls.push({
type: 'fallback',
url: `${fallback}/${modelName}/${version}/${modelName}.onnx`,
priority: SOURCE_PRIORITY.fallback,
});
}
// Sort by priority
return urls.sort((a, b) => a.priority - b.priority);
}
// ============================================
// DOWNLOAD
// ============================================
/**
* Download a model from the best available source
* @param {object} metadata - Model metadata
* @param {object} options - Download options
* @returns {Promise<Buffer>}
*/
async download(metadata, options = {}) {
const { name, version, sources, size, hash } = metadata;
const key = `${name}@${version}`;
// Check for in-flight download
if (this.activeDownloads.has(key)) {
return this.activeDownloads.get(key);
}
const downloadPromise = this._executeDownload(metadata, options);
this.activeDownloads.set(key, downloadPromise);
try {
const result = await downloadPromise;
return result;
} finally {
this.activeDownloads.delete(key);
}
}
/**
* Execute the download with fallback
* @private
*/
async _executeDownload(metadata, options = {}) {
const { name, version, sources, size, hash } = metadata;
const sourceUrls = this.generateSourceUrls(sources, name, version);
const progress = new ProgressTracker(size);
if (options.onProgress) {
progress.on('progress', options.onProgress);
}
let lastError = null;
for (const source of sourceUrls) {
try {
this.emit('download_attempt', { source, model: name, version });
const data = await this._downloadFromUrl(source.url, {
...options,
progress,
expectedSize: size,
});
// Verify integrity
if (hash) {
const computedHash = `sha256:${createHash('sha256').update(data).digest('hex')}`;
if (computedHash !== hash) {
throw new Error(`Hash mismatch: expected ${hash}, got ${computedHash}`);
}
}
this.stats.downloads++;
this.stats.bytesDownloaded += data.length;
progress.complete();
this.emit('download_complete', {
source,
model: name,
version,
size: data.length,
});
return data;
} catch (error) {
lastError = error;
this.emit('download_failed', {
source,
model: name,
version,
error: error.message,
});
// Continue to next source
continue;
}
}
this.stats.failures++;
progress.fail(lastError);
throw new Error(`Failed to download ${name}@${version} from all sources: ${lastError?.message}`);
}
/**
* Download from a URL with streaming and progress
* @private
*/
_downloadFromUrl(url, options = {}) {
return new Promise((resolve, reject) => {
const { progress, expectedSize, timeout = 60000 } = options;
const parsedUrl = new URL(url);
const protocol = parsedUrl.protocol === 'https:' ? https : http;
const chunks = [];
let bytesReceived = 0;
const request = protocol.get(url, {
timeout,
headers: {
'User-Agent': 'RuVector-EdgeNet/1.0',
'Accept': 'application/octet-stream',
},
}, (response) => {
// Handle redirects
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
this._downloadFromUrl(response.headers.location, options)
.then(resolve)
.catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
return;
}
const contentLength = parseInt(response.headers['content-length'] || expectedSize || 0, 10);
if (progress && contentLength) {
progress.totalBytes = contentLength;
}
response.on('data', (chunk) => {
chunks.push(chunk);
bytesReceived += chunk.length;
if (progress) {
progress.update(chunk.length);
}
});
response.on('end', () => {
const data = Buffer.concat(chunks);
resolve(data);
});
response.on('error', reject);
});
request.on('error', reject);
request.on('timeout', () => {
request.destroy();
reject(new Error('Request timeout'));
});
});
}
/**
* Download to a file with streaming
* @param {object} metadata - Model metadata
* @param {string} destPath - Destination file path
* @param {object} options - Download options
*/
async downloadToFile(metadata, destPath, options = {}) {
const data = await this.download(metadata, options);
// Ensure directory exists
const dir = path.dirname(destPath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(destPath, data);
return {
path: destPath,
size: data.length,
};
}
// ============================================
// UPLOAD
// ============================================
/**
* Upload a model to Google Cloud Storage
* @param {Buffer} data - Model data
* @param {string} modelName - Model name
* @param {string} version - Version
* @param {object} options - Upload options
* @returns {Promise<string>} GCS URL
*/
async uploadToGcs(data, modelName, version, options = {}) {
const { filename = `${modelName}.onnx` } = options;
const gcsPath = `${modelName}/${version}/${filename}`;
// Check for @google-cloud/storage
let storage;
try {
const { Storage } = await import('@google-cloud/storage');
storage = new Storage({
projectId: this.gcsConfig.projectId,
keyFilename: this.gcsConfig.keyFilePath,
});
} catch (error) {
throw new Error('GCS upload requires @google-cloud/storage package');
}
const bucket = storage.bucket(this.gcsConfig.bucket);
const file = bucket.file(gcsPath);
const progress = new ProgressTracker(data.length);
if (options.onProgress) {
progress.on('progress', options.onProgress);
}
await new Promise((resolve, reject) => {
const stream = file.createWriteStream({
metadata: {
contentType: 'application/octet-stream',
metadata: {
modelName,
version,
hash: `sha256:${createHash('sha256').update(data).digest('hex')}`,
},
},
});
stream.on('error', reject);
stream.on('finish', resolve);
// Write in chunks for progress tracking
let offset = 0;
const writeChunk = () => {
while (offset < data.length) {
const end = Math.min(offset + CHUNK_SIZE, data.length);
const chunk = data.slice(offset, end);
if (!stream.write(chunk)) {
offset = end;
stream.once('drain', writeChunk);
return;
}
progress.update(chunk.length);
offset = end;
}
stream.end();
};
writeChunk();
});
progress.complete();
this.stats.uploads++;
this.stats.bytesUploaded += data.length;
const gcsUrl = this.getGcsUrl(modelName, version, filename);
this.emit('upload_complete', {
type: 'gcs',
url: gcsUrl,
model: modelName,
version,
size: data.length,
});
return gcsUrl;
}
/**
* Upload a model to IPFS via web3.storage
* @param {Buffer} data - Model data
* @param {string} modelName - Model name
* @param {string} version - Version
* @param {object} options - Upload options
* @returns {Promise<string>} IPFS CID
*/
async uploadToIpfs(data, modelName, version, options = {}) {
const { filename = `${modelName}.onnx`, provider = 'web3storage' } = options;
let cid;
if (provider === 'web3storage' && this.ipfsConfig.web3StorageToken) {
cid = await this._uploadToWeb3Storage(data, filename);
} else if (provider === 'nftstorage' && this.ipfsConfig.nftStorageToken) {
cid = await this._uploadToNftStorage(data, filename);
} else {
throw new Error('No IPFS provider configured. Set WEB3_STORAGE_TOKEN or NFT_STORAGE_TOKEN');
}
this.stats.uploads++;
this.stats.bytesUploaded += data.length;
const ipfsUrl = this.getIpfsUrl(cid);
this.emit('upload_complete', {
type: 'ipfs',
url: ipfsUrl,
cid,
model: modelName,
version,
size: data.length,
});
return ipfsUrl;
}
/**
* Upload to web3.storage
* @private
*/
async _uploadToWeb3Storage(data, filename) {
// web3.storage API upload
const response = await this._httpRequest('https://api.web3.storage/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.ipfsConfig.web3StorageToken}`,
'X-Name': filename,
},
body: data,
});
if (!response.cid) {
throw new Error('web3.storage upload failed: no CID returned');
}
return response.cid;
}
/**
* Upload to nft.storage
* @private
*/
async _uploadToNftStorage(data, filename) {
// nft.storage API upload
const response = await this._httpRequest('https://api.nft.storage/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.ipfsConfig.nftStorageToken}`,
},
body: data,
});
if (!response.value?.cid) {
throw new Error('nft.storage upload failed: no CID returned');
}
return response.value.cid;
}
/**
* Make an HTTP request
* @private
*/
_httpRequest(url, options = {}) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const protocol = parsedUrl.protocol === 'https:' ? https : http;
const requestOptions = {
method: options.method || 'GET',
headers: options.headers || {},
hostname: parsedUrl.hostname,
path: parsedUrl.pathname + parsedUrl.search,
port: parsedUrl.port,
};
const request = protocol.request(requestOptions, (response) => {
const chunks = [];
response.on('data', chunk => chunks.push(chunk));
response.on('end', () => {
const body = Buffer.concat(chunks).toString('utf-8');
if (response.statusCode >= 400) {
reject(new Error(`HTTP ${response.statusCode}: ${body}`));
return;
}
try {
resolve(JSON.parse(body));
} catch {
resolve(body);
}
});
});
request.on('error', reject);
if (options.body) {
request.write(options.body);
}
request.end();
});
}
// ============================================
// INTEGRITY VERIFICATION
// ============================================
/**
* Compute SHA256 hash of data
* @param {Buffer} data - Data to hash
* @returns {string} Hash string with sha256: prefix
*/
computeHash(data) {
return `sha256:${createHash('sha256').update(data).digest('hex')}`;
}
/**
* Verify data integrity against expected hash
* @param {Buffer} data - Data to verify
* @param {string} expectedHash - Expected hash
* @returns {boolean}
*/
verifyIntegrity(data, expectedHash) {
const computed = this.computeHash(data);
return computed === expectedHash;
}
/**
* Verify a downloaded model
* @param {Buffer} data - Model data
* @param {object} metadata - Model metadata
* @returns {object} Verification result
*/
verifyModel(data, metadata) {
const result = {
valid: true,
checks: [],
};
// Size check
if (metadata.size) {
const sizeMatch = data.length === metadata.size;
result.checks.push({
type: 'size',
expected: metadata.size,
actual: data.length,
passed: sizeMatch,
});
if (!sizeMatch) result.valid = false;
}
// Hash check
if (metadata.hash) {
const hashMatch = this.verifyIntegrity(data, metadata.hash);
result.checks.push({
type: 'hash',
expected: metadata.hash,
actual: this.computeHash(data),
passed: hashMatch,
});
if (!hashMatch) result.valid = false;
}
return result;
}
// ============================================
// STATS AND INFO
// ============================================
/**
* Get distribution manager stats
* @returns {object}
*/
getStats() {
return {
...this.stats,
activeDownloads: this.activeDownloads.size,
};
}
}
// ============================================
// DEFAULT EXPORT
// ============================================
export default DistributionManager;