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
};