import HoloSphere from 'holosphere' import * as h3 from 'h3-js' export interface HolonData { id: string name: string description?: string latitude: number longitude: number resolution: number data: Record timestamp: number } export interface HolonLens { name: string schema?: any data: any[] } export interface HolonConnection { id: string name: string type: 'federation' | 'reference' targetSpace: string status: 'connected' | 'disconnected' | 'error' } export class HoloSphereService { private sphere!: HoloSphere private isInitialized: boolean = false private connections: Map = new Map() private connectionErrorLogged: boolean = false // Track if we've already logged connection errors constructor(appName: string = 'canvas-holons', strict: boolean = false, openaiKey?: string) { try { this.sphere = new HoloSphere(appName, strict, openaiKey) this.isInitialized = true console.log('✅ HoloSphere service initialized') } catch (error) { console.error('❌ Failed to initialize HoloSphere:', error) this.isInitialized = false } } async initialize(): Promise { if (!this.isInitialized) { console.error('❌ HoloSphere not initialized') return false } return true } // Get a holon for specific coordinates and resolution async getHolon(lat: number, lng: number, resolution: number): Promise { if (!this.isInitialized) return '' try { return await this.sphere.getHolon(lat, lng, resolution) } catch (error) { console.error('❌ Error getting holon:', error) return '' } } // Store data in a holon async putData(holon: string, lens: string, data: any): Promise { if (!this.isInitialized) return false try { await this.sphere.put(holon, lens, data) return true } catch (error) { console.error('❌ Error storing data:', error) return false } } // Retrieve data from a holon async getData(holon: string, lens: string, key?: string): Promise { if (!this.isInitialized) return null try { if (key) { return await this.sphere.get(holon, lens, key) } else { return await this.sphere.getAll(holon, lens) } } catch (error) { console.error('❌ Error retrieving data:', error) return null } } // Retrieve data with subscription and timeout (better for Gun's async nature) async getDataWithWait(holon: string, lens: string, timeoutMs: number = 5000): Promise { if (!this.isInitialized) { console.log(`⚠️ HoloSphere not initialized for ${lens}`) return null } // Check for WebSocket connection issues // Note: GunDB connection errors appear in browser console, we can't directly detect them // but we can provide better feedback when no data is received return new Promise((resolve) => { let resolved = false let collectedData: any = {} let subscriptionActive = false console.log(`🔍 getDataWithWait: holon=${holon}, lens=${lens}, timeout=${timeoutMs}ms`) // Listen for WebSocket errors (they appear in console but we can't catch them directly) // Instead, we'll detect the pattern: subscription never fires + getAll never resolves // Set up timeout (increased default to 5 seconds for network sync) const timeout = setTimeout(() => { if (!resolved) { resolved = true const keyCount = Object.keys(collectedData).length const status = subscriptionActive ? '(subscription was active)' : '(subscription never fired - possible WebSocket connection issue)' console.log(`⏱️ Timeout for lens ${lens}, returning collected data:`, keyCount, 'keys', status) // If no data and subscription never fired, it's likely a connection issue // Only log this once to avoid console spam if (keyCount === 0 && !subscriptionActive && !this.connectionErrorLogged) { this.connectionErrorLogged = true console.error(`❌ GunDB Connection Issue: WebSocket to 'wss://gun.holons.io/gun' is failing`) console.error(`💡 This prevents loading data from the Holosphere. Possible causes:`) console.error(` • GunDB server may be down or unreachable`) console.error(` • Network/firewall blocking WebSocket connections`) console.error(` • Check browser console for WebSocket connection errors`) console.error(` • Data will not load until connection is established`) } resolve(keyCount > 0 ? collectedData : null) } }, timeoutMs) try { // Check if methods exist if (!this.sphere.subscribe) { console.error(`❌ sphere.subscribe does not exist`) } if (!this.sphere.getAll) { console.error(`❌ sphere.getAll does not exist`) } if (!this.sphere.get) { console.error(`❌ sphere.get does not exist`) } console.log(`🔧 Attempting to subscribe to ${holon}/${lens}`) // Try subscribe if it exists let unsubscribe: (() => void) | undefined = undefined if (this.sphere.subscribe) { try { const subscribeResult = this.sphere.subscribe(holon, lens, (data: any, key?: string) => { subscriptionActive = true console.log(`📥 Subscription callback fired for ${lens}:`, { data, key, dataType: typeof data, isObject: typeof data === 'object', isArray: Array.isArray(data) }) if (data !== null && data !== undefined) { if (key) { // If we have a key, it's a key-value pair collectedData[key] = data console.log(`📥 Added key-value pair: ${key} =`, data) } else if (typeof data === 'object' && !Array.isArray(data)) { // If it's an object, merge it collectedData = { ...collectedData, ...data } console.log(`📥 Merged object data, total keys:`, Object.keys(collectedData).length) } else if (Array.isArray(data)) { // If it's an array, convert to object with indices data.forEach((item, index) => { collectedData[String(index)] = item }) console.log(`📥 Converted array to object, total keys:`, Object.keys(collectedData).length) } else { // Primitive value collectedData['value'] = data console.log(`📥 Added primitive value:`, data) } console.log(`📥 Current collected data for ${lens}:`, Object.keys(collectedData).length, 'keys') } }) // Handle Promise if subscribe returns one if (subscribeResult instanceof Promise) { subscribeResult.then((result) => { unsubscribe = result?.unsubscribe || undefined console.log(`✅ Subscribe called successfully for ${lens}`) }).catch((err) => { console.error(`❌ Error in subscribe promise for ${lens}:`, err) }) } else { unsubscribe = subscribeResult?.unsubscribe || undefined console.log(`✅ Subscribe called successfully for ${lens}`) } } catch (subError) { console.error(`❌ Error calling subscribe for ${lens}:`, subError) } } // Try getAll if it exists if (this.sphere.getAll) { console.log(`🔧 Attempting getAll for ${holon}/${lens}`) this.sphere.getAll(holon, lens).then((immediateData: any) => { console.log(`📦 getAll returned for ${lens}:`, { data: immediateData, type: typeof immediateData, isObject: typeof immediateData === 'object', isArray: Array.isArray(immediateData), keys: immediateData && typeof immediateData === 'object' ? Object.keys(immediateData).length : 'N/A' }) if (immediateData !== null && immediateData !== undefined) { if (typeof immediateData === 'object' && !Array.isArray(immediateData)) { collectedData = { ...collectedData, ...immediateData } console.log(`📦 Merged immediate data, total keys:`, Object.keys(collectedData).length) } else if (Array.isArray(immediateData)) { immediateData.forEach((item, index) => { collectedData[String(index)] = item }) console.log(`📦 Converted immediate array to object, total keys:`, Object.keys(collectedData).length) } else { collectedData['value'] = immediateData console.log(`📦 Added immediate primitive value`) } } // If we have data immediately, resolve early if (Object.keys(collectedData).length > 0 && !resolved) { resolved = true clearTimeout(timeout) if (unsubscribe) unsubscribe() console.log(`✅ Resolving early with ${Object.keys(collectedData).length} keys for ${lens}`) resolve(collectedData) } }).catch((error: any) => { console.error(`⚠️ Error getting immediate data for ${lens}:`, error) }) } else { // Fallback: try using getData method instead console.log(`🔧 getAll not available, trying getData as fallback for ${lens}`) this.getData(holon, lens).then((fallbackData: any) => { console.log(`📦 getData (fallback) returned for ${lens}:`, fallbackData) if (fallbackData !== null && fallbackData !== undefined) { if (typeof fallbackData === 'object' && !Array.isArray(fallbackData)) { collectedData = { ...collectedData, ...fallbackData } } else { collectedData['value'] = fallbackData } if (Object.keys(collectedData).length > 0 && !resolved) { resolved = true clearTimeout(timeout) if (unsubscribe) unsubscribe() console.log(`✅ Resolving with fallback data: ${Object.keys(collectedData).length} keys for ${lens}`) resolve(collectedData) } } }).catch((error: any) => { console.error(`⚠️ Error in fallback getData for ${lens}:`, error) }) } } catch (error) { console.error(`❌ Error setting up subscription for ${lens}:`, error) clearTimeout(timeout) if (!resolved) { resolved = true resolve(null) } } }) } // Delete data from a holon async deleteData(holon: string, lens: string, key?: string): Promise { if (!this.isInitialized) return false try { if (key) { await this.sphere.delete(holon, lens, key) } else { await this.sphere.deleteAll(holon, lens) } return true } catch (error) { console.error('❌ Error deleting data:', error) return false } } // Set schema for data validation async setSchema(lens: string, schema: any): Promise { if (!this.isInitialized) return false try { await this.sphere.setSchema(lens, schema) return true } catch (error) { console.error('❌ Error setting schema:', error) return false } } // Get current schema async getSchema(lens: string): Promise { if (!this.isInitialized) return null try { return await this.sphere.getSchema(lens) } catch (error) { console.error('❌ Error getting schema:', error) return null } } // Subscribe to changes in a holon subscribe(holon: string, lens: string, callback: (data: any) => void): void { if (!this.isInitialized) return try { this.sphere.subscribe(holon, lens, callback) } catch (error) { console.error('❌ Error subscribing to changes:', error) } } // Get holon hierarchy (parent and children) getHolonHierarchy(holon: string): { parent?: string; children: string[] } { try { const resolution = h3.getResolution(holon) const parent = resolution > 0 ? h3.cellToParent(holon, resolution - 1) : undefined const children = h3.cellToChildren(holon, resolution + 1) return { parent, children } } catch (error) { console.error('❌ Error getting holon hierarchy:', error) return { children: [] } } } // Get all scales for a holon (all containing holons) getHolonScalespace(holon: string): string[] { try { return this.sphere.getHolonScalespace(holon) } catch (error) { console.error('❌ Error getting holon scalespace:', error) return [] } } // Federation methods async federate(spaceId1: string, spaceId2: string, password1?: string, password2?: string, bidirectional?: boolean): Promise { if (!this.isInitialized) return false try { await this.sphere.federate(spaceId1, spaceId2, password1, password2, bidirectional) return true } catch (error) { console.error('❌ Error federating spaces:', error) return false } } async propagate(holon: string, lens: string, data: any, options?: { useReferences?: boolean; targetSpaces?: string[] }): Promise { if (!this.isInitialized) return false try { await this.sphere.propagate(holon, lens, data, options) return true } catch (error) { console.error('❌ Error propagating data:', error) return false } } // Message federation async federateMessage(originalChatId: string, messageId: string, federatedChatId: string, federatedMessageId: string, type: string): Promise { if (!this.isInitialized) return false try { await this.sphere.federateMessage(originalChatId, messageId, federatedChatId, federatedMessageId, type) return true } catch (error) { console.error('❌ Error federating message:', error) return false } } async getFederatedMessages(originalChatId: string, messageId: string): Promise { if (!this.isInitialized) return [] try { const result = await this.sphere.getFederatedMessages(originalChatId, messageId) return Array.isArray(result) ? result : [] } catch (error) { console.error('❌ Error getting federated messages:', error) return [] } } async updateFederatedMessages(originalChatId: string, messageId: string, updateCallback: (chatId: string, messageId: string) => Promise): Promise { if (!this.isInitialized) return false try { await this.sphere.updateFederatedMessages(originalChatId, messageId, updateCallback) return true } catch (error) { console.error('❌ Error updating federated messages:', error) return false } } // Utility methods for working with coordinates and resolutions static getResolutionName(resolution: number): string { const names = [ 'Country', 'State/Province', 'Metropolitan Area', 'City', 'District', 'Neighborhood', 'Block', 'Building', 'Room', 'Desk', 'Chair', 'Point' ] return names[resolution] || `Level ${resolution}` } static getResolutionDescription(resolution: number): string { const descriptions = [ 'Country level - covers entire countries', 'State/Province level - covers states and provinces', 'Metropolitan area level - covers large urban areas', 'City level - covers individual cities', 'District level - covers city districts', 'Neighborhood level - covers neighborhoods', 'Block level - covers city blocks', 'Building level - covers individual buildings', 'Room level - covers individual rooms', 'Desk level - covers individual desks', 'Chair level - covers individual chairs', 'Point level - covers individual points' ] return descriptions[resolution] || `Geographic level ${resolution}` } // Get connection status getConnectionStatus(spaceId: string): HolonConnection | undefined { return this.connections.get(spaceId) } // Add connection addConnection(connection: HolonConnection): void { this.connections.set(connection.id, connection) } // Remove connection removeConnection(spaceId: string): boolean { return this.connections.delete(spaceId) } // Get all connections getAllConnections(): HolonConnection[] { return Array.from(this.connections.values()) } } // Create a singleton instance export const holosphereService = new HoloSphereService('canvas-holons', false)