UNPKG

holosphere

Version:

Holonic Geospatial Communication Infrastructure

1,101 lines (966 loc) 54.6 kB
/** * Federation functionality for HoloSphere * Provides methods for creating, managing, and using federated spaces */ import * as h3 from 'h3-js'; /** * Creates a federation relationship between two spaces * Federation is bidirectional by default, and data propagation uses soul references by default. * * @param {object} holosphere - The HoloSphere instance * @param {string} spaceId1 - The first space ID * @param {string} spaceId2 - The second space ID * @param {string} [password1] - Optional password for the first space * @param {string} [password2] - Optional password for the second space * @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications (default: true) * @param {object} [lensConfig] - Optional lens-specific configuration * @param {string[]} [lensConfig.federate] - List of lenses to federate (default: all) * @param {string[]} [lensConfig.notify] - List of lenses to notify (default: all) * @returns {Promise<boolean>} - True if federation was created successfully */ export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true, lensConfig = {}) { if (!spaceId1 || !spaceId2) { throw new Error('federate: Missing required space IDs'); } // Prevent self-federation if (spaceId1 === spaceId2) { throw new Error('Cannot federate a space with itself'); } // Validate lens configuration const { federate = [], notify = [] } = lensConfig; if (!Array.isArray(federate) || !Array.isArray(notify)) { throw new Error('federate: lensConfig.federate and lensConfig.notify must be arrays'); } // Use the provided lens configurations directly const federateLenses = federate; const notifyLenses = notify; try { // Get or create federation info for first space (A) let fedInfo1 = null; try { fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1); } catch (error) { } if (fedInfo1 == null) { fedInfo1 = { id: spaceId1, name: spaceId1, federation: [], notify: [], lensConfig: {}, // New field for lens-specific settings timestamp: Date.now() }; } // Ensure arrays and lensConfig exist if (!fedInfo1.federation) fedInfo1.federation = []; if (!fedInfo1.notify) fedInfo1.notify = []; if (!fedInfo1.lensConfig) fedInfo1.lensConfig = {}; // Add space2 to space1's federation list if not already present if (!fedInfo1.federation.includes(spaceId2)) { fedInfo1.federation.push(spaceId2); } // Add space2 to space1's notify list if not already present if (!fedInfo1.notify.includes(spaceId2)) { fedInfo1.notify.push(spaceId2); } // Store lens configuration for space2 const newLensConfigsForSpace1 = { ...(fedInfo1.lensConfig || {}) }; // Shallow copy existing lensConfigs for space1 newLensConfigsForSpace1[spaceId2] = { // Add/update config for the target spaceId2 federate: [...federateLenses], // federateLenses & notifyLenses are from the main lensConfig parameter notify: [...notifyLenses], timestamp: Date.now() }; fedInfo1.lensConfig = newLensConfigsForSpace1; // Assign the new/modified object back to fedInfo1.lensConfig // Update timestamp fedInfo1.timestamp = Date.now(); // Save updated federation info for space1 try { await holosphere.putGlobal('federation', fedInfo1, password1); } catch (error) { throw new Error(`Failed to create federation: ${error.message}`); } // If bidirectional is true, handle space2 (B) as well { let fedInfo2 = null; try { fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2); } catch (error) { } if (fedInfo2 == null) { fedInfo2 = { id: spaceId2, name: spaceId2, federation: [], notify: [], lensConfig: {}, // New field for lens-specific settings timestamp: Date.now() }; } // Ensure arrays and lensConfig exist if (!fedInfo2.federation) fedInfo2.federation = []; if (!fedInfo2.notify) fedInfo2.notify = []; if (!fedInfo2.lensConfig) fedInfo2.lensConfig = {}; // Add space1 to space2's federation list if bidirectional if (bidirectional && !fedInfo2.federation.includes(spaceId1)) { fedInfo2.federation.push(spaceId1); } // Add space1 to space2's notify list if not already present if (!fedInfo2.notify.includes(spaceId1)) { fedInfo2.notify.push(spaceId1); } // Store lens configuration for space1 fedInfo2.lensConfig[spaceId1] = { federate: bidirectional ? [...federateLenses] : [], // Create a copy of the array notify: bidirectional ? [...notifyLenses] : [], // Create a copy of the array timestamp: Date.now() }; // Update timestamp fedInfo2.timestamp = Date.now(); // Save updated federation info for space2 try { await holosphere.putGlobal('federation', fedInfo2, password2); } catch (error) { } } // Create federation metadata record const federationMeta = { id: `${spaceId1}_${spaceId2}`, space1: spaceId1, space2: spaceId2, created: Date.now(), status: 'active', bidirectional: bidirectional, lensConfig: { federate: [...federateLenses], // Create a copy of the array notify: [...notifyLenses] // Create a copy of the array } }; try { await holosphere.putGlobal('federationMeta', federationMeta); } catch (error) { } return true; } catch (error) { throw error; } } /** * Subscribes to federation notifications for a space * @param {object} holosphere - The HoloSphere instance * @param {string} spaceId - The space ID to subscribe to * @param {string} [password] - Optional password for the space * @param {function} callback - The callback to execute on notifications * @param {object} [options] - Subscription options * @param {string[]} [options.lenses] - Specific lenses to subscribe to (default: all) * @param {number} [options.throttle] - Throttle notifications in ms (default: 0) * @returns {Promise<object>} - Subscription object with unsubscribe() method */ export async function subscribeFederation(holosphere, spaceId, password = null, callback, options = {}) { if (!spaceId || !callback) { throw new Error('subscribeFederation: Missing required parameters'); } const { lenses = ['*'], throttle = 0 } = options; // Get federation info const fedInfo = await holosphere.getGlobal('federation', spaceId, password); if (!fedInfo) { throw new Error('No federation info found for space'); } // Create subscription for each federated space const subscriptions = []; let lastNotificationTime = {}; if (fedInfo.federation && fedInfo.federation.length > 0) { for (const federatedSpace of fedInfo.federation) { // For each lens specified (or all if '*') for (const lens of lenses) { try { const sub = await holosphere.subscribe(federatedSpace, lens, async (data) => { try { // Skip if data is missing or not from federated space if (!data || !data.id) return; // Apply throttling if configured const now = Date.now(); const key = `${federatedSpace}_${lens}_${data.id}`; if (throttle > 0) { if (lastNotificationTime[key] && (now - lastNotificationTime[key]) < throttle) { return; // Skip this notification (throttled) } lastNotificationTime[key] = now; } // Add federation metadata if not present if (!data.federation) { data.federation = { origin: federatedSpace, timestamp: now }; } // Execute callback with the data await callback(data, federatedSpace, lens); } catch (error) { } }); if (sub && typeof sub.unsubscribe === 'function') { subscriptions.push(sub); } } catch (error) { } } } } // Return combined subscription object return { unsubscribe: () => { subscriptions.forEach(sub => { try { if (sub && typeof sub.unsubscribe === 'function') { sub.unsubscribe(); } } catch (error) { } }); // Clear the subscriptions array subscriptions.length = 0; // Clear throttling data lastNotificationTime = {}; }, getSubscriptionCount: () => subscriptions.length }; } /** * Gets federation info for a space * @param {object} holosphere - The HoloSphere instance * @param {string} spaceId - The space ID * @param {string} [password] - Optional password for the space * @returns {Promise<object|null>} - Federation info or null if not found */ export async function getFederation(holosphere, spaceId, password = null) { if (!spaceId) { throw new Error('getFederation: Missing space ID'); } return await holosphere.getGlobal('federation', spaceId, password); } /** * Retrieves the lens-specific configuration for a federation link between two spaces. * @param {object} holosphere - The HoloSphere instance * @param {string} spaceId - The ID of the source space. * @param {string} targetSpaceId - The ID of the target space in the federation link. * @param {string} [password] - Optional password for the source space. * @returns {Promise<object|null>} - An object with 'federate' and 'notify' arrays, or null if not found. */ export async function getFederatedConfig(holosphere, spaceId, targetSpaceId, password = null) { if (!holosphere || !spaceId || !targetSpaceId) { throw new Error('getFederatedConfig: Missing required parameters'); } try { const fedInfo = await getFederation(holosphere, spaceId, password); if (fedInfo && fedInfo.lensConfig && fedInfo.lensConfig[targetSpaceId]) { return { federate: fedInfo.lensConfig[targetSpaceId].federate || [], notify: fedInfo.lensConfig[targetSpaceId].notify || [] }; } return null; // Or return an empty config: { federate: [], notify: [] } } catch (error) { console.error(`Error getting federated config for ${spaceId} -> ${targetSpaceId}: ${error.message}`); throw error; } } /** * Removes a federation relationship between spaces * @param {object} holosphere - The HoloSphere instance * @param {string} spaceId1 - The first space ID * @param {string} spaceId2 - The second space ID * @param {string} [password1] - Optional password for the first space * @param {string} [password2] - Optional password for the second space * @returns {Promise<boolean>} - True if federation was removed successfully */ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) { if (!spaceId1 || !spaceId2) { throw new Error('unfederate: Missing required space IDs'); } try { // Get federation info for first space let fedInfo1 = null; try { fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1); } catch (error) { console.error(`Error getting fedInfo1 for ${spaceId1} during unfederate: ${error.message}`); // If we can't get fedInfo1, we can't modify it. Decide if this is a critical failure. // For now, we'll let it proceed to attempt metadata cleanup, but a throw here might be valid. } if (!fedInfo1) { // If fedInfo1 doesn't exist, log and proceed to metadata cleanup. console.warn(`No federation info found for ${spaceId1}. Skipping its update.`); } else { // Ensure arrays exist if (!fedInfo1.federation) fedInfo1.federation = []; if (!fedInfo1.notify) fedInfo1.notify = []; // Update first space federation info - remove from both federation and notify arrays const originalFederationLength = fedInfo1.federation.length; const originalNotifyLength = fedInfo1.notify.length; fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2); fedInfo1.notify = fedInfo1.notify.filter(id => id !== spaceId2); fedInfo1.timestamp = Date.now(); console.log(`Unfederate: Removed ${spaceId2} from ${spaceId1}: federation ${originalFederationLength} -> ${fedInfo1.federation.length}, notify ${originalNotifyLength} -> ${fedInfo1.notify.length}`); try { await holosphere.putGlobal('federation', fedInfo1, password1); } catch (error) { console.error(`Failed to update fedInfo1 for ${spaceId1} during unfederate: ${error.message}`); throw error; // RE-THROW to signal failure } } // Update second space federation info (remove spaceId1 from spaceId2's notify list) // This part is usually for full bidirectional unfederation cleanup. // The original code only did this if password2 was provided. if (password2) { // Retaining original condition for this block let fedInfo2 = null; try { fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2); } catch (error) { console.error(`Error getting fedInfo2 for ${spaceId2} during unfederate: ${error.message}`); } if (!fedInfo2 || !fedInfo2.notify) { console.warn(`No notify array found for ${spaceId2} or fedInfo2 is null. Skipping its update.`); } else { fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1); fedInfo2.timestamp = Date.now(); try { await holosphere.putGlobal('federation', fedInfo2, password2); } catch (error) { console.error(`Failed to update fedInfo2 for ${spaceId2} during unfederate: ${error.message}`); throw error; // RE-THROW to signal failure } } } // Update federation metadata const metaId = `${spaceId1}_${spaceId2}`; const altMetaId = `${spaceId2}_${spaceId1}`; try { const meta = await holosphere.getGlobal('federationMeta', metaId) || await holosphere.getGlobal('federationMeta', altMetaId); if (meta) { meta.status = 'inactive'; meta.endedAt = Date.now(); await holosphere.putGlobal('federationMeta', meta); // Not re-throwing here as it's metadata cleanup } } catch (error) { console.warn(`Failed to update federationMeta during unfederate: ${error.message}`); } return true; } catch (error) { // This will catch errors re-thrown from putGlobal or from getGlobal if they occur before specific catches. console.error(`Critical error during unfederate operation for ${spaceId1}-${spaceId2}: ${error.message}`); throw error; // Ensure the main operation failure is propagated } } /** * Removes a notification relationship between two spaces * This removes spaceId2 from the notify list of spaceId1 * * @param {object} holosphere - The HoloSphere instance * @param {string} spaceId1 - The space to modify (remove from its notify list) * @param {string} spaceId2 - The space to be removed from notifications * @param {string} [password1] - Optional password for the first space * @returns {Promise<boolean>} - True if notification was removed successfully */ export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = null) { if (!spaceId1 || !spaceId2) { throw new Error('removeNotify: Missing required space IDs'); } try { // Get federation info for space let fedInfo = await holosphere.getGlobal('federation', spaceId1, password1); if (!fedInfo) { throw new Error(`No federation info found for ${spaceId1}`); } // Ensure notify array exists if (!fedInfo.notify) fedInfo.notify = []; // Remove space2 from space1's notify list if present if (fedInfo.notify.includes(spaceId2)) { fedInfo.notify = fedInfo.notify.filter(id => id !== spaceId2); // Update timestamp fedInfo.timestamp = Date.now(); // Save updated federation info await holosphere.putGlobal('federation', fedInfo, password1); return true; } else { return false; } } catch (error) { throw error; } } /** * Get and combine data from local and federated sources. * If `options.queryIds` is provided, fetches only those specific IDs using `get()`. * Otherwise, falls back to fetching all data using `getAll()` (potentially inefficient). * * @param {HoloSphere} holosphere The HoloSphere instance * @param {string} holon The local holon name (used as the space ID for federation info) * @param {string} lens The lens to query * @param {Object} options Options for data retrieval and aggregation * @param {string[]} [options.queryIds] Optional array of specific item IDs to fetch. * @param {boolean} [options.aggregate=false] Whether to aggregate results by ID * @param {string} [options.idField='id'] The field to use as ID * @param {string[]} [options.sumFields=[]] Fields to sum during aggregation * @param {string[]} [options.concatArrays=[]] Array fields to concatenate during aggregation * @param {boolean} [options.removeDuplicates=true] Whether to remove duplicates in concatenated arrays * @param {Function} [options.mergeStrategy=null] Custom merge function for aggregation * @param {boolean} [options.includeLocal=true] Whether to include local data * @param {boolean} [options.includeFederated=true] Whether to include federated data * @param {boolean} [options.resolveReferences=true] Whether to resolve federation references * @param {number} [options.maxFederatedSpaces=-1] Maximum number of federated spaces to query (-1 for all) * @param {number} [options.timeout=10000] Timeout in milliseconds for federated queries * @returns {Promise<Array>} Combined array of local and federated data */ export async function getFederated(holosphere, holon, lens, options = {}) { // Set default options and extract queryIds const { queryIds = null, // New option aggregate = false, idField = 'id', sumFields = [], concatArrays = [], removeDuplicates = true, mergeStrategy = null, includeLocal = true, includeFederated = true, resolveReferences = true, maxFederatedSpaces = -1, timeout = 10000 } = options; console.log(`resolveReferences option: ${resolveReferences}`); console.log(`Querying specific IDs:`, queryIds ? queryIds.join(', ') : 'No (fetching all)'); // Validate required parameters if (!holosphere || !holon || !lens) { throw new Error('Missing required parameters: holosphere, holon, and lens are required'); } // Get federation info for current space (using holon as spaceId) const spaceId = holon; const fedInfo = await getFederation(holosphere, spaceId); // Initialize result array and track processed IDs to avoid duplicates/redundant fetches const fetchedItems = new Map(); // Use Map to store fetched items by ID const processedIds = new Set(); // Track IDs added to the final result const fetchPromises = []; // Determine list of spaces to query (local + federated) let spacesToQuery = []; if (includeLocal) { spacesToQuery.push(holon); // Add local holon first } if (includeFederated && fedInfo && fedInfo.federation && fedInfo.federation.length > 0) { const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.federation : fedInfo.federation.slice(0, maxFederatedSpaces); spacesToQuery = spacesToQuery.concat(federatedSpaces); } // Fetch data from all relevant spaces for (const currentSpace of spacesToQuery) { if (queryIds && Array.isArray(queryIds)) { // --- Fetch specific IDs using holosphere.get --- console.log(`Fetching specific IDs from ${currentSpace}: ${queryIds.join(', ')}`); for (const itemId of queryIds) { if (fetchedItems.has(itemId)) continue; // Skip if already fetched fetchPromises.push( holosphere.get(currentSpace, lens, itemId) .then(item => { if (item) { fetchedItems.set(itemId, item); } }) .catch(err => console.warn(`Error fetching item ${itemId} from ${currentSpace}: ${err.message}`)) ); } } else { // --- Fetch all data using holosphere.getAll (Fallback - inefficient) --- if(currentSpace === holon && includeLocal) { // Only warn once for local console.warn(`getFederated: No queryIds provided. Falling back to fetching ALL items from ${currentSpace} using getAll. This can be inefficient.`); } console.log(`Fetching ALL items from ${currentSpace}`); fetchPromises.push( holosphere.getAll(currentSpace, lens) .then(items => { for (const item of items) { if (item && item[idField] && !fetchedItems.has(item[idField])) { fetchedItems.set(item[idField], item); } } }) .catch(err => console.warn(`Error fetching all items from ${currentSpace}: ${err.message}`)) ); } } // Wait for all fetches to complete await Promise.all(fetchPromises); // Convert Map values to array for processing const result = Array.from(fetchedItems.values()); // Now resolve references if needed if (resolveReferences && result.length > 0) { console.log(`Resolving references for ${result.length} fetched items`); for (let i = 0; i < result.length; i++) { const item = result[i]; // Check for simplified reference (item with id and soul) if (item.soul && item.id) { console.log(`Found simple reference with soul: ${item.soul}`); try { // Parse the soul to get the components const soulParts = item.soul.split('/'); if (soulParts.length >= 4) { const originHolon = soulParts[1]; const originLens = soulParts[2]; const originKey = soulParts[3]; console.log(`Extracting from soul - holon: ${originHolon}, lens: ${originLens}, key: ${originKey}`); // Get original data using the extracted path - always resolve references const originalData = await holosphere.get( originHolon, originLens, originKey, null, { resolveReferences: true } // Always resolve nested references ); console.log(`Original data found via soul path:`, JSON.stringify(originalData)); if (originalData) { // Replace the reference with the original data result[i] = { ...originalData, _federation: { isReference: true, resolved: true, soul: item.soul, timestamp: Date.now() } }; } else { // Instead of leaving the original reference, create an error object result[i] = { id: item.id, _federation: { isReference: true, resolved: false, soul: item.soul, error: 'Referenced data not found', timestamp: Date.now() } }; } } else { console.warn(`Soul doesn't match expected format: ${item.soul}`); // Instead of leaving the original reference, create an error object result[i] = { id: item.id, _federation: { isReference: true, resolved: false, soul: item.soul, error: 'Invalid soul format', timestamp: Date.now() } }; } } catch (refError) { // Instead of leaving the original reference, create an error object result[i] = { id: item.id, _federation: { isReference: true, resolved: false, soul: item.soul, error: refError.message || 'Error resolving reference', timestamp: Date.now() } }; } } // For backward compatibility, check for old-style references else if (item._federation && item._federation.isReference) { console.log(`Found legacy reference: ${item._federation.origin}/${item._federation.lens}/${item[idField]}`); try { const reference = item._federation; console.log(`Getting original data from ${reference.origin} / ${reference.lens} / ${item[idField]}`); // Get original data const originalData = await holosphere.get( reference.origin, reference.lens, item[idField], null, { resolveReferences: false } // Prevent infinite recursion ); console.log(`Original data found:`, JSON.stringify(originalData)); if (originalData) { // Add federation information to the resolved data result[i] = { ...originalData, _federation: { ...reference, resolved: true, timestamp: Date.now() } }; } else { console.warn(`Could not resolve legacy reference: original data not found`); } } catch (refError) { } } } } // Apply aggregation if requested if (aggregate && result.length > 0) { // Group items by ID const groupedById = result.reduce((acc, item) => { const id = item[idField]; if (!acc[id]) { acc[id] = []; } acc[id].push(item); return acc; }, {}); // Aggregate each group const aggregatedData = Object.values(groupedById).map(group => { // If only one item in group, no aggregation needed if (group.length === 1) return group[0]; // Use custom merge strategy if provided if (mergeStrategy && typeof mergeStrategy === 'function') { return mergeStrategy(group); } // Default aggregation strategy const base = { ...group[0] }; // Sum numeric fields for (const field of sumFields) { if (typeof base[field] === 'number') { base[field] = group.reduce((sum, item) => sum + (Number(item[field]) || 0), 0); } } // Concatenate array fields for (const field of concatArrays) { if (Array.isArray(base[field])) { const allValues = group.reduce((all, item) => { return Array.isArray(item[field]) ? [...all, ...item[field]] : all; }, []); // Remove duplicates if requested base[field] = removeDuplicates ? Array.from(new Set(allValues)) : allValues; } } // Add aggregation metadata base._aggregated = { count: group.length, timestamp: Date.now() }; return base; }); return aggregatedData; } return result; } /** * Propagates data to federated spaces * @param {object} holosphere - The HoloSphere instance * @param {string} holon - The holon identifier * @param {string} lens - The lens identifier * @param {object} data - The data to propagate * @param {object} [options] - Propagation options * @param {boolean} [options.useHolograms=true] - Use holograms for propagation (default: true) * @param {string[]} [options.targetSpaces] - Specific target spaces to propagate to (defaults to all federated spaces) * @param {string} [options.password] - Password for accessing the source holon (if needed) * @param {boolean} [options.propagateToParents=true] - Whether to automatically propagate to parent hexagons (default: true) * @param {number} [options.maxParentLevels=15] - Maximum number of parent levels to propagate to (default: 15) * @returns {Promise<object>} - Result with success count and errors */ export async function propagate(holosphere, holon, lens, data, options = {}) { if (!holosphere || !holon || !lens || !data) { throw new Error('propagate: Missing required parameters'); } // Default propagation options const { useHolograms = true, targetSpaces = null, password = null, propagateToParents = true, maxParentLevels = 15 } = options; const result = { success: 0, errors: 0, skipped: 0, messages: [], parentPropagation: { success: 0, errors: 0, skipped: 0, messages: [] } }; try { // ================================ FEDERATION PROPAGATION ================================ // Get federation info for this holon using getFederation const fedInfo = await getFederation(holosphere, holon, password); // Only perform federation propagation if there's valid federation info if (fedInfo && fedInfo.federation && fedInfo.federation.length > 0 && fedInfo.notify && fedInfo.notify.length > 0) { // Filter federation spaces to those in notify list let spaces = fedInfo.notify; // Further filter by targetSpaces if provided if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) { spaces = spaces.filter(space => targetSpaces.includes(space)); } if (spaces.length > 0) { // Filter spaces based on lens configuration spaces = spaces.filter(targetSpace => { const spaceConfig = fedInfo.lensConfig?.[targetSpace]; if (!spaceConfig) { result.messages.push(`No lens configuration for target space ${targetSpace}. Skipping propagation of lens '${lens}'.`); result.skipped++; return false; } // Ensure .federate is an array before calling .includes const federateLenses = Array.isArray(spaceConfig.federate) ? spaceConfig.federate : []; const shouldFederate = federateLenses.includes('*') || federateLenses.includes(lens); // Propagation now only depends on the 'federate' list configuration for the lens const shouldPropagate = shouldFederate; if (!shouldPropagate) { result.messages.push(`Propagation of lens '${lens}' to target space ${targetSpace} skipped: lens not in 'federate' configuration.`); result.skipped++; } return shouldPropagate; }); if (spaces.length > 0) { // Check if data is already a hologram const isAlreadyHologram = holosphere.isHologram(data); // For each target space, propagate the data const propagatePromises = spaces.map(async (targetSpace) => { try { let payloadToPut; const federationMeta = { origin: holon, // The space from which this data is being propagated sourceLens: lens, // The lens from which this data is being propagated propagatedAt: Date.now(), originalId: data.id }; if (useHolograms && !isAlreadyHologram) { // Create a new hologram referencing the original data const newHologram = holosphere.createHologram(holon, lens, data); payloadToPut = { ...newHologram, // This will be { id: data.id, soul: 'path/to/original' } _federation: federationMeta }; } else { // Propagate existing data (could be a full object or an existing hologram) // Make a shallow copy and update/add _federation metadata payloadToPut = { ...data, _federation: { ...(data._federation || {}), // Preserve existing _federation fields if any ...federationMeta // Add/overwrite with current propagation info } }; } // Store in the target space with redirection disabled and no further auto-propagation await holosphere.put(targetSpace, lens, payloadToPut, null, { disableHologramRedirection: true, autoPropagate: false }); result.success++; return true; } catch (error) { result.errors++; result.messages.push(`Error propagating ${data.id} to ${targetSpace}: ${error.message}`); return false; } }); await Promise.all(propagatePromises); } else { result.messages.push('No valid target spaces for federation propagation after lens filtering.'); } } else { result.messages.push('No valid target spaces found for federation propagation.'); } } else { result.messages.push(`No federation found for ${holon} or no notification targets available.`); } // ================================ PARENT PROPAGATION ================================ // Check if we should propagate to parent hexagons if (propagateToParents) { try { // Validate if the holon is a proper H3 hexagon // H3 hexagons should start with '8' and have a valid format let holonResolution; let isValidH3 = false; // First check: H3 hexagons should start with '8' and be at least 15 characters long if (typeof holon === 'string' && /^[8][0-9A-Fa-f]+$/.test(holon) && holon.length >= 15) { try { holonResolution = h3.getResolution(holon); // Additional validation: resolution should be >= 0 and <= 15 if (holonResolution >= 0 && holonResolution <= 15) { isValidH3 = true; } else { console.log(`[Federation] Holon ${holon} has invalid resolution: ${holonResolution}`); } } catch (error) { console.log(`[Federation] Holon ${holon} failed H3 validation: ${error.message}`); } } else { } if (!isValidH3) { result.parentPropagation.messages.push(`Holon ${holon} is not a valid H3 hexagon. Skipping parent propagation.`); result.parentPropagation.skipped++; } if (isValidH3 && holonResolution !== undefined) { // Get all parent hexagons up to the specified max levels const parentHexagons = []; let currentHolon = holon; let currentRes = holonResolution; let levelsProcessed = 0; while (currentRes > 0 && levelsProcessed < maxParentLevels) { try { const parent = h3.cellToParent(currentHolon, currentRes - 1); parentHexagons.push(parent); currentHolon = parent; currentRes--; levelsProcessed++; } catch (error) { console.error(`[Federation] Error getting parent for ${currentHolon}: ${error.message}`); result.parentPropagation.messages.push(`Error getting parent for ${currentHolon}: ${error.message}`); result.parentPropagation.errors++; break; } } if (parentHexagons.length > 0) { result.parentPropagation.messages.push(`Found ${parentHexagons.length} parent hexagons to propagate to: ${parentHexagons.join(', ')}`); // Check if data is already a hologram (reuse from federation section) const isAlreadyHologram = holosphere.isHologram(data); // Propagate to each parent hexagon const parentPropagatePromises = parentHexagons.map(async (parentHexagon) => { try { let payloadToPut; const parentFederationMeta = { origin: holon, // The original holon from which this data is being propagated sourceLens: lens, // The lens from which this data is being propagated propagatedAt: Date.now(), originalId: data.id, propagationType: 'parent', // Indicate this is parent propagation parentLevel: holonResolution - h3.getResolution(parentHexagon) // How many levels up }; if (useHolograms && !isAlreadyHologram) { // Create a new hologram referencing the original data const newHologram = holosphere.createHologram(holon, lens, data); payloadToPut = { ...newHologram, // This will be { id: data.id, soul: 'path/to/original' } _federation: parentFederationMeta }; } else { // Propagate existing data (could be a full object or an existing hologram) // Make a shallow copy and update/add _federation metadata payloadToPut = { ...data, _federation: { ...(data._federation || {}), // Preserve existing _federation fields if any ...parentFederationMeta // Add/overwrite with current propagation info } }; } // Store in the parent hexagon with redirection disabled and no further auto-propagation await holosphere.put(parentHexagon, lens, payloadToPut, null, { disableHologramRedirection: true, autoPropagate: false }); console.log(`[Federation] Successfully propagated to parent hexagon: ${parentHexagon}`); result.parentPropagation.success++; return true; } catch (error) { console.error(`[Federation] Error propagating ${data.id} to parent hexagon ${parentHexagon}: ${error.message}`); result.parentPropagation.errors++; result.parentPropagation.messages.push(`Error propagating ${data.id} to parent hexagon ${parentHexagon}: ${error.message}`); return false; } }); await Promise.all(parentPropagatePromises); } else { result.parentPropagation.messages.push(`No parent hexagons found for ${holon} (already at resolution 0 or max levels reached)`); result.parentPropagation.skipped++; } } } catch (error) { console.error(`[Federation] Error during parent propagation: ${error.message}`); result.parentPropagation.errors++; result.parentPropagation.messages.push(`Error during parent propagation: ${error.message}`); } } // ================================ END PARENT PROPAGATION ================================ result.propagated = result.success > 0 || result.parentPropagation.success > 0; return result; } catch (error) { return { ...result, error: error.message }; } } /** * Tracks a federated message across different chats * @param {object} holosphere - The HoloSphere instance * @param {string} originalChatId - The ID of the original chat * @param {string} messageId - The ID of the original message * @param {string} federatedChatId - The ID of the federated chat * @param {string} federatedMessageId - The ID of the message in the federated chat * @param {string} type - The type of message (e.g., 'quest', 'announcement') * @returns {Promise<void>} */ export async function federateMessage(holosphere, originalChatId, messageId, federatedChatId, federatedMessageId, type = 'generic') { const trackingKey = `${originalChatId}_${messageId}_fedmsgs`; const tracking = await holosphere.getGlobal('federation_messages', trackingKey) || { id: trackingKey, originalChatId, originalMessageId: messageId, type, messages: [] }; // Update or add the federated message info const existingMsg = tracking.messages.find(m => m.chatId === federatedChatId); if (existingMsg) { existingMsg.messageId = federatedMessageId; existingMsg.timestamp = Date.now(); } else { tracking.messages.push({ chatId: federatedChatId, messageId: federatedMessageId, timestamp: Date.now() }); } await holosphere.putGlobal('federation_messages', tracking); } /** * Gets all federated messages for a given original message * @param {object} holosphere - The HoloSphere instance * @param {string} originalChatId - The ID of the original chat * @param {string} messageId - The ID of the original message * @returns {Promise<Object|null>} The tracking information for the message */ export async function getFederatedMessages(holosphere, originalChatId, messageId) { const trackingKey = `${originalChatId}_${messageId}_fedmsgs`; return await holosphere.getGlobal('federation_messages', trackingKey); } /** * Updates a federated message across all federated chats * @param {object} holosphere - The HoloSphere instance * @param {string} originalChatId - The ID of the original chat * @param {string} messageId - The ID of the original message * @param {Function} updateCallback - Function to update the message in each chat * @returns {Promise<void>} */ export async function updateFederatedMessages(holosphere, originalChatId, messageId, updateCallback) { const tracking = await getFederatedMessages(holosphere, originalChatId, messageId); if (!tracking?.messages) return; for (const msg of tracking.messages) { try { await updateCallback(msg.chatId, msg.messageId); } catch (error) { } } } /** * Resets all federation relationships for a space * @param {object} holosphere - The HoloSphere instance * @param {string} spaceId - The ID of the space to reset federation for * @param {string} [password] - Optional password for the space * @param {object} [options] - Reset options * @param {boolean} [options.notifyPartners=true] - Whether to notify federation partners about the reset * @param {string} [options.spaceName] - Optional name for the space (defaults to spaceId if not provided) * @returns {Promise<object>} - Result object with success/error info */ export async function resetFederation(holosphe