455 lines
17 KiB
TypeScript
455 lines
17 KiB
TypeScript
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<string, any>
|
|
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<string, HolonConnection> = 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<boolean> {
|
|
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<string> {
|
|
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<boolean> {
|
|
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<any> {
|
|
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<any> {
|
|
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: any) => {
|
|
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 as any)?.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<boolean> {
|
|
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<boolean> {
|
|
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<any> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<any[]> {
|
|
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<void>): Promise<boolean> {
|
|
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)
|