Add files via upload
This commit is contained in:
commit
fc5884b7dc
82
eslint.config.js
Normal file
82
eslint.config.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import globals from 'globals';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import prettier from 'eslint-plugin-prettier';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// Base configuration for all files
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts}'],
|
||||||
|
ignores: ['dist/**', 'docs/**', 'src/components/bot/templates/**'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
NodeJS: 'readonly', // Add NodeJS as a global
|
||||||
|
Express: 'readonly', // Add Express as a global
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
prettier: prettier,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
...prettier.configs.recommended.rules,
|
||||||
|
camelcase: 'off',
|
||||||
|
'no-invalid-this': 'off',
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
'require-jsdoc': 'off',
|
||||||
|
'valid-jsdoc': 'off',
|
||||||
|
'new-cap': ['error', { capIsNew: false }],
|
||||||
|
'no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// TypeScript-specific configuration
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tseslint.plugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...tseslint.configs.recommended.rules,
|
||||||
|
'@typescript-eslint/naming-convention': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
selector: 'default',
|
||||||
|
format: ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE'],
|
||||||
|
leadingUnderscore: 'allow',
|
||||||
|
trailingUnderscore: 'allow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'property',
|
||||||
|
format: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
51
examples/basic-node.ts
Normal file
51
examples/basic-node.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { initIpfs, initOrbitDB, logger, createServiceLogger } from '../src/index';
|
||||||
|
|
||||||
|
const appLogger = createServiceLogger('APP');
|
||||||
|
|
||||||
|
async function startNode() {
|
||||||
|
try {
|
||||||
|
appLogger.info('Starting Debros node...');
|
||||||
|
|
||||||
|
// Initialize IPFS
|
||||||
|
const ipfs = await initIpfs();
|
||||||
|
appLogger.info('IPFS node initialized');
|
||||||
|
|
||||||
|
// Initialize OrbitDB
|
||||||
|
const ipfsService = {
|
||||||
|
getHelia: () => ipfs,
|
||||||
|
};
|
||||||
|
|
||||||
|
const orbitdb = await initOrbitDB(ipfsService);
|
||||||
|
appLogger.info('OrbitDB initialized');
|
||||||
|
|
||||||
|
// Create a test database
|
||||||
|
const db = await orbitdb.open('test-db', {
|
||||||
|
type: 'feed',
|
||||||
|
overwrite: false,
|
||||||
|
});
|
||||||
|
appLogger.info(`Database opened: ${db.address.toString()}`);
|
||||||
|
|
||||||
|
// Add some data
|
||||||
|
const hash = await db.add('Hello from Debros Network!');
|
||||||
|
appLogger.info(`Added entry: ${hash}`);
|
||||||
|
|
||||||
|
// Query data
|
||||||
|
const allEntries = db.iterator({ limit: 10 }).collect();
|
||||||
|
appLogger.info('Database entries:', allEntries);
|
||||||
|
|
||||||
|
// Keep the process running
|
||||||
|
appLogger.info('Node is running. Press Ctrl+C to stop.');
|
||||||
|
|
||||||
|
// Handle shutdown
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
appLogger.info('Shutting down...');
|
||||||
|
await orbitdb.stop();
|
||||||
|
await initIpfs.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
appLogger.error('Failed to start node:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startNode();
|
126
examples/integration-with-app.ts
Normal file
126
examples/integration-with-app.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Example of how to integrate the new @debros/node-core package into an application
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import { initIpfs, initOrbitDB, createServiceLogger, getConnectedPeers } from '@debros/node-core'; // This would be the import path when installed from npm
|
||||||
|
|
||||||
|
// Create service-specific loggers
|
||||||
|
const apiLogger = createServiceLogger('API');
|
||||||
|
const networkLogger = createServiceLogger('NETWORK');
|
||||||
|
|
||||||
|
// Initialize Express app
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Network state
|
||||||
|
let ipfsNode: any;
|
||||||
|
let orbitInstance: any;
|
||||||
|
let messageDB: any;
|
||||||
|
|
||||||
|
// Initialize network components
|
||||||
|
async function initializeNetwork() {
|
||||||
|
try {
|
||||||
|
// Initialize IPFS
|
||||||
|
networkLogger.info('Initializing IPFS node...');
|
||||||
|
ipfsNode = await initIpfs();
|
||||||
|
|
||||||
|
// Initialize OrbitDB
|
||||||
|
networkLogger.info('Initializing OrbitDB...');
|
||||||
|
orbitInstance = await initOrbitDB({
|
||||||
|
getHelia: () => ipfsNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open message database
|
||||||
|
messageDB = await orbitInstance.open('messages', {
|
||||||
|
type: 'feed',
|
||||||
|
});
|
||||||
|
|
||||||
|
networkLogger.info('Network components initialized successfully');
|
||||||
|
|
||||||
|
// Log connected peers every minute
|
||||||
|
setInterval(() => {
|
||||||
|
const peers = getConnectedPeers();
|
||||||
|
networkLogger.info(`Connected to ${peers.size} peers`);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
networkLogger.error('Failed to initialize network:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
app.get('/api/peers', (req, res) => {
|
||||||
|
const peers = getConnectedPeers();
|
||||||
|
const peerList: any[] = [];
|
||||||
|
|
||||||
|
peers.forEach((data, peerId) => {
|
||||||
|
peerList.push({
|
||||||
|
id: peerId,
|
||||||
|
load: data.load,
|
||||||
|
address: data.publicAddress,
|
||||||
|
lastSeen: data.lastSeen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ peers: peerList });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/messages', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const messages = messageDB.iterator({ limit: 100 }).collect();
|
||||||
|
res.json({ messages });
|
||||||
|
} catch (error) {
|
||||||
|
apiLogger.error('Error fetching messages:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch messages' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/messages', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { content } = req.body;
|
||||||
|
if (!content) {
|
||||||
|
return res.status(400).json({ error: 'Content is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await messageDB.add({
|
||||||
|
content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({ id: entry });
|
||||||
|
} catch (error) {
|
||||||
|
apiLogger.error('Error creating message:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create message' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the application
|
||||||
|
async function startApp() {
|
||||||
|
const networkInitialized = await initializeNetwork();
|
||||||
|
|
||||||
|
if (networkInitialized) {
|
||||||
|
const port = config.env.port;
|
||||||
|
app.listen(port, () => {
|
||||||
|
apiLogger.info(`Server listening on port ${port}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
apiLogger.error('Cannot start application: Network initialization failed');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown handler
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
networkLogger.info('Application shutting down...');
|
||||||
|
if (orbitInstance) {
|
||||||
|
await orbitInstance.stop();
|
||||||
|
}
|
||||||
|
if (ipfsNode) {
|
||||||
|
await initIpfs.stop();
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the application
|
||||||
|
startApp();
|
14
orbitdb.d.ts
vendored
Normal file
14
orbitdb.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// custom.d.ts
|
||||||
|
declare module '@orbitdb/core' {
|
||||||
|
// Import the types from @constl/orbit-db-types
|
||||||
|
import { OrbitDBTypes } from '@constl/orbit-db-types';
|
||||||
|
|
||||||
|
// Assuming @orbitdb/core exports an interface or type you want to override
|
||||||
|
// Replace 'OrbitDB' with the actual export name from @orbitdb/core you want to type
|
||||||
|
export interface OrbitDB extends OrbitDBTypes {
|
||||||
|
// You can add additional properties or methods here if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// If @orbitdb/core exports a default export, you might need something like:
|
||||||
|
// export default interface OrbitDBDefault extends OrbitDBTypes {}
|
||||||
|
}
|
74
package.json
Normal file
74
package.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "@DeBrosOfficial/network",
|
||||||
|
"version": "0.0.10",
|
||||||
|
"description": "Debros network core functionality for IPFS, libp2p and OrbitDB",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"typings": "types.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"types.d.ts"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc -w",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"prepublishOnly": "npm run clean && npm run build",
|
||||||
|
"prepare": "husky",
|
||||||
|
"lint": "npx eslint src"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ipfs",
|
||||||
|
"libp2p",
|
||||||
|
"orbitdb",
|
||||||
|
"decentralized",
|
||||||
|
"p2p"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@chainsafe/libp2p-gossipsub": "^14.1.0",
|
||||||
|
"@chainsafe/libp2p-noise": "^16.1.0",
|
||||||
|
"@chainsafe/libp2p-yamux": "^7.0.1",
|
||||||
|
"@libp2p/bootstrap": "^11.0.32",
|
||||||
|
"@libp2p/crypto": "^5.0.15",
|
||||||
|
"@libp2p/identify": "^3.0.27",
|
||||||
|
"@libp2p/interface": "^2.7.0",
|
||||||
|
"@libp2p/mdns": "^11.0.32",
|
||||||
|
"@libp2p/peer-id": "^5.1.0",
|
||||||
|
"@libp2p/pubsub": "^10.1.8",
|
||||||
|
"@libp2p/tcp": "^10.1.8",
|
||||||
|
"@multiformats/multiaddr": "^12.4.0",
|
||||||
|
"@orbitdb/core": "^2.5.0",
|
||||||
|
"@orbitdb/feed-db": "^1.1.2",
|
||||||
|
"blockstore-fs": "^2.0.2",
|
||||||
|
"helia": "^5.3.0",
|
||||||
|
"libp2p": "^2.8.2",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
|
"winston": "^3.17.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@constl/orbit-db-types": "^2.0.6",
|
||||||
|
"@orbitdb/core-types": "^1.0.14",
|
||||||
|
"@types/node": "^22.13.10",
|
||||||
|
"@types/node-forge": "^1.3.11",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"lint-staged": "^15.5.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
|
"typescript": "^5.8.2"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./node_modules/@constl/orbit-db-types"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"@constl/orbit-db-types"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
73
src/config.ts
Normal file
73
src/config.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export interface DebrosConfig {
|
||||||
|
env: {
|
||||||
|
isDevelopment: boolean;
|
||||||
|
port: string | number;
|
||||||
|
fingerprint: string;
|
||||||
|
nickname?: string;
|
||||||
|
keyPath: string;
|
||||||
|
host: string;
|
||||||
|
};
|
||||||
|
features: {
|
||||||
|
enableLoadBalancing: boolean;
|
||||||
|
};
|
||||||
|
ipfs: {
|
||||||
|
repo: string;
|
||||||
|
swarmKey: string;
|
||||||
|
bootstrapNodes?: string;
|
||||||
|
blockstorePath: string;
|
||||||
|
serviceDiscovery: {
|
||||||
|
topic: string;
|
||||||
|
heartbeatInterval: number;
|
||||||
|
staleTimeout: number;
|
||||||
|
logInterval: number;
|
||||||
|
publicAddress: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
orbitdb: {
|
||||||
|
directory: string;
|
||||||
|
};
|
||||||
|
loadBalancer: {
|
||||||
|
maxConnections: number;
|
||||||
|
strategy: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default configuration values
|
||||||
|
export const defaultConfig: DebrosConfig = {
|
||||||
|
env: {
|
||||||
|
isDevelopment: process.env.NODE_ENV !== 'production',
|
||||||
|
port: process.env.PORT || 7777,
|
||||||
|
fingerprint: process.env.FINGERPRINT || 'default-fingerprint',
|
||||||
|
nickname: process.env.NICKNAME,
|
||||||
|
keyPath: process.env.KEY_PATH || '/var/lib/debros/keys',
|
||||||
|
host: process.env.HOST || '',
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
enableLoadBalancing: process.env.ENABLE_LOAD_BALANCING !== 'false',
|
||||||
|
},
|
||||||
|
ipfs: {
|
||||||
|
repo: './ipfs-repo',
|
||||||
|
swarmKey: path.resolve(process.cwd(), 'swarm.key'),
|
||||||
|
bootstrapNodes: process.env.BOOTSTRAP_NODES,
|
||||||
|
blockstorePath: path.resolve(process.cwd(), 'blockstore'),
|
||||||
|
serviceDiscovery: {
|
||||||
|
topic: process.env.SERVICE_DISCOVERY_TOPIC || 'debros-service-discovery',
|
||||||
|
heartbeatInterval: parseInt(process.env.HEARTBEAT_INTERVAL || '5000'),
|
||||||
|
staleTimeout: parseInt(process.env.STALE_PEER_TIMEOUT || '30000'),
|
||||||
|
logInterval: parseInt(process.env.PEER_LOG_INTERVAL || '60000'),
|
||||||
|
publicAddress: process.env.NODE_PUBLIC_ADDRESS || `http://localhost:${process.env.PORT || 7777}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orbitdb: {
|
||||||
|
directory: path.resolve(process.cwd(), 'orbitdb/debros'),
|
||||||
|
},
|
||||||
|
loadBalancer: {
|
||||||
|
maxConnections: parseInt(process.env.MAX_CONNECTIONS || '1000'),
|
||||||
|
strategy: process.env.LOAD_BALANCING_STRATEGY || 'least-loaded',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export a singleton config
|
||||||
|
export const config = defaultConfig;
|
98
src/index.ts
Normal file
98
src/index.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// Config exports
|
||||||
|
import { config, defaultConfig, type DebrosConfig } from './config';
|
||||||
|
import { validateConfig, type ValidationResult } from './ipfs/config/configValidator';
|
||||||
|
|
||||||
|
// IPFS exports
|
||||||
|
import ipfsService, {
|
||||||
|
init as initIpfs,
|
||||||
|
stop as stopIpfs,
|
||||||
|
getHelia,
|
||||||
|
getProxyAgent,
|
||||||
|
getInstance,
|
||||||
|
getLibp2p,
|
||||||
|
getConnectedPeers,
|
||||||
|
getOptimalPeer,
|
||||||
|
updateNodeLoad,
|
||||||
|
logPeersStatus,
|
||||||
|
type IPFSModule,
|
||||||
|
} from './ipfs/ipfsService';
|
||||||
|
|
||||||
|
import { ipfsConfig, getIpfsPort, getBlockstorePath } from './ipfs/config/ipfsConfig';
|
||||||
|
|
||||||
|
// OrbitDB exports
|
||||||
|
import orbitDBService, {
|
||||||
|
init as initOrbitDB,
|
||||||
|
openDB,
|
||||||
|
getOrbitDB,
|
||||||
|
db as orbitDB,
|
||||||
|
getOrbitDBDir,
|
||||||
|
getDBAddress,
|
||||||
|
saveDBAddress,
|
||||||
|
} from './orbit/orbitDBService';
|
||||||
|
|
||||||
|
import loadBalancerControllerDefault from './ipfs/loadBalancerController';
|
||||||
|
export const loadBalancerController = loadBalancerControllerDefault;
|
||||||
|
|
||||||
|
// Logger exports
|
||||||
|
import logger, { createServiceLogger, createDebrosLogger, type LoggerOptions } from './utils/logger';
|
||||||
|
|
||||||
|
// Crypto exports
|
||||||
|
import { getPrivateKey } from './ipfs/utils/crypto';
|
||||||
|
|
||||||
|
// Export everything
|
||||||
|
export {
|
||||||
|
// Config
|
||||||
|
config,
|
||||||
|
defaultConfig,
|
||||||
|
validateConfig,
|
||||||
|
type DebrosConfig,
|
||||||
|
type ValidationResult,
|
||||||
|
|
||||||
|
// IPFS
|
||||||
|
ipfsService,
|
||||||
|
initIpfs,
|
||||||
|
stopIpfs,
|
||||||
|
getHelia,
|
||||||
|
getProxyAgent,
|
||||||
|
getInstance,
|
||||||
|
getLibp2p,
|
||||||
|
getConnectedPeers,
|
||||||
|
getOptimalPeer,
|
||||||
|
updateNodeLoad,
|
||||||
|
logPeersStatus,
|
||||||
|
type IPFSModule,
|
||||||
|
|
||||||
|
// IPFS Config
|
||||||
|
ipfsConfig,
|
||||||
|
getIpfsPort,
|
||||||
|
getBlockstorePath,
|
||||||
|
|
||||||
|
// OrbitDB
|
||||||
|
orbitDBService,
|
||||||
|
initOrbitDB,
|
||||||
|
openDB,
|
||||||
|
getOrbitDB,
|
||||||
|
orbitDB,
|
||||||
|
getOrbitDBDir,
|
||||||
|
getDBAddress,
|
||||||
|
saveDBAddress,
|
||||||
|
|
||||||
|
// Logger
|
||||||
|
logger,
|
||||||
|
createServiceLogger,
|
||||||
|
createDebrosLogger,
|
||||||
|
type LoggerOptions,
|
||||||
|
|
||||||
|
// Crypto
|
||||||
|
getPrivateKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default export for convenience
|
||||||
|
export default {
|
||||||
|
config,
|
||||||
|
validateConfig,
|
||||||
|
ipfsService,
|
||||||
|
orbitDBService,
|
||||||
|
logger,
|
||||||
|
createServiceLogger,
|
||||||
|
};
|
44
src/ipfs/config/configValidator.ts
Normal file
44
src/ipfs/config/configValidator.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { config } from '../../config';
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the IPFS configuration
|
||||||
|
*/
|
||||||
|
export const validateConfig = (): ValidationResult => {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Check fingerprint
|
||||||
|
if (!config.env.fingerprint || config.env.fingerprint === 'default-fingerprint') {
|
||||||
|
errors.push('Fingerprint not set or using default value. Please set a unique fingerprint.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check port
|
||||||
|
const port = Number(config.env.port);
|
||||||
|
if (isNaN(port) || port < 1 || port > 65535) {
|
||||||
|
errors.push('Invalid port configuration. Port must be a number between 1 and 65535.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check service discovery topic
|
||||||
|
if (!config.ipfs.serviceDiscovery.topic) {
|
||||||
|
errors.push('Service discovery topic not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check blockstore path
|
||||||
|
if (!config.ipfs.blockstorePath) {
|
||||||
|
errors.push('Blockstore path not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check orbitdb directory
|
||||||
|
if (!config.orbitdb.directory) {
|
||||||
|
errors.push('OrbitDB directory not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
};
|
31
src/ipfs/config/ipfsConfig.ts
Normal file
31
src/ipfs/config/ipfsConfig.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { config } from '../../config';
|
||||||
|
|
||||||
|
// Determine the IPFS port to use
|
||||||
|
export const getIpfsPort = (): number => {
|
||||||
|
if (process.env.IPFS_PORT) {
|
||||||
|
return parseInt(process.env.IPFS_PORT);
|
||||||
|
}
|
||||||
|
const httpPort = parseInt(process.env.PORT || '7777');
|
||||||
|
return httpPort + 1; // Default to HTTP port + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get a node-specific blockstore path
|
||||||
|
export const getBlockstorePath = (): string => {
|
||||||
|
const basePath = config.ipfs.blockstorePath;
|
||||||
|
const fingerprint = config.env.fingerprint;
|
||||||
|
return `${basePath}-${fingerprint}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// IPFS configuration
|
||||||
|
export const ipfsConfig = {
|
||||||
|
blockstorePath: getBlockstorePath(),
|
||||||
|
port: getIpfsPort(),
|
||||||
|
serviceDiscovery: {
|
||||||
|
topic: config.ipfs.serviceDiscovery.topic,
|
||||||
|
heartbeatInterval: config.ipfs.serviceDiscovery.heartbeatInterval || 2000,
|
||||||
|
staleTimeout: config.ipfs.serviceDiscovery.staleTimeout || 30000,
|
||||||
|
logInterval: config.ipfs.serviceDiscovery.logInterval || 60000,
|
||||||
|
publicAddress: config.ipfs.serviceDiscovery.publicAddress,
|
||||||
|
},
|
||||||
|
bootstrapNodes: process.env.BOOTSTRAP_NODES,
|
||||||
|
};
|
99
src/ipfs/ipfsService.ts
Normal file
99
src/ipfs/ipfsService.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import type { Libp2p } from 'libp2p';
|
||||||
|
|
||||||
|
import {
|
||||||
|
initIpfsNode,
|
||||||
|
stopIpfsNode,
|
||||||
|
getHeliaInstance,
|
||||||
|
getLibp2pInstance,
|
||||||
|
getProxyAgentInstance,
|
||||||
|
} from './services/ipfsCoreService';
|
||||||
|
|
||||||
|
import { getConnectedPeers, getOptimalPeer, updateNodeLoad, logPeersStatus } from './services/discoveryService';
|
||||||
|
import { createServiceLogger } from '../utils/logger';
|
||||||
|
|
||||||
|
// Create logger for IPFS service
|
||||||
|
const logger = createServiceLogger('IPFS');
|
||||||
|
|
||||||
|
// Interface definition for the IPFS module
|
||||||
|
export interface IPFSModule {
|
||||||
|
init: (externalProxyAgent?: any) => Promise<void>;
|
||||||
|
stop: () => Promise<void>;
|
||||||
|
getHelia: () => any;
|
||||||
|
getProxyAgent: () => any;
|
||||||
|
getInstance: (externalProxyAgent?: any) => Promise<{
|
||||||
|
getHelia: () => any;
|
||||||
|
getProxyAgent: () => any;
|
||||||
|
}>;
|
||||||
|
getLibp2p: () => Libp2p;
|
||||||
|
getConnectedPeers: () => Map<string, { lastSeen: number; load: number; publicAddress: string }>;
|
||||||
|
getOptimalPeer: () => string | null;
|
||||||
|
updateNodeLoad: (load: number) => void;
|
||||||
|
logPeersStatus: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = async (externalProxyAgent: any = null) => {
|
||||||
|
try {
|
||||||
|
await initIpfsNode(externalProxyAgent);
|
||||||
|
logger.info('IPFS service initialized successfully');
|
||||||
|
return getHeliaInstance();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to initialize IPFS service:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stop = async () => {
|
||||||
|
await stopIpfsNode();
|
||||||
|
logger.info('IPFS service stopped');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHelia = () => {
|
||||||
|
return getHeliaInstance();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProxyAgent = () => {
|
||||||
|
return getProxyAgentInstance();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLibp2p = () => {
|
||||||
|
return getLibp2pInstance();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstance = async (externalProxyAgent: any = null) => {
|
||||||
|
if (!getHeliaInstance()) {
|
||||||
|
await init(externalProxyAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getHelia,
|
||||||
|
getProxyAgent,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export individual functions
|
||||||
|
export {
|
||||||
|
init,
|
||||||
|
stop,
|
||||||
|
getHelia,
|
||||||
|
getProxyAgent,
|
||||||
|
getInstance,
|
||||||
|
getLibp2p,
|
||||||
|
getConnectedPeers,
|
||||||
|
getOptimalPeer,
|
||||||
|
updateNodeLoad,
|
||||||
|
logPeersStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export as default module
|
||||||
|
export default {
|
||||||
|
init,
|
||||||
|
stop,
|
||||||
|
getHelia,
|
||||||
|
getProxyAgent,
|
||||||
|
getInstance,
|
||||||
|
getLibp2p,
|
||||||
|
getConnectedPeers,
|
||||||
|
getOptimalPeer,
|
||||||
|
updateNodeLoad,
|
||||||
|
logPeersStatus,
|
||||||
|
} as IPFSModule;
|
107
src/ipfs/loadBalancerController.ts
Normal file
107
src/ipfs/loadBalancerController.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// Load balancer controller - Handles API routes for service discovery and load balancing
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import loadBalancerService from './loadBalancerService';
|
||||||
|
import { config } from '../config';
|
||||||
|
|
||||||
|
export interface LoadBalancerControllerModule {
|
||||||
|
getNodeInfo: (_req: Request, _res: Response, _next: NextFunction) => void;
|
||||||
|
getOptimalPeer: (_req: Request, _res: Response, _next: NextFunction) => void;
|
||||||
|
getAllPeers: (_req: Request, _res: Response, _next: NextFunction) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about the node and its load
|
||||||
|
*/
|
||||||
|
const getNodeInfo = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const status = loadBalancerService.getNodeStatus();
|
||||||
|
res.json({
|
||||||
|
fingerprint: config.env.fingerprint,
|
||||||
|
peerCount: status.peerCount,
|
||||||
|
isLoadBalancer: config.features.enableLoadBalancing,
|
||||||
|
loadBalancerStrategy: config.loadBalancer.strategy,
|
||||||
|
maxConnections: config.loadBalancer.maxConnections,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the optimal peer for client connection
|
||||||
|
*/
|
||||||
|
const getOptimalPeer = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
// Check if load balancing is enabled
|
||||||
|
if (!config.features.enableLoadBalancing) {
|
||||||
|
res.status(200).json({
|
||||||
|
useThisNode: true,
|
||||||
|
message: 'Load balancing is disabled, use this node',
|
||||||
|
fingerprint: config.env.fingerprint,
|
||||||
|
publicAddress: config.ipfs.serviceDiscovery.publicAddress,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the optimal peer
|
||||||
|
const optimalPeer = loadBalancerService.getOptimalPeer();
|
||||||
|
|
||||||
|
// If there are no peer nodes, use this node
|
||||||
|
if (!optimalPeer) {
|
||||||
|
res.status(200).json({
|
||||||
|
useThisNode: true,
|
||||||
|
message: 'No other peers available, use this node',
|
||||||
|
fingerprint: config.env.fingerprint,
|
||||||
|
publicAddress: config.ipfs.serviceDiscovery.publicAddress,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this node is the optimal peer
|
||||||
|
const isThisNodeOptimal = optimalPeer.peerId === config.env.fingerprint;
|
||||||
|
|
||||||
|
if (isThisNodeOptimal) {
|
||||||
|
res.status(200).json({
|
||||||
|
useThisNode: true,
|
||||||
|
message: 'This node is optimal',
|
||||||
|
fingerprint: config.env.fingerprint,
|
||||||
|
publicAddress: config.ipfs.serviceDiscovery.publicAddress,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the optimal peer information
|
||||||
|
res.status(200).json({
|
||||||
|
useThisNode: false,
|
||||||
|
optimalPeer: {
|
||||||
|
peerId: optimalPeer.peerId,
|
||||||
|
load: optimalPeer.load,
|
||||||
|
publicAddress: optimalPeer.publicAddress,
|
||||||
|
},
|
||||||
|
message: 'Found optimal peer',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available peers
|
||||||
|
*/
|
||||||
|
const getAllPeers = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const peers = loadBalancerService.getAllPeers();
|
||||||
|
res.status(200).json({
|
||||||
|
peerCount: peers.length,
|
||||||
|
peers,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getNodeInfo,
|
||||||
|
getOptimalPeer,
|
||||||
|
getAllPeers,
|
||||||
|
} as LoadBalancerControllerModule;
|
143
src/ipfs/loadBalancerService.ts
Normal file
143
src/ipfs/loadBalancerService.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// Load balancer service - Implements load balancing strategies for distributing connections
|
||||||
|
import * as ipfsService from './ipfsService';
|
||||||
|
import { config } from '../config';
|
||||||
|
|
||||||
|
// Track last peer chosen for round-robin strategy
|
||||||
|
let lastPeerIndex = -1;
|
||||||
|
|
||||||
|
interface PeerInfo {
|
||||||
|
peerId: string;
|
||||||
|
load: number;
|
||||||
|
publicAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PeerStatus extends PeerInfo {
|
||||||
|
lastSeen: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeStatus {
|
||||||
|
fingerprint: string;
|
||||||
|
peerCount: number;
|
||||||
|
isHealthy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoadBalancerServiceModule {
|
||||||
|
getOptimalPeer: () => PeerInfo | null;
|
||||||
|
getAllPeers: () => PeerStatus[];
|
||||||
|
getNodeStatus: () => NodeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the optimal peer based on the configured load balancing strategy
|
||||||
|
* @returns Object containing the selected peer information or null if no peers available
|
||||||
|
*/
|
||||||
|
export const getOptimalPeer = (): { peerId: string; load: number; publicAddress: string } | null => {
|
||||||
|
// Get all available peers
|
||||||
|
const connectedPeers = ipfsService.getConnectedPeers();
|
||||||
|
|
||||||
|
// If no peers are available, return null
|
||||||
|
if (connectedPeers.size === 0) {
|
||||||
|
console.log('[LOAD-BALANCER] No peers available for load balancing');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Map to Array for easier manipulation
|
||||||
|
const peersArray = Array.from(connectedPeers.entries()).map(([peerId, data]) => {
|
||||||
|
return {
|
||||||
|
peerId,
|
||||||
|
load: data.load,
|
||||||
|
lastSeen: data.lastSeen,
|
||||||
|
publicAddress: data.publicAddress,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply the load balancing strategy
|
||||||
|
const strategy = config.loadBalancer.strategy;
|
||||||
|
let selectedPeer;
|
||||||
|
|
||||||
|
switch (strategy) {
|
||||||
|
case 'least-loaded':
|
||||||
|
// Find the peer with the lowest load
|
||||||
|
selectedPeer = peersArray.reduce((min, current) => (current.load < min.load ? current : min), peersArray[0]);
|
||||||
|
console.log(
|
||||||
|
`[LOAD-BALANCER] Selected least loaded peer: ${selectedPeer.peerId.substring(0, 15)}... with load ${
|
||||||
|
selectedPeer.load
|
||||||
|
}%`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'round-robin':
|
||||||
|
// Simple round-robin strategy
|
||||||
|
lastPeerIndex = (lastPeerIndex + 1) % peersArray.length;
|
||||||
|
selectedPeer = peersArray[lastPeerIndex];
|
||||||
|
console.log(
|
||||||
|
`[LOAD-BALANCER] Selected round-robin peer: ${selectedPeer.peerId.substring(0, 15)}... with load ${
|
||||||
|
selectedPeer.load
|
||||||
|
}%`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'random':
|
||||||
|
// Random selection
|
||||||
|
const randomIndex = Math.floor(Math.random() * peersArray.length);
|
||||||
|
selectedPeer = peersArray[randomIndex];
|
||||||
|
console.log(
|
||||||
|
`[LOAD-BALANCER] Selected random peer: ${selectedPeer.peerId.substring(0, 15)}... with load ${
|
||||||
|
selectedPeer.load
|
||||||
|
}%`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Default to least-loaded if unknown strategy
|
||||||
|
selectedPeer = peersArray.reduce((min, current) => (current.load < min.load ? current : min), peersArray[0]);
|
||||||
|
console.log(
|
||||||
|
`[LOAD-BALANCER] Selected least loaded peer: ${selectedPeer.peerId.substring(0, 15)}... with load ${
|
||||||
|
selectedPeer.load
|
||||||
|
}%`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
peerId: selectedPeer.peerId,
|
||||||
|
load: selectedPeer.load,
|
||||||
|
publicAddress: selectedPeer.publicAddress,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available peers with their load information
|
||||||
|
* @returns Array of peer information objects
|
||||||
|
*/
|
||||||
|
export const getAllPeers = () => {
|
||||||
|
const connectedPeers = ipfsService.getConnectedPeers();
|
||||||
|
const result: any = [];
|
||||||
|
|
||||||
|
connectedPeers.forEach((data, peerId) => {
|
||||||
|
result.push({
|
||||||
|
peerId,
|
||||||
|
load: data.load,
|
||||||
|
lastSeen: data.lastSeen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about the current node's load
|
||||||
|
*/
|
||||||
|
export const getNodeStatus = () => {
|
||||||
|
const connectedPeers = ipfsService.getConnectedPeers();
|
||||||
|
return {
|
||||||
|
fingerprint: config.env.fingerprint,
|
||||||
|
peerCount: connectedPeers.size,
|
||||||
|
isHealthy: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getOptimalPeer,
|
||||||
|
getAllPeers,
|
||||||
|
getNodeStatus,
|
||||||
|
} as LoadBalancerServiceModule;
|
147
src/ipfs/services/discoveryService.ts
Normal file
147
src/ipfs/services/discoveryService.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import type { PubSub } from '@libp2p/interface';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { ipfsConfig } from '../config/ipfsConfig';
|
||||||
|
import { createServiceLogger } from '../../utils/logger';
|
||||||
|
|
||||||
|
// Create loggers for service discovery and heartbeat
|
||||||
|
const discoveryLogger = createServiceLogger('SERVICE-DISCOVERY');
|
||||||
|
const heartbeatLogger = createServiceLogger('HEARTBEAT');
|
||||||
|
|
||||||
|
// Node metadata
|
||||||
|
const fingerprint = config.env.fingerprint;
|
||||||
|
|
||||||
|
const connectedPeers: Map<string, { lastSeen: number; load: number; publicAddress: string; fingerprint: string }> =
|
||||||
|
new Map();
|
||||||
|
const SERVICE_DISCOVERY_TOPIC = ipfsConfig.serviceDiscovery.topic;
|
||||||
|
const HEARTBEAT_INTERVAL = ipfsConfig.serviceDiscovery.heartbeatInterval;
|
||||||
|
let heartbeatInterval: NodeJS.Timeout;
|
||||||
|
let nodeLoad = 0;
|
||||||
|
|
||||||
|
export const setupServiceDiscovery = async (pubsub: PubSub) => {
|
||||||
|
await pubsub.subscribe(SERVICE_DISCOVERY_TOPIC);
|
||||||
|
discoveryLogger.info(`Subscribed to topic: ${SERVICE_DISCOVERY_TOPIC}`);
|
||||||
|
|
||||||
|
// Listen for other peers heartbeats
|
||||||
|
pubsub.addEventListener('message', (event: any) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.detail.data.toString());
|
||||||
|
if (message.type === 'heartbeat' && message.fingerprint !== fingerprint) {
|
||||||
|
const peerId = event.detail.from.toString();
|
||||||
|
const existingPeer = connectedPeers.has(peerId);
|
||||||
|
|
||||||
|
connectedPeers.set(peerId, {
|
||||||
|
lastSeen: Date.now(),
|
||||||
|
load: message.load,
|
||||||
|
publicAddress: message.publicAddress,
|
||||||
|
fingerprint: message.fingerprint,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingPeer) {
|
||||||
|
discoveryLogger.info(`New peer discovered: ${peerId} (fingerprint=${message.fingerprint})`);
|
||||||
|
}
|
||||||
|
heartbeatLogger.info(`Received from ${peerId}: load=${message.load}, addr=${message.publicAddress}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
discoveryLogger.error(`Error processing message:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send periodic heartbeats with our load information
|
||||||
|
heartbeatInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
nodeLoad = calculateNodeLoad();
|
||||||
|
const heartbeatMsg = {
|
||||||
|
type: 'heartbeat',
|
||||||
|
fingerprint,
|
||||||
|
load: nodeLoad,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
publicAddress: ipfsConfig.serviceDiscovery.publicAddress,
|
||||||
|
};
|
||||||
|
|
||||||
|
await pubsub.publish(SERVICE_DISCOVERY_TOPIC, new TextEncoder().encode(JSON.stringify(heartbeatMsg)));
|
||||||
|
heartbeatLogger.info(`Sent: fingerprint=${fingerprint}, load=${nodeLoad}, addr=${heartbeatMsg.publicAddress}`);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const staleTime = ipfsConfig.serviceDiscovery.staleTimeout;
|
||||||
|
|
||||||
|
for (const [peerId, peerData] of connectedPeers.entries()) {
|
||||||
|
if (now - peerData.lastSeen > staleTime) {
|
||||||
|
discoveryLogger.info(`Peer ${peerId.substring(0, 15)}... is stale, removing from load balancer`);
|
||||||
|
connectedPeers.delete(peerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() % 60000 < HEARTBEAT_INTERVAL) {
|
||||||
|
logPeersStatus();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
discoveryLogger.error(`Error sending heartbeat:`, err);
|
||||||
|
}
|
||||||
|
}, HEARTBEAT_INTERVAL);
|
||||||
|
|
||||||
|
discoveryLogger.info(`Service initialized with fingerprint: ${fingerprint}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the current node load
|
||||||
|
*/
|
||||||
|
export const calculateNodeLoad = (): number => {
|
||||||
|
// This is a simple implementation and could be enhanced with
|
||||||
|
// actual metrics like CPU usage, memory, active connections, etc.
|
||||||
|
return Math.floor(Math.random() * 100); // Placeholder implementation
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the status of connected peers
|
||||||
|
*/
|
||||||
|
export const logPeersStatus = () => {
|
||||||
|
const peerCount = connectedPeers.size;
|
||||||
|
discoveryLogger.info(`Connected peers: ${peerCount}`);
|
||||||
|
discoveryLogger.info(`Current node load: ${nodeLoad}`);
|
||||||
|
|
||||||
|
if (peerCount > 0) {
|
||||||
|
discoveryLogger.info('Peer status:');
|
||||||
|
connectedPeers.forEach((data, peerId) => {
|
||||||
|
discoveryLogger.debug(` - ${peerId} Load: ${data.load}% Last seen: ${new Date(data.lastSeen).toISOString()}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOptimalPeer = (): string | null => {
|
||||||
|
if (connectedPeers.size === 0) return null;
|
||||||
|
|
||||||
|
let lowestLoad = Number.MAX_SAFE_INTEGER;
|
||||||
|
let optimalPeer: string | null = null;
|
||||||
|
|
||||||
|
connectedPeers.forEach((data, peerId) => {
|
||||||
|
if (data.load < lowestLoad) {
|
||||||
|
lowestLoad = data.load;
|
||||||
|
optimalPeer = peerId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return optimalPeer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateNodeLoad = (load: number) => {
|
||||||
|
nodeLoad = load;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getConnectedPeers = () => {
|
||||||
|
return connectedPeers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stopDiscoveryService = async (pubsub: PubSub | null) => {
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
clearInterval(heartbeatInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pubsub) {
|
||||||
|
try {
|
||||||
|
await pubsub.unsubscribe(SERVICE_DISCOVERY_TOPIC);
|
||||||
|
discoveryLogger.info(`Unsubscribed from topic: ${SERVICE_DISCOVERY_TOPIC}`);
|
||||||
|
} catch (err) {
|
||||||
|
discoveryLogger.error(`Error unsubscribing from topic:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
215
src/ipfs/services/ipfsCoreService.ts
Normal file
215
src/ipfs/services/ipfsCoreService.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { createHelia } from 'helia';
|
||||||
|
import { FsBlockstore } from 'blockstore-fs';
|
||||||
|
import { createLibp2p } from 'libp2p';
|
||||||
|
import { gossipsub } from '@chainsafe/libp2p-gossipsub';
|
||||||
|
import { tcp } from '@libp2p/tcp';
|
||||||
|
import { noise } from '@chainsafe/libp2p-noise';
|
||||||
|
import { yamux } from '@chainsafe/libp2p-yamux';
|
||||||
|
import { identify } from '@libp2p/identify';
|
||||||
|
import { bootstrap } from '@libp2p/bootstrap';
|
||||||
|
import type { Libp2p } from 'libp2p';
|
||||||
|
import { FaultTolerance, PubSub } from '@libp2p/interface';
|
||||||
|
|
||||||
|
import { ipfsConfig } from '../config/ipfsConfig';
|
||||||
|
import { getPrivateKey } from '../utils/crypto';
|
||||||
|
import { setupServiceDiscovery, stopDiscoveryService } from './discoveryService';
|
||||||
|
import { createServiceLogger } from '../../utils/logger';
|
||||||
|
|
||||||
|
const logger = createServiceLogger('IPFS');
|
||||||
|
const p2pLogger = createServiceLogger('P2P');
|
||||||
|
|
||||||
|
let helia: any;
|
||||||
|
let proxyAgent: any;
|
||||||
|
let libp2pNode: Libp2p;
|
||||||
|
let reconnectInterval: NodeJS.Timeout;
|
||||||
|
|
||||||
|
export const initIpfsNode = async (externalProxyAgent: any = null) => {
|
||||||
|
try {
|
||||||
|
proxyAgent = externalProxyAgent;
|
||||||
|
|
||||||
|
const blockstorePath = ipfsConfig.blockstorePath;
|
||||||
|
if (!fs.existsSync(blockstorePath)) {
|
||||||
|
fs.mkdirSync(blockstorePath, { recursive: true });
|
||||||
|
logger.info(`Created blockstore directory: ${blockstorePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockstore = new FsBlockstore(blockstorePath);
|
||||||
|
|
||||||
|
const currentNodeIp = process.env.HOSTNAME || '';
|
||||||
|
logger.info(`Current node public IP: ${currentNodeIp}`);
|
||||||
|
|
||||||
|
const bootstrapList = getBootstrapList();
|
||||||
|
logger.info(`Bootstrap peers: ${JSON.stringify(bootstrapList)}`);
|
||||||
|
|
||||||
|
const bootStrap = bootstrap({
|
||||||
|
list: bootstrapList,
|
||||||
|
}) as unknown as any;
|
||||||
|
|
||||||
|
logger.info(`Configuring bootstrap with peers: ${JSON.stringify(bootstrapList)}`);
|
||||||
|
|
||||||
|
const ipfsPort = ipfsConfig.port;
|
||||||
|
logger.info(`Using port ${ipfsPort} for IPFS/libp2p`);
|
||||||
|
|
||||||
|
libp2pNode = await createLibp2p({
|
||||||
|
transports: [tcp()],
|
||||||
|
streamMuxers: [yamux()],
|
||||||
|
connectionEncrypters: [noise()],
|
||||||
|
services: {
|
||||||
|
identify: identify(),
|
||||||
|
pubsub: gossipsub({
|
||||||
|
allowPublishToZeroTopicPeers: true,
|
||||||
|
emitSelf: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
peerDiscovery: [bootStrap],
|
||||||
|
addresses: {
|
||||||
|
listen: [`/ip4/0.0.0.0/tcp/${ipfsPort}`],
|
||||||
|
},
|
||||||
|
transportManager: {
|
||||||
|
faultTolerance: FaultTolerance.NO_FATAL,
|
||||||
|
},
|
||||||
|
privateKey: await getPrivateKey(),
|
||||||
|
});
|
||||||
|
|
||||||
|
p2pLogger.info(`PEER ID: ${libp2pNode.peerId.toString()}`);
|
||||||
|
logger.info(
|
||||||
|
`Listening on: ${libp2pNode
|
||||||
|
.getMultiaddrs()
|
||||||
|
.map((addr: any) => addr.toString())
|
||||||
|
.join(', ')}`
|
||||||
|
);
|
||||||
|
|
||||||
|
helia = await createHelia({
|
||||||
|
blockstore,
|
||||||
|
libp2p: libp2pNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pubsub = libp2pNode.services.pubsub as PubSub;
|
||||||
|
await setupServiceDiscovery(pubsub);
|
||||||
|
|
||||||
|
setupPeerEventListeners(libp2pNode);
|
||||||
|
|
||||||
|
connectToSpecificPeers(libp2pNode);
|
||||||
|
|
||||||
|
return helia;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to initialize node:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getBootstrapList(): string[] {
|
||||||
|
let bootstrapList: string[] = [];
|
||||||
|
bootstrapList = process.env.BOOTSTRAP_NODES?.split(',').map((node) => node.trim()) || [];
|
||||||
|
|
||||||
|
return bootstrapList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupPeerEventListeners(node: Libp2p) {
|
||||||
|
node.addEventListener('peer:discovery', (event) => {
|
||||||
|
const peerId = event.detail.id.toString();
|
||||||
|
logger.info(`Discovered peer: ${peerId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.addEventListener('peer:connect', (event) => {
|
||||||
|
const peerId = event.detail.toString();
|
||||||
|
logger.info(`Peer connection succeeded: ${peerId}`);
|
||||||
|
node.peerStore
|
||||||
|
.get(event.detail)
|
||||||
|
.then((peerInfo) => {
|
||||||
|
const multiaddrs = peerInfo?.addresses.map((addr) => addr.multiaddr.toString()) || ['unknown'];
|
||||||
|
logger.info(`Peer multiaddrs: ${multiaddrs.join(', ')}`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(`Error fetching peer info for ${peerId}: ${error.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
node.addEventListener('peer:disconnect', (event) => {
|
||||||
|
const peerId = event.detail.toString();
|
||||||
|
logger.info(`Disconnected from peer: ${peerId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.addEventListener('peer:reconnect-failure', (event) => {
|
||||||
|
const peerId = event.detail.toString();
|
||||||
|
logger.error(`Peer reconnection failed: ${peerId}`);
|
||||||
|
node.peerStore
|
||||||
|
.get(event.detail)
|
||||||
|
.then((peerInfo) => {
|
||||||
|
const multiaddrs = peerInfo?.addresses.map((addr) => addr.multiaddr.toString()) || ['unknown'];
|
||||||
|
logger.error(`Peer multiaddrs: ${multiaddrs.join(', ')}`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(`Error fetching peer info for ${peerId}: ${error.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
node.addEventListener('connection:close', (event) => {
|
||||||
|
const connection = event.detail;
|
||||||
|
const peerId = connection.remotePeer.toString();
|
||||||
|
const remoteAddr = connection.remoteAddr.toString();
|
||||||
|
logger.info(`Connection closed for peer: ${peerId}`);
|
||||||
|
logger.info(`Remote address: ${remoteAddr}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stopIpfsNode = async () => {
|
||||||
|
if (reconnectInterval) {
|
||||||
|
clearInterval(reconnectInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (libp2pNode) {
|
||||||
|
const pubsub = libp2pNode.services.pubsub as PubSub;
|
||||||
|
await stopDiscoveryService(pubsub);
|
||||||
|
} else {
|
||||||
|
await stopDiscoveryService(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (helia) {
|
||||||
|
await helia.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHeliaInstance = () => {
|
||||||
|
return helia;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLibp2pInstance = () => {
|
||||||
|
return libp2pNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProxyAgentInstance = () => {
|
||||||
|
return proxyAgent;
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectToSpecificPeers(node: Libp2p) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await attemptPeerConnections(node);
|
||||||
|
|
||||||
|
reconnectInterval = setInterval(async () => {
|
||||||
|
await attemptPeerConnections(node);
|
||||||
|
}, 120000);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attemptPeerConnections(node: Libp2p) {
|
||||||
|
logger.info('Current peer connections:');
|
||||||
|
const peers = node.getPeers();
|
||||||
|
if (peers.length === 0) {
|
||||||
|
logger.info(' - No connected peers');
|
||||||
|
} else {
|
||||||
|
for (const peerId of peers) {
|
||||||
|
try {
|
||||||
|
// Get peer info including addresses
|
||||||
|
const peerInfo = await node.peerStore.get(peerId);
|
||||||
|
const addresses = peerInfo?.addresses.map((addr) => addr.multiaddr.toString()).join(', ') || 'unknown';
|
||||||
|
logger.info(` - Connected to peer: ${peerId.toString()}`);
|
||||||
|
logger.info(` Addresses: ${addresses}`);
|
||||||
|
} catch (_error) {
|
||||||
|
// Fallback to just showing the peer ID if we can't get address info
|
||||||
|
logger.info(` - Connected to peer: ${peerId.toString()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
src/ipfs/utils/crypto.ts
Normal file
30
src/ipfs/utils/crypto.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { generateKeyPairFromSeed } from '@libp2p/crypto/keys';
|
||||||
|
import forge from 'node-forge';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { createServiceLogger } from '../../utils/logger';
|
||||||
|
|
||||||
|
const logger = createServiceLogger('CRYPTO');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a deterministic private key based on the node's fingerprint
|
||||||
|
*/
|
||||||
|
export const getPrivateKey = async () => {
|
||||||
|
try {
|
||||||
|
const userInput = config.env.fingerprint;
|
||||||
|
|
||||||
|
// Use SHA-256 to create a deterministic seed
|
||||||
|
const md = forge.md.sha256.create();
|
||||||
|
md.update(userInput);
|
||||||
|
const seedString = md.digest().getBytes(); // Get raw bytes as a string
|
||||||
|
|
||||||
|
// Convert the seed string to Uint8Array
|
||||||
|
const seed = Uint8Array.from(forge.util.binary.raw.decode(seedString));
|
||||||
|
|
||||||
|
// Generate an Ed25519 private key (libp2p-compatible)
|
||||||
|
const privateKey = await generateKeyPairFromSeed('Ed25519', seed);
|
||||||
|
return privateKey;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error generating private key:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
120
src/orbit/orbitDBService.ts
Normal file
120
src/orbit/orbitDBService.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { createOrbitDB, IPFSAccessController } from '@orbitdb/core';
|
||||||
|
import { registerFeed } from '@orbitdb/feed-db';
|
||||||
|
import { config } from '../config';
|
||||||
|
import { createServiceLogger } from '../utils/logger';
|
||||||
|
import { getHelia } from '../ipfs/ipfsService';
|
||||||
|
|
||||||
|
const logger = createServiceLogger('ORBITDB');
|
||||||
|
|
||||||
|
let orbitdb: any;
|
||||||
|
|
||||||
|
// Create a node-specific directory based on fingerprint to avoid lock conflicts
|
||||||
|
export const getOrbitDBDir = (): string => {
|
||||||
|
const baseDir = config.orbitdb.directory;
|
||||||
|
const fingerprint = config.env.fingerprint;
|
||||||
|
return `${baseDir}-${fingerprint}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ORBITDB_DIR = getOrbitDBDir();
|
||||||
|
|
||||||
|
export const getDBAddress = (name: string): string | null => {
|
||||||
|
const addressFile = path.join(ORBITDB_DIR, `${name}.address`);
|
||||||
|
if (fs.existsSync(addressFile)) {
|
||||||
|
return fs.readFileSync(addressFile, 'utf-8').trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveDBAddress = (name: string, address: string) => {
|
||||||
|
const addressFile = path.join(ORBITDB_DIR, `${name}.address`);
|
||||||
|
fs.writeFileSync(addressFile, address);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const init = async () => {
|
||||||
|
try {
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(ORBITDB_DIR)) {
|
||||||
|
fs.mkdirSync(ORBITDB_DIR, { recursive: true });
|
||||||
|
logger.info(`Created OrbitDB directory: ${ORBITDB_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerFeed();
|
||||||
|
|
||||||
|
const ipfs = getHelia();
|
||||||
|
if (!ipfs) {
|
||||||
|
throw new Error('IPFS instance is not initialized.');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Initializing OrbitDB with directory: ${ORBITDB_DIR}`);
|
||||||
|
|
||||||
|
orbitdb = await createOrbitDB({
|
||||||
|
ipfs,
|
||||||
|
directory: ORBITDB_DIR,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('OrbitDB initialized successfully.');
|
||||||
|
return orbitdb;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to initialize OrbitDB:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openDB = async (name: string, type: string) => {
|
||||||
|
if (!orbitdb) {
|
||||||
|
throw new Error('OrbitDB not initialized. Call init() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingAddress = getDBAddress(name);
|
||||||
|
let db;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dbOptions = {
|
||||||
|
type,
|
||||||
|
overwrite: false,
|
||||||
|
AccessController: IPFSAccessController({ write: ['*'], storage: getHelia() }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingAddress) {
|
||||||
|
logger.info(`Loading existing database with address: ${existingAddress}`);
|
||||||
|
db = await orbitdb.open(existingAddress, dbOptions);
|
||||||
|
} else {
|
||||||
|
logger.info(`Creating new database: ${name}`);
|
||||||
|
db = await orbitdb.open(name, dbOptions);
|
||||||
|
saveDBAddress(name, db.address.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the access controller type to verify
|
||||||
|
logger.info('Access Controller Type:', db.access.type);
|
||||||
|
return db;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error opening database '${name}':`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOrbitDB = () => {
|
||||||
|
return orbitdb;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const db = async (dbName: string, type: string) => {
|
||||||
|
try {
|
||||||
|
if (!orbitdb) {
|
||||||
|
throw new Error('OrbitDB not initialized. Call init() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await openDB(dbName, type);
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Error accessing database '${dbName}':`, error);
|
||||||
|
throw new Error(`Database error: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
init,
|
||||||
|
openDB,
|
||||||
|
getOrbitDB,
|
||||||
|
db,
|
||||||
|
};
|
147
src/utils/logger.ts
Normal file
147
src/utils/logger.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { createLogger, format, transports } from 'winston';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Define logger options interface
|
||||||
|
export interface LoggerOptions {
|
||||||
|
logsDir?: string;
|
||||||
|
level?: string;
|
||||||
|
disableConsole?: boolean;
|
||||||
|
disableFile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define colors for different service types
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
error: '\x1b[31m', // red
|
||||||
|
warn: '\x1b[33m', // yellow
|
||||||
|
info: '\x1b[32m', // green
|
||||||
|
debug: '\x1b[36m', // cyan
|
||||||
|
reset: '\x1b[0m', // reset
|
||||||
|
|
||||||
|
// Service specific colors
|
||||||
|
IPFS: '\x1b[36m', // cyan
|
||||||
|
HEARTBEAT: '\x1b[33m', // yellow
|
||||||
|
SOCKET: '\x1b[34m', // blue
|
||||||
|
'LOAD-BALANCER': '\x1b[35m', // magenta
|
||||||
|
DEFAULT: '\x1b[37m', // white
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a customizable logger factory
|
||||||
|
export function createDebrosLogger(options: LoggerOptions = {}) {
|
||||||
|
// Set default options
|
||||||
|
const logsDir = options.logsDir || path.join(process.cwd(), 'logs');
|
||||||
|
const logLevel = options.level || process.env.LOG_LEVEL || 'info';
|
||||||
|
|
||||||
|
// Create logs directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(logsDir) && !options.disableFile) {
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom format for console output with colors
|
||||||
|
const customConsoleFormat = format.printf(({ level, message, timestamp, service }: any) => {
|
||||||
|
// Truncate error messages
|
||||||
|
if (level === 'error' && typeof message === 'string' && message.length > 300) {
|
||||||
|
message = message.substring(0, 300) + '... [truncated]';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle objects and errors
|
||||||
|
if (typeof message === 'object' && message !== null) {
|
||||||
|
if (message instanceof Error) {
|
||||||
|
message = message.message;
|
||||||
|
// Truncate error messages
|
||||||
|
if (message.length > 300) {
|
||||||
|
message = message.substring(0, 300) + '... [truncated]';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
message = JSON.stringify(message, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
message = '[Object]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceColor = service && colors[service] ? colors[service] : colors.DEFAULT;
|
||||||
|
const levelColor = colors[level] || colors.DEFAULT;
|
||||||
|
const serviceTag = service ? `[${service}]` : '';
|
||||||
|
|
||||||
|
return `${timestamp} ${levelColor}${level}${colors.reset}: ${serviceColor}${serviceTag}${colors.reset} ${message}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom format for file output (no colors)
|
||||||
|
const customFileFormat = format.printf(({ level, message, timestamp, service }) => {
|
||||||
|
// Handle objects and errors
|
||||||
|
if (typeof message === 'object' && message !== null) {
|
||||||
|
if (message instanceof Error) {
|
||||||
|
message = message.message;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
message = JSON.stringify(message);
|
||||||
|
} catch (e) {
|
||||||
|
message = '[Object]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceTag = service ? `[${service}]` : '';
|
||||||
|
return `${timestamp} ${level}: ${serviceTag} ${message}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure transports
|
||||||
|
const loggerTransports = [];
|
||||||
|
|
||||||
|
// Add console transport if not disabled
|
||||||
|
if (!options.disableConsole) {
|
||||||
|
loggerTransports.push(
|
||||||
|
new transports.Console({
|
||||||
|
format: format.combine(format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), customConsoleFormat),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file transports if not disabled
|
||||||
|
if (!options.disableFile) {
|
||||||
|
loggerTransports.push(
|
||||||
|
// Combined log file
|
||||||
|
new transports.File({
|
||||||
|
filename: path.join(logsDir, 'app.log'),
|
||||||
|
format: format.combine(format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), customFileFormat),
|
||||||
|
}),
|
||||||
|
// Error log file
|
||||||
|
new transports.File({
|
||||||
|
filename: path.join(logsDir, 'error.log'),
|
||||||
|
level: 'error',
|
||||||
|
format: format.combine(format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), customFileFormat),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the logger
|
||||||
|
const logger = createLogger({
|
||||||
|
level: logLevel,
|
||||||
|
format: format.combine(format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), format.splat()),
|
||||||
|
defaultMeta: { service: 'DEFAULT' },
|
||||||
|
transports: loggerTransports,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to create a logger for a specific service
|
||||||
|
const createServiceLogger = (serviceName: string) => {
|
||||||
|
return {
|
||||||
|
error: (message: any, ...meta: any[]) => logger.error(message, { service: serviceName, ...meta }),
|
||||||
|
warn: (message: any, ...meta: any[]) => logger.warn(message, { service: serviceName, ...meta }),
|
||||||
|
info: (message: any, ...meta: any[]) => logger.info(message, { service: serviceName, ...meta }),
|
||||||
|
debug: (message: any, ...meta: any[]) => logger.debug(message, { service: serviceName, ...meta }),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
logger,
|
||||||
|
createServiceLogger,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a default logger instance
|
||||||
|
const { logger, createServiceLogger } = createDebrosLogger();
|
||||||
|
|
||||||
|
export { logger, createServiceLogger };
|
||||||
|
export default logger;
|
58
tsconfig.json
Normal file
58
tsconfig.json
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "Node 20",
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Specify what module code is generated. */
|
||||||
|
"module": "ES2020",
|
||||||
|
/* Specify which ECMAScript version the project's output JavaScript code must support.*/
|
||||||
|
"target": "ESNext",
|
||||||
|
/*
|
||||||
|
* Specify how TypeScript looks up a file from a given module specifier.
|
||||||
|
* Bundler removes the need to specify the full path inside imports
|
||||||
|
*/
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
/* Skip type checking of declaration files. */
|
||||||
|
"skipLibCheck": true,
|
||||||
|
/* Removes comments from the project's output JavaScript code. */
|
||||||
|
"removeComments": true,
|
||||||
|
/* Enables experimental support for emitting type metadata for decorators which works with the module reflect-metadata. */
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
/* Enables experimental support for decorators, which is a version of decorators that predates the TC39 standardization process. */
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
/* When set, TypeScript will include the original content of the .ts file as an embedded string in the source map (using the source map's sourcesContent property). This is often useful in the same cases as inlineSourceMap. */
|
||||||
|
"inlineSources": true,
|
||||||
|
/* Enables the generation of sourcemap files. */
|
||||||
|
"sourceMap": true,
|
||||||
|
/* Allow JavaScript files to be imported inside the project, instead of just .ts and .tsx files. */
|
||||||
|
"allowJs": true,
|
||||||
|
/* By default (with esModuleInterop false or not set) TypeScript treats CommonJS/AMD/UMD modules similar to ES6 modules. */
|
||||||
|
"esModuleInterop": true,
|
||||||
|
/* The strict flag enables a wide range of type checking behavior that results in stronger guarantees of program correctness. */
|
||||||
|
"strict": true,
|
||||||
|
/* When set to true, TypeScript will raise an error when a class property was declared but not set in the constructor. */
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
/* This flag controls how import works */
|
||||||
|
"importsNotUsedAsValues": "remove",
|
||||||
|
/* Specify the location where a debugger should locate TypeScript files instead of relative source locations */
|
||||||
|
"sourceRoot": "/",
|
||||||
|
/* The longest common path of all non-declaration input files. */
|
||||||
|
"rootDir": "src",
|
||||||
|
/* Directory name of the project's output JavaScript code. */
|
||||||
|
"outDir": "dist",
|
||||||
|
/* Specify to generate .d.ts files for every TypeScript or JavaScript file inside the project. */
|
||||||
|
"declaration": true,
|
||||||
|
/* Sets a base directory from which to resolve non-relative module names. */
|
||||||
|
"baseUrl": "."
|
||||||
|
/* A series of entries which re-map imports to lookup locations relative to the baseUrl if set, or to the tsconfig file itself otherwise. */
|
||||||
|
// "paths": {
|
||||||
|
// "@/*": [
|
||||||
|
// "./src/*"
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "orbitdb.d.ts"],
|
||||||
|
"exclude": ["coverage", "dist", "eslint.config.js", "node_modules"],
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
}
|
||||||
|
}
|
110
types.d.ts
vendored
Normal file
110
types.d.ts
vendored
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Type definitions for @debros/network
|
||||||
|
// Project: https://github.com/debros/anchat-relay
|
||||||
|
// Definitions by: Debros Team
|
||||||
|
|
||||||
|
declare module '@DeBrosOfficial/network' {
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
// Config types
|
||||||
|
export interface DebrosConfig {
|
||||||
|
ipfs: {
|
||||||
|
swarm: {
|
||||||
|
port: number;
|
||||||
|
announceAddresses: string[];
|
||||||
|
listenAddresses: string[];
|
||||||
|
connectAddresses: string[];
|
||||||
|
};
|
||||||
|
blockstorePath: string;
|
||||||
|
orbitdbPath: string;
|
||||||
|
bootstrap: string[];
|
||||||
|
privateKey?: string;
|
||||||
|
};
|
||||||
|
logger: {
|
||||||
|
level: string;
|
||||||
|
file?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
errors?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core configuration
|
||||||
|
export const config: DebrosConfig;
|
||||||
|
export const defaultConfig: DebrosConfig;
|
||||||
|
export function validateConfig(config: Partial<DebrosConfig>): ValidationResult;
|
||||||
|
|
||||||
|
// IPFS types
|
||||||
|
export interface IPFSModule {
|
||||||
|
helia: any;
|
||||||
|
libp2p: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPFS Service
|
||||||
|
export const ipfsService: {
|
||||||
|
init(): Promise<IPFSModule>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
};
|
||||||
|
export function initIpfs(): Promise<IPFSModule>;
|
||||||
|
export function stopIpfs(): Promise<void>;
|
||||||
|
export function getHelia(): any;
|
||||||
|
export function getProxyAgent(): any;
|
||||||
|
export function getInstance(): IPFSModule;
|
||||||
|
export function getLibp2p(): any;
|
||||||
|
export function getConnectedPeers(): any[];
|
||||||
|
export function getOptimalPeer(): any;
|
||||||
|
export function updateNodeLoad(load: number): void;
|
||||||
|
export function logPeersStatus(): void;
|
||||||
|
|
||||||
|
// IPFS Config
|
||||||
|
export const ipfsConfig: any;
|
||||||
|
export function getIpfsPort(): number;
|
||||||
|
export function getBlockstorePath(): string;
|
||||||
|
|
||||||
|
// LoadBalancerController interface and value declaration
|
||||||
|
export interface LoadBalancerController {
|
||||||
|
getNodeInfo: (_req: Request, _res: Response, _next: NextFunction) => void;
|
||||||
|
getOptimalPeer: (_req: Request, _res: Response, _next: NextFunction) => void;
|
||||||
|
getAllPeers: (_req: Request, _res: Response, _next: NextFunction) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare loadBalancerController as a value
|
||||||
|
export const loadBalancerController: LoadBalancerController;
|
||||||
|
|
||||||
|
// OrbitDB
|
||||||
|
export const orbitDBService: {
|
||||||
|
init(): Promise<any>;
|
||||||
|
};
|
||||||
|
export function initOrbitDB(): Promise<any>;
|
||||||
|
export function openDB(dbName: string, dbType: string, options?: any): Promise<any>;
|
||||||
|
export function getOrbitDB(): any;
|
||||||
|
export const orbitDB: any;
|
||||||
|
export function getOrbitDBDir(): string;
|
||||||
|
export function getDBAddress(dbName: string): string | null;
|
||||||
|
export function saveDBAddress(dbName: string, address: string): void;
|
||||||
|
|
||||||
|
// Logger
|
||||||
|
export interface LoggerOptions {
|
||||||
|
level?: string;
|
||||||
|
file?: string;
|
||||||
|
service?: string;
|
||||||
|
}
|
||||||
|
export const logger: any;
|
||||||
|
export function createServiceLogger(name: string, options?: LoggerOptions): any;
|
||||||
|
export function createDebrosLogger(options?: LoggerOptions): any;
|
||||||
|
|
||||||
|
// Crypto
|
||||||
|
export function getPrivateKey(): Promise<string>;
|
||||||
|
|
||||||
|
// Default export
|
||||||
|
const defaultExport: {
|
||||||
|
config: DebrosConfig;
|
||||||
|
validateConfig: typeof validateConfig;
|
||||||
|
ipfsService: typeof ipfsService;
|
||||||
|
orbitDBService: typeof orbitDBService;
|
||||||
|
logger: any;
|
||||||
|
createServiceLogger: typeof createServiceLogger;
|
||||||
|
};
|
||||||
|
export default defaultExport;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user