Initial commit

This commit is contained in:
David Snelling 2025-06-24 11:41:30 -07:00
commit 5a8a6c1ba3
81 changed files with 39269 additions and 0 deletions

106
cloud-wrapper/src/index.ts Normal file
View file

@ -0,0 +1,106 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import dotenv from 'dotenv';
import http from 'http';
import { WebSocketServer } from 'ws';
import { BrainyData } from '@soulcraft/brainy';
import { setupRoutes } from './routes.js';
import { initializeBrainy } from './services/brainyService.js';
import { setupWebSocketHandlers } from './websocket.js';
import { initializeMCPService } from './services/mcpService.js';
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
const port = process.env.PORT || 3000;
// Middleware
app.use(helmet()); // Security headers
app.use(cors()); // Enable CORS
app.use(morgan('combined')); // Logging
app.use(express.json()); // Parse JSON bodies
// Initialize Brainy
let brainyInstance: BrainyData;
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// Setup API routes
const apiRouter = setupRoutes();
app.use('/api', (req, res, next) => {
// Attach brainy instance to request
(req as any).brainy = brainyInstance;
next();
}, apiRouter);
// Start the server
async function startServer() {
try {
// Initialize Brainy
brainyInstance = await initializeBrainy();
console.log('Brainy initialized successfully');
// Create HTTP server
const server = http.createServer(app);
// Create WebSocket server
const wss = new WebSocketServer({ server });
// Setup WebSocket handlers
setupWebSocketHandlers(wss, brainyInstance);
console.log('WebSocket server initialized');
// Initialize MCP service
const mcpWsPort = process.env.MCP_WS_PORT ? parseInt(process.env.MCP_WS_PORT, 10) : undefined;
const mcpRestPort = process.env.MCP_REST_PORT ? parseInt(process.env.MCP_REST_PORT, 10) : undefined;
if (mcpWsPort || mcpRestPort) {
initializeMCPService(brainyInstance, {
wsPort: mcpWsPort,
restPort: mcpRestPort,
enableAuth: process.env.MCP_ENABLE_AUTH === 'true',
apiKeys: process.env.MCP_API_KEYS ? process.env.MCP_API_KEYS.split(',') : undefined,
rateLimit: process.env.MCP_RATE_LIMIT_REQUESTS ? {
windowMs: parseInt(process.env.MCP_RATE_LIMIT_WINDOW_MS || '60000', 10),
maxRequests: parseInt(process.env.MCP_RATE_LIMIT_REQUESTS, 10)
} : undefined,
cors: process.env.MCP_ENABLE_CORS === 'true' ? {} : undefined
});
console.log('MCP service initialized');
}
// Start HTTP server
server.listen(port, () => {
console.log(`Server running on port ${port}`);
console.log(`WebSocket server running on ws://localhost:${port}`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
// Handle graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully');
// Cleanup Brainy resources
await brainyInstance?.clear();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received, shutting down gracefully');
// Cleanup Brainy resources
await brainyInstance?.clear();
process.exit(0);
});
// Start the server
startServer();

186
cloud-wrapper/src/routes.ts Normal file
View file

@ -0,0 +1,186 @@
import { Router, Request, Response, NextFunction, RequestHandler } from 'express';
import { BrainyData, NounType, VerbType } from '@soulcraft/brainy';
// Define a custom request type that includes the Brainy instance
interface BrainyRequest extends Request {
brainy: BrainyData;
}
// Helper function to handle async route handlers
const asyncHandler = (fn: (req: BrainyRequest, res: Response, next: NextFunction) => Promise<any>) =>
(req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req as BrainyRequest, res, next)).catch(next);
};
export function setupRoutes() {
const router = Router();
// Get database status
router.get('/status', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const status = await req.brainy.status();
res.status(200).json(status);
} catch (error) {
console.error('Error getting status:', error);
res.status(500).json({ error: 'Failed to get database status' });
}
}));
// Add a noun (entity)
router.post('/nouns', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { text, metadata } = req.body;
if (!text) {
return res.status(400).json({ error: 'Text is required' });
}
const id = await req.brainy.add(text, metadata || {});
res.status(201).json({ id });
} catch (error) {
console.error('Error adding noun:', error);
res.status(500).json({ error: 'Failed to add noun' });
}
}));
// Get a noun by ID
router.get('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const noun = await req.brainy.get(id);
if (!noun) {
return res.status(404).json({ error: 'Noun not found' });
}
res.status(200).json(noun);
} catch (error) {
console.error('Error getting noun:', error);
res.status(500).json({ error: 'Failed to get noun' });
}
}));
// Update noun metadata
router.put('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const { metadata } = req.body;
if (!metadata) {
return res.status(400).json({ error: 'Metadata is required' });
}
await req.brainy.updateMetadata(id, metadata);
res.status(200).json({ success: true });
} catch (error) {
console.error('Error updating noun:', error);
res.status(500).json({ error: 'Failed to update noun' });
}
}));
// Delete a noun
router.delete('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
await req.brainy.delete(id);
res.status(200).json({ success: true });
} catch (error) {
console.error('Error deleting noun:', error);
res.status(500).json({ error: 'Failed to delete noun' });
}
}));
// Search for similar nouns
router.post('/search', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { query, limit = 10 } = req.body;
if (!query) {
return res.status(400).json({ error: 'Query is required' });
}
const results = await req.brainy.searchText(query, limit);
res.status(200).json(results);
} catch (error) {
console.error('Error searching:', error);
res.status(500).json({ error: 'Failed to search' });
}
}));
// Add a verb (relationship)
router.post('/verbs', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { sourceId, targetId, metadata } = req.body;
if (!sourceId || !targetId) {
return res.status(400).json({ error: 'Source ID and Target ID are required' });
}
await req.brainy.addVerb(sourceId, targetId, metadata || {});
res.status(201).json({ success: true });
} catch (error) {
console.error('Error adding verb:', error);
res.status(500).json({ error: 'Failed to add verb' });
}
}));
// Get all verbs
router.get('/verbs', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const verbs = await req.brainy.getAllVerbs();
res.status(200).json(verbs);
} catch (error) {
console.error('Error getting verbs:', error);
res.status(500).json({ error: 'Failed to get verbs' });
}
}));
// Get verbs by source
router.get('/verbs/source/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const verbs = await req.brainy.getVerbsBySource(id);
res.status(200).json(verbs);
} catch (error) {
console.error('Error getting verbs by source:', error);
res.status(500).json({ error: 'Failed to get verbs by source' });
}
}));
// Get verbs by target
router.get('/verbs/target/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const verbs = await req.brainy.getVerbsByTarget(id);
res.status(200).json(verbs);
} catch (error) {
console.error('Error getting verbs by target:', error);
res.status(500).json({ error: 'Failed to get verbs by target' });
}
}));
// Delete a verb
router.delete('/verbs/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
await req.brainy.deleteVerb(id);
res.status(200).json({ success: true });
} catch (error) {
console.error('Error deleting verb:', error);
res.status(500).json({ error: 'Failed to delete verb' });
}
}));
// Clear all data
router.delete('/clear', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
try {
await req.brainy.clear();
res.status(200).json({ success: true });
} catch (error) {
console.error('Error clearing data:', error);
res.status(500).json({ error: 'Failed to clear data' });
}
}));
return router;
}

View file

@ -0,0 +1,97 @@
import { BrainyData, BrainyDataConfig } from '@soulcraft/brainy'
import dotenv from 'dotenv'
// Load environment variables
dotenv.config()
/**
* Extended configuration interface that includes rootDirectory
*/
interface ExtendedBrainyDataConfig extends BrainyDataConfig {
storage: BrainyDataConfig['storage'] & {
rootDirectory?: string
}
}
/**
* Initialize the Brainy instance with appropriate configuration
* based on environment variables
*/
export async function initializeBrainy(): Promise<BrainyData> {
// Get storage configuration from environment variables
const storageType = process.env.STORAGE_TYPE || 'filesystem'
// Create configuration object
const config: ExtendedBrainyDataConfig = {
storage: {}
}
// Configure storage based on type
switch (storageType) {
case 's3':
// Configure S3 storage
if (process.env.S3_ENDPOINT) {
// Use customS3Storage for S3-compatible services with custom endpoints
config.storage.customS3Storage = {
bucketName: process.env.S3_BUCKET_NAME,
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: process.env.S3_REGION || 'us-east-1',
endpoint: process.env.S3_ENDPOINT
}
} else {
// Use standard S3 storage for AWS S3
config.storage.s3Storage = {
bucketName: process.env.S3_BUCKET_NAME,
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: process.env.S3_REGION || 'us-east-1'
}
}
break
case 'filesystem':
// Configure filesystem storage
config.storage.rootDirectory = process.env.STORAGE_ROOT_DIR || './data'
break
case 'memory':
// No additional configuration needed for memory storage
break
default:
// Default to filesystem storage
config.storage.rootDirectory = process.env.STORAGE_ROOT_DIR || './data'
break
}
// Note: Universal Sentence Encoder is now the only embedding option
// TensorFlow.js is required for embedding to work
// Configure HNSW index parameters if provided
if (process.env.HNSW_M) {
config.hnsw = {
M: parseInt(process.env.HNSW_M, 10),
efConstruction: process.env.HNSW_EF_CONSTRUCTION
? parseInt(process.env.HNSW_EF_CONSTRUCTION, 10)
: 200,
efSearch: process.env.HNSW_EF_SEARCH
? parseInt(process.env.HNSW_EF_SEARCH, 10)
: 50
}
}
// Create and initialize the Brainy instance
const brainy = new BrainyData(config)
await brainy.init()
return brainy
}
/**
* Get the current storage type being used by Brainy
*/
export async function getStorageType(brainy: BrainyData): Promise<string> {
const status = await brainy.status()
return status.type || 'unknown'
}

View file

@ -0,0 +1,204 @@
import { WebSocketServer } from 'ws'
import express from 'express'
import cors from 'cors'
import { BrainyData, BrainyMCPService, MCPRequestType } from '@soulcraft/brainy'
import { v4 as uuidv4 } from 'uuid'
/**
* Initialize the MCP service with WebSocket and REST API servers
*/
export function initializeMCPService(
brainy: BrainyData,
options: {
wsPort?: number
restPort?: number
enableAuth?: boolean
apiKeys?: string[]
rateLimit?: {
windowMs: number
maxRequests: number
}
cors?: any
}
) {
// Create the MCP service
const mcpService = new BrainyMCPService(brainy, options)
// Start WebSocket server if port is provided
if (options.wsPort) {
startWebSocketServer(mcpService, options.wsPort)
}
// Start REST server if port is provided
if (options.restPort) {
startRESTServer(mcpService, options.restPort, options.cors)
}
return mcpService
}
/**
* Start a WebSocket server for the MCP service
*/
function startWebSocketServer(mcpService: BrainyMCPService, port: number) {
const wss = new WebSocketServer({ port })
wss.on('connection', (ws: any) => {
ws.on('message', async (message: string) => {
try {
const request = JSON.parse(message)
// Handle the request using the MCP service
const response = await mcpService.handleMCPRequest(request)
// Send the response
ws.send(JSON.stringify(response))
} catch (error) {
// Send error response
ws.send(
JSON.stringify({
success: false,
requestId: uuidv4(),
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : String(error)
}
})
)
}
})
})
console.log(`MCP WebSocket server started on port ${port}`)
return wss
}
/**
* Start a REST server for the MCP service
*/
function startRESTServer(
mcpService: BrainyMCPService,
port: number,
corsOptions?: any
) {
const app = express()
// Parse JSON request bodies
app.use(express.json())
// Enable CORS if configured
if (corsOptions) {
app.use(cors(corsOptions))
}
// MCP endpoints
app.post('/mcp/data', async (req: any, res: any) => {
try {
const response = await mcpService.handleMCPRequest({
...req.body,
type: 'DATA_ACCESS'
})
res.json(response)
} catch (error) {
res.status(500).json({
success: false,
requestId: uuidv4(),
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : String(error)
}
})
}
})
app.post('/mcp/tools', async (req: any, res: any) => {
try {
const response = await mcpService.handleMCPRequest({
...req.body,
type: 'TOOL_EXECUTION'
})
res.json(response)
} catch (error) {
res.status(500).json({
success: false,
requestId: uuidv4(),
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : String(error)
}
})
}
})
app.post('/mcp/system', async (req: any, res: any) => {
try {
const response = await mcpService.handleMCPRequest({
...req.body,
type: 'SYSTEM_INFO'
})
res.json(response)
} catch (error) {
res.status(500).json({
success: false,
requestId: uuidv4(),
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : String(error)
}
})
}
})
app.post('/mcp/auth', async (req: any, res: any) => {
try {
const response = await mcpService.handleMCPRequest({
...req.body,
type: 'AUTHENTICATION'
})
res.json(response)
} catch (error) {
res.status(500).json({
success: false,
requestId: uuidv4(),
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : String(error)
}
})
}
})
// Get available tools
app.get('/mcp/tools', async (req: any, res: any) => {
try {
const response = await mcpService.handleMCPRequest({
type: MCPRequestType.SYSTEM_INFO,
requestId: uuidv4(),
version: '1.0'
})
res.json(response)
} catch (error) {
res.status(500).json({
success: false,
requestId: uuidv4(),
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : String(error)
}
})
}
})
// Start the server
const server = app.listen(port, () => {
console.log(`MCP REST API server started on port ${port}`)
})
return server
}

View file

@ -0,0 +1,433 @@
import { WebSocketServer, WebSocket } from 'ws';
import { BrainyData } from '@soulcraft/brainy';
import { v4 as uuidv4 } from 'uuid';
// Define message types
enum MessageType {
STATUS = 'status',
ADD_NOUN = 'addNoun',
GET_NOUN = 'getNoun',
UPDATE_NOUN = 'updateNoun',
DELETE_NOUN = 'deleteNoun',
SEARCH = 'search',
ADD_VERB = 'addVerb',
GET_VERBS = 'getVerbs',
GET_VERBS_BY_SOURCE = 'getVerbsBySource',
GET_VERBS_BY_TARGET = 'getVerbsByTarget',
DELETE_VERB = 'deleteVerb',
CLEAR = 'clear',
SUBSCRIBE = 'subscribe',
UNSUBSCRIBE = 'unsubscribe',
ERROR = 'error'
}
// Define subscription types
enum SubscriptionType {
NOUNS = 'nouns',
VERBS = 'verbs',
SEARCH_RESULTS = 'searchResults'
}
// Define message interface
interface WebSocketMessage {
type: MessageType;
id?: string;
payload?: any;
}
// Define client interface
interface WebSocketClient {
id: string;
socket: WebSocket;
subscriptions: Set<SubscriptionType>;
}
// Store connected clients
const clients: Map<string, WebSocketClient> = new Map();
// Setup WebSocket handlers
export function setupWebSocketHandlers(wss: WebSocketServer, brainy: BrainyData) {
// Handle new connections
wss.on('connection', (socket: WebSocket) => {
const clientId = uuidv4();
// Create client object
const client: WebSocketClient = {
id: clientId,
socket,
subscriptions: new Set()
};
// Store client
clients.set(clientId, client);
console.log(`WebSocket client connected: ${clientId}`);
// Send welcome message
sendMessage(socket, {
type: MessageType.STATUS,
id: uuidv4(),
payload: {
clientId,
message: 'Connected to Brainy WebSocket API',
status: 'connected'
}
});
// Handle messages
socket.on('message', async (data: WebSocket.Data) => {
try {
const message: WebSocketMessage = JSON.parse(data.toString());
// Ensure message has an ID
const messageId = message.id || uuidv4();
console.log(`Received message: ${message.type} (${messageId})`);
// Process message based on type
await processMessage(client, message, messageId, brainy);
} catch (error) {
console.error('Error processing WebSocket message:', error);
sendMessage(socket, {
type: MessageType.ERROR,
id: uuidv4(),
payload: {
message: 'Invalid message format',
error: (error as Error).message
}
});
}
});
// Handle disconnection
socket.on('close', () => {
// Remove client
clients.delete(clientId);
console.log(`WebSocket client disconnected: ${clientId}`);
});
});
}
// Process incoming messages
async function processMessage(
client: WebSocketClient,
message: WebSocketMessage,
messageId: string,
brainy: BrainyData
) {
const { socket } = client;
try {
switch (message.type) {
case MessageType.STATUS:
// Return database status
const status = await brainy.status();
sendMessage(socket, {
type: MessageType.STATUS,
id: messageId,
payload: status
});
break;
case MessageType.ADD_NOUN:
// Add a noun
if (!message.payload?.text) {
throw new Error('Text is required');
}
const nounId = await brainy.add(
message.payload.text,
message.payload.metadata || {}
);
sendMessage(socket, {
type: MessageType.ADD_NOUN,
id: messageId,
payload: { id: nounId }
});
// Notify subscribers
notifySubscribers(SubscriptionType.NOUNS, {
type: 'added',
id: nounId,
data: await brainy.get(nounId)
});
break;
case MessageType.GET_NOUN:
// Get a noun by ID
if (!message.payload?.id) {
throw new Error('Noun ID is required');
}
const noun = await brainy.get(message.payload.id);
if (!noun) {
throw new Error('Noun not found');
}
sendMessage(socket, {
type: MessageType.GET_NOUN,
id: messageId,
payload: noun
});
break;
case MessageType.UPDATE_NOUN:
// Update noun metadata
if (!message.payload?.id || !message.payload?.metadata) {
throw new Error('Noun ID and metadata are required');
}
await brainy.updateMetadata(message.payload.id, message.payload.metadata);
sendMessage(socket, {
type: MessageType.UPDATE_NOUN,
id: messageId,
payload: { success: true }
});
// Notify subscribers
notifySubscribers(SubscriptionType.NOUNS, {
type: 'updated',
id: message.payload.id,
data: await brainy.get(message.payload.id)
});
break;
case MessageType.DELETE_NOUN:
// Delete a noun
if (!message.payload?.id) {
throw new Error('Noun ID is required');
}
await brainy.delete(message.payload.id);
sendMessage(socket, {
type: MessageType.DELETE_NOUN,
id: messageId,
payload: { success: true }
});
// Notify subscribers
notifySubscribers(SubscriptionType.NOUNS, {
type: 'deleted',
id: message.payload.id
});
break;
case MessageType.SEARCH:
// Search for similar nouns
if (!message.payload?.query) {
throw new Error('Query is required');
}
const limit = message.payload.limit || 10;
const results = await brainy.searchText(message.payload.query, limit);
sendMessage(socket, {
type: MessageType.SEARCH,
id: messageId,
payload: results
});
// Notify subscribers
notifySubscribers(SubscriptionType.SEARCH_RESULTS, {
type: 'search',
query: message.payload.query,
results
});
break;
case MessageType.ADD_VERB:
// Add a verb (relationship)
if (!message.payload?.sourceId || !message.payload?.targetId) {
throw new Error('Source ID and Target ID are required');
}
await brainy.addVerb(
message.payload.sourceId,
message.payload.targetId,
message.payload.metadata || {}
);
sendMessage(socket, {
type: MessageType.ADD_VERB,
id: messageId,
payload: { success: true }
});
// Notify subscribers
notifySubscribers(SubscriptionType.VERBS, {
type: 'added',
sourceId: message.payload.sourceId,
targetId: message.payload.targetId,
metadata: message.payload.metadata
});
break;
case MessageType.GET_VERBS:
// Get all verbs
const verbs = await brainy.getAllVerbs();
sendMessage(socket, {
type: MessageType.GET_VERBS,
id: messageId,
payload: verbs
});
break;
case MessageType.GET_VERBS_BY_SOURCE:
// Get verbs by source
if (!message.payload?.id) {
throw new Error('Source ID is required');
}
const sourceVerbs = await brainy.getVerbsBySource(message.payload.id);
sendMessage(socket, {
type: MessageType.GET_VERBS_BY_SOURCE,
id: messageId,
payload: sourceVerbs
});
break;
case MessageType.GET_VERBS_BY_TARGET:
// Get verbs by target
if (!message.payload?.id) {
throw new Error('Target ID is required');
}
const targetVerbs = await brainy.getVerbsByTarget(message.payload.id);
sendMessage(socket, {
type: MessageType.GET_VERBS_BY_TARGET,
id: messageId,
payload: targetVerbs
});
break;
case MessageType.DELETE_VERB:
// Delete a verb
if (!message.payload?.id) {
throw new Error('Verb ID is required');
}
await brainy.deleteVerb(message.payload.id);
sendMessage(socket, {
type: MessageType.DELETE_VERB,
id: messageId,
payload: { success: true }
});
// Notify subscribers
notifySubscribers(SubscriptionType.VERBS, {
type: 'deleted',
id: message.payload.id
});
break;
case MessageType.CLEAR:
// Clear all data
await brainy.clear();
sendMessage(socket, {
type: MessageType.CLEAR,
id: messageId,
payload: { success: true }
});
// Notify all subscribers
notifySubscribers(SubscriptionType.NOUNS, { type: 'cleared' });
notifySubscribers(SubscriptionType.VERBS, { type: 'cleared' });
break;
case MessageType.SUBSCRIBE:
// Subscribe to events
if (!message.payload?.type) {
throw new Error('Subscription type is required');
}
const subscriptionType = message.payload.type as SubscriptionType;
// Add subscription
client.subscriptions.add(subscriptionType);
sendMessage(socket, {
type: MessageType.SUBSCRIBE,
id: messageId,
payload: {
success: true,
type: subscriptionType
}
});
break;
case MessageType.UNSUBSCRIBE:
// Unsubscribe from events
if (!message.payload?.type) {
throw new Error('Subscription type is required');
}
const unsubscribeType = message.payload.type as SubscriptionType;
// Remove subscription
client.subscriptions.delete(unsubscribeType);
sendMessage(socket, {
type: MessageType.UNSUBSCRIBE,
id: messageId,
payload: {
success: true,
type: unsubscribeType
}
});
break;
default:
throw new Error(`Unknown message type: ${message.type}`);
}
} catch (error) {
console.error(`Error processing message ${message.type}:`, error);
sendMessage(socket, {
type: MessageType.ERROR,
id: messageId,
payload: {
originalType: message.type,
message: 'Error processing message',
error: (error as Error).message
}
});
}
}
// Send a message to a client
function sendMessage(socket: WebSocket, message: WebSocketMessage) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
}
// Notify subscribers of events
function notifySubscribers(type: SubscriptionType, data: any) {
for (const client of clients.values()) {
if (client.subscriptions.has(type)) {
sendMessage(client.socket, {
type: MessageType.SUBSCRIBE,
payload: {
type,
data
}
});
}
}
}