holosphere
Version: 
Holonic Geospatial Communication Infrastructure
302 lines (272 loc) • 11.4 kB
JavaScript
// holo_utils.js
import * as h3 from 'h3-js';
/**
 * Converts latitude and longitude to a holon identifier.
 * @param {number} lat - The latitude.
 * @param {number} lng - The longitude.
 * @param {number} resolution - The resolution level.
 * @returns {Promise<string>} - The resulting holon identifier.
 */
export async function getHolon(lat, lng, resolution) { // Doesn't need holoInstance
    return h3.latLngToCell(lat, lng, resolution);
}
/**
 * Retrieves all containing holonagons at all scales for given coordinates.
 * @param {number} lat - The latitude.
 * @param {number} lng - The longitude.
 * @returns {Array<string>} - List of holon identifiers.
 */
export function getScalespace(lat, lng) { // Doesn't need holoInstance
    let list = []
    let cell = h3.latLngToCell(lat, lng, 14);
    list.push(cell)
    for (let i = 13; i >= 0; i--) {
        list.push(h3.cellToParent(cell, i))
    }
    return list
}
/**
 * Retrieves all containing holonagons at all scales for a given holon.
 * @param {string} holon - The holon identifier.
 * @returns {Array<string>} - List of holon identifiers.
 */
export function getHolonScalespace(holon) { // Doesn't need holoInstance
    let list = []
    let res = h3.getResolution(holon)
    for (let i = res; i >= 0; i--) {
        list.push(h3.cellToParent(holon, i))
    }
    return list
}
/**
 * Subscribes to changes in a specific holon and lens.
 * @param {HoloSphere} holoInstance - The HoloSphere instance.
 * @param {string} holon - The holon identifier.
 * @param {string} lens - The lens to subscribe to.
 * @param {function} callback - The callback to execute on changes.
 * @returns {Promise<object>} - Subscription object with unsubscribe method
 */
export async function subscribe(holoInstance, holon, lens, callback) {
    if (!holon || !lens) {
        throw new Error('subscribe: Missing holon or lens parameters:', holon, lens);
    }
    if (!callback || typeof callback !== 'function') {
        throw new Error('subscribe: Callback must be a function');
    }
    const subscriptionId = holoInstance.generateId(); // Use instance's generateId
    try {
        // Get the Gun chain up to the map()
        const mapChain = holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).map();
        // Create the subscription by calling .on() on the map chain
        const gunListener = mapChain.on(async (data, key) => { // Renamed variable
            // Check if subscription ID still exists (might have been unsubscribed)
            if (!holoInstance.subscriptions[subscriptionId]) {
                return;
            }
            if (data) {
                try {
                    let parsed = await holoInstance.parse(data);
                    if (parsed && holoInstance.isHologram(parsed)) {
                        const resolved = await holoInstance.resolveHologram(parsed, { followHolograms: true });
                        if (resolved !== parsed) {
                            parsed = resolved;
                        }
                    }
                    // Check again if subscription ID still exists before calling callback
                    if (holoInstance.subscriptions[subscriptionId]) {
                        callback(parsed, key);
                    }
                } catch (error) {
                    console.error('Error processing subscribed data:', error);
                }
            }
        });
        // Store the subscription with its ID on the instance
        holoInstance.subscriptions[subscriptionId] = {
            id: subscriptionId,
            holon,
            lens,
            callback,
            mapChain: mapChain,       // Store the map chain
            gunListener: gunListener  // Store the listener too (optional, maybe needed for close?)
        };
        // Return an object with unsubscribe method
        return {
            unsubscribe: async () => {
                const sub = holoInstance.subscriptions[subscriptionId];
                if (!sub) {
                    return;
                }
                try {
                    // Turn off the Gun subscription using the stored mapChain reference
                    if (sub.mapChain) { // Check if mapChain exists
                        sub.mapChain.off(); // Call off() on the chain where .on() was attached
                        // Optional: Add delay back? Let's omit for now.
                        // await new Promise(res => setTimeout(res, 50));
                    } // We might not need to call off() on gunListener explicitly
                    // Remove from subscriptions object AFTER turning off listener
                    delete holoInstance.subscriptions[subscriptionId];
                } catch (error) {
                    console.error(`Error during unsubscribe logic for ${subscriptionId}:`, error);
                }
            }
        };
    } catch (error) {
        console.error('Error creating subscription:', error);
        throw error;
    }
}
/**
 * Notifies subscribers about data changes
 * @param {HoloSphere} holoInstance - The HoloSphere instance.
 * @param {object} data - The data to notify about
 * @private
 */
export function notifySubscribers(holoInstance, data) {
    if (!data || !data.holon || !data.lens) {
        return;
    }
    try {
        Object.values(holoInstance.subscriptions).forEach(subscription => {
            if (subscription.holon === data.holon &&
                subscription.lens === data.lens) {
                try {
                    if (subscription.callback && typeof subscription.callback === 'function') {
                        subscription.callback(data);
                    }
                } catch (error) {
                    console.warn('Error in subscription callback:', error);
                }
            }
        });
    } catch (error) {
        console.warn('Error notifying subscribers:', error);
    }
}
// Add ID generation method
export function generateId() { // Doesn't need holoInstance
    return Date.now().toString(10) + Math.random().toString(2);
}
/**
 * Closes the HoloSphere instance and cleans up resources.
 * @param {HoloSphere} holoInstance - The HoloSphere instance.
 * @returns {Promise<void>}
 */
export async function close(holoInstance) {
    try {
        if (holoInstance.gun) {
            // Unsubscribe from all subscriptions
            const subscriptionIds = Object.keys(holoInstance.subscriptions);
            for (const id of subscriptionIds) {
                try {
                    const subscription = holoInstance.subscriptions[id];
                    if (subscription) {
                        // Turn off the Gun subscription using the stored mapChain reference
                        if (subscription.mapChain) {
                            subscription.mapChain.off();
                        } // Also turn off listener directly? Might be redundant.
                        // if (subscription.gunListener) {
                        //     subscription.gunListener.off();
                        // }
                    }
                } catch (error) {
                    console.warn(`Error cleaning up subscription ${id}:`, error);
                }
            }
            // Clear subscriptions
            holoInstance.subscriptions = {};
            // Clear schema cache using instance method
            holoInstance.clearSchemaCache();
            // Close Gun connections
            if (holoInstance.gun.back) {
                try {
                    // Clean up mesh connections
                    const mesh = holoInstance.gun.back('opt.mesh');
                    if (mesh) {
                        // Clean up mesh.hear
                        if (mesh.hear) {
                            try {
                                // Safely clear mesh.hear without modifying function properties
                                const hearKeys = Object.keys(mesh.hear);
                                for (const key of hearKeys) {
                                    // Check if it's an array before trying to clear it
                                    if (Array.isArray(mesh.hear[key])) {
                                        mesh.hear[key] = [];
                                    }
                                }
                                // Create a new empty object for mesh.hear
                                // Only if mesh.hear is not a function
                                if (typeof mesh.hear !== 'function') {
                                    mesh.hear = {};
                                }
                            } catch (meshError) {
                                console.warn('Error cleaning up Gun mesh hear:', meshError);
                            }
                        }
                        // Close any open sockets in the mesh
                        if (mesh.way) {
                            try {
                                Object.values(mesh.way).forEach(connection => {
                                    if (connection && connection.wire && connection.wire.close) {
                                        connection.wire.close();
                                    }
                                });
                            } catch (sockError) {
                                console.warn('Error closing mesh sockets:', sockError);
                            }
                        }
                        // Clear the peers list
                        if (mesh.opt && mesh.opt.peers) {
                            mesh.opt.peers = {};
                        }
                    }
                    // Attempt to clean up any TCP connections
                    if (holoInstance.gun.back('opt.web')) {
                        try {
                            const server = holoInstance.gun.back('opt.web');
                            if (server && server.close) {
                                server.close();
                            }
                        } catch (webError) {
                            console.warn('Error closing web server:', webError);
                        }
                    }
                } catch (error) {
                    console.warn('Error accessing Gun mesh:', error);
                }
            }
            // Clear all Gun instance listeners
            try {
                holoInstance.gun.off();
            } catch (error) {
                console.warn('Error turning off Gun listeners:', error);
            }
            // Wait a moment for cleanup to complete
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        console.log('HoloSphere instance closed successfully');
    } catch (error) {
        console.error('Error closing HoloSphere instance:', error);
    }
}
/**
 * Creates a namespaced username for Gun authentication
 * @param {HoloSphere} holoInstance - The HoloSphere instance.
 * @param {string} holonId - The holon ID
 * @returns {string} - Namespaced username
 */
export function userName(holoInstance, holonId) {
    if (!holonId) return null;
    return `${holoInstance.appname}:${holonId}`;
} 
// Export all utility operations as default
export default {
    getHolon,
    getScalespace,
    getHolonScalespace,
    subscribe,
    notifySubscribers,
    generateId,
    close,
    userName
};