holosphere 1.1.10 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/federation.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Provides methods for creating, managing, and using federated spaces
4
4
  */
5
5
 
6
+ import * as h3 from 'h3-js';
6
7
 
7
8
  /**
8
9
  * Creates a federation relationship between two spaces
@@ -14,10 +15,12 @@
14
15
  * @param {string} [password1] - Optional password for the first space
15
16
  * @param {string} [password2] - Optional password for the second space
16
17
  * @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications (default: true)
18
+ * @param {object} [lensConfig] - Optional lens-specific configuration
19
+ * @param {string[]} [lensConfig.federate] - List of lenses to federate (default: all)
20
+ * @param {string[]} [lensConfig.notify] - List of lenses to notify (default: all)
17
21
  * @returns {Promise<boolean>} - True if federation was created successfully
18
22
  */
19
- export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true) {
20
- console.log('FEDERATING', spaceId1, spaceId2, password1, password2, bidirectional)
23
+ export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true, lensConfig = {}) {
21
24
  if (!spaceId1 || !spaceId2) {
22
25
  throw new Error('federate: Missing required space IDs');
23
26
  }
@@ -27,40 +30,59 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
27
30
  throw new Error('Cannot federate a space with itself');
28
31
  }
29
32
 
33
+ // Validate lens configuration
34
+ const { federate = [], notify = [] } = lensConfig;
35
+ if (!Array.isArray(federate) || !Array.isArray(notify)) {
36
+ throw new Error('federate: lensConfig.federate and lensConfig.notify must be arrays');
37
+ }
38
+
39
+ // Use the provided lens configurations directly
40
+ const federateLenses = federate;
41
+ const notifyLenses = notify;
42
+
30
43
  try {
31
44
  // Get or create federation info for first space (A)
32
- let fedInfo1 = null;
45
+ let fedInfo1 = null;
33
46
 
34
47
  try {
35
48
  fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
36
49
  } catch (error) {
37
- console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
38
- // Create new federation info if it doesn't exist
39
-
40
50
  }
51
+
41
52
  if (fedInfo1 == null) {
42
53
  fedInfo1 = {
43
54
  id: spaceId1,
44
55
  name: spaceId1,
45
56
  federation: [],
46
57
  notify: [],
58
+ lensConfig: {}, // New field for lens-specific settings
47
59
  timestamp: Date.now()
48
60
  };
49
61
  }
50
-
51
62
 
52
- // Ensure arrays exist
63
+ // Ensure arrays and lensConfig exist
53
64
  if (!fedInfo1.federation) fedInfo1.federation = [];
54
65
  if (!fedInfo1.notify) fedInfo1.notify = [];
66
+ if (!fedInfo1.lensConfig) fedInfo1.lensConfig = {};
55
67
 
56
- // Add space2 to space1's federation and notify lists if not already present
68
+ // Add space2 to space1's federation list if not already present
57
69
  if (!fedInfo1.federation.includes(spaceId2)) {
58
70
  fedInfo1.federation.push(spaceId2);
59
71
  }
60
- // // Always add to notify list for the first space (primary direction)
61
- // if (!fedInfo1.notify.includes(spaceId2)) {
62
- // fedInfo1.notify.push(spaceId2);
63
- // }
72
+
73
+ // Add space2 to space1's notify list if not already present
74
+ if (!fedInfo1.notify.includes(spaceId2)) {
75
+ fedInfo1.notify.push(spaceId2);
76
+ }
77
+
78
+ // Store lens configuration for space2
79
+ const newLensConfigsForSpace1 = { ...(fedInfo1.lensConfig || {}) }; // Shallow copy existing lensConfigs for space1
80
+ newLensConfigsForSpace1[spaceId2] = { // Add/update config for the target spaceId2
81
+ federate: [...federateLenses], // federateLenses & notifyLenses are from the main lensConfig parameter
82
+ notify: [...notifyLenses],
83
+ timestamp: Date.now()
84
+ };
85
+ fedInfo1.lensConfig = newLensConfigsForSpace1; // Assign the new/modified object back to fedInfo1.lensConfig
64
86
 
65
87
  // Update timestamp
66
88
  fedInfo1.timestamp = Date.now();
@@ -68,42 +90,50 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
68
90
  // Save updated federation info for space1
69
91
  try {
70
92
  await holosphere.putGlobal('federation', fedInfo1, password1);
71
- console.log(`Updated federation info for ${spaceId1}`);
72
93
  } catch (error) {
73
- console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
74
94
  throw new Error(`Failed to create federation: ${error.message}`);
75
95
  }
76
-
96
+
77
97
  // If bidirectional is true, handle space2 (B) as well
78
- //if (bidirectional && password2) {
79
98
  {
80
99
  let fedInfo2 = null;
81
100
  try {
82
101
  fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
83
102
  } catch (error) {
84
- console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
85
- // Create new federation info if it doesn't exist
86
-
87
103
  }
104
+
88
105
  if (fedInfo2 == null) {
89
106
  fedInfo2 = {
90
107
  id: spaceId2,
91
108
  name: spaceId2,
92
109
  federation: [],
93
110
  notify: [],
111
+ lensConfig: {}, // New field for lens-specific settings
94
112
  timestamp: Date.now()
95
113
  };
96
114
  }
97
-
98
- // Add nEnsure arrays exist
99
-
115
+
116
+ // Ensure arrays and lensConfig exist
117
+ if (!fedInfo2.federation) fedInfo2.federation = [];
100
118
  if (!fedInfo2.notify) fedInfo2.notify = [];
119
+ if (!fedInfo2.lensConfig) fedInfo2.lensConfig = {};
120
+
121
+ // Add space1 to space2's federation list if bidirectional
122
+ if (bidirectional && !fedInfo2.federation.includes(spaceId1)) {
123
+ fedInfo2.federation.push(spaceId1);
124
+ }
101
125
 
102
- // Add space1 to space2's federation list if not already present
126
+ // Add space1 to space2's notify list if not already present
103
127
  if (!fedInfo2.notify.includes(spaceId1)) {
104
128
  fedInfo2.notify.push(spaceId1);
105
129
  }
106
-
130
+
131
+ // Store lens configuration for space1
132
+ fedInfo2.lensConfig[spaceId1] = {
133
+ federate: bidirectional ? [...federateLenses] : [], // Create a copy of the array
134
+ notify: bidirectional ? [...notifyLenses] : [], // Create a copy of the array
135
+ timestamp: Date.now()
136
+ };
107
137
 
108
138
  // Update timestamp
109
139
  fedInfo2.timestamp = Date.now();
@@ -111,10 +141,7 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
111
141
  // Save updated federation info for space2
112
142
  try {
113
143
  await holosphere.putGlobal('federation', fedInfo2, password2);
114
- console.log(`Updated federation info for ${spaceId2}`);
115
144
  } catch (error) {
116
- console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
117
- // Don't throw here as the main federation was successful
118
145
  }
119
146
  }
120
147
 
@@ -125,19 +152,20 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
125
152
  space2: spaceId2,
126
153
  created: Date.now(),
127
154
  status: 'active',
128
- bidirectional: bidirectional
155
+ bidirectional: bidirectional,
156
+ lensConfig: {
157
+ federate: [...federateLenses], // Create a copy of the array
158
+ notify: [...notifyLenses] // Create a copy of the array
159
+ }
129
160
  };
130
-
161
+
131
162
  try {
132
163
  await holosphere.putGlobal('federationMeta', federationMeta);
133
- console.log(`Created federation metadata for ${spaceId1} and ${spaceId2}`);
134
164
  } catch (error) {
135
- console.warn(`Could not create federation metadata: ${error.message}`);
136
165
  }
137
166
 
138
167
  return true;
139
168
  } catch (error) {
140
- console.error(`Federation creation failed: ${error.message}`);
141
169
  throw error;
142
170
  }
143
171
  }
@@ -203,7 +231,6 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
203
231
  // Execute callback with the data
204
232
  await callback(data, federatedSpace, lens);
205
233
  } catch (error) {
206
- console.warn('Federation notification error:', error);
207
234
  }
208
235
  });
209
236
 
@@ -211,7 +238,6 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
211
238
  subscriptions.push(sub);
212
239
  }
213
240
  } catch (error) {
214
- console.warn(`Error creating subscription for ${federatedSpace}/${lens}:`, error);
215
241
  }
216
242
  }
217
243
  }
@@ -226,7 +252,6 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
226
252
  sub.unsubscribe();
227
253
  }
228
254
  } catch (error) {
229
- console.warn('Error unsubscribing:', error);
230
255
  }
231
256
  });
232
257
  // Clear the subscriptions array
@@ -252,6 +277,35 @@ export async function getFederation(holosphere, spaceId, password = null) {
252
277
  return await holosphere.getGlobal('federation', spaceId, password);
253
278
  }
254
279
 
280
+ /**
281
+ * Retrieves the lens-specific configuration for a federation link between two spaces.
282
+ * @param {object} holosphere - The HoloSphere instance
283
+ * @param {string} spaceId - The ID of the source space.
284
+ * @param {string} targetSpaceId - The ID of the target space in the federation link.
285
+ * @param {string} [password] - Optional password for the source space.
286
+ * @returns {Promise<object|null>} - An object with 'federate' and 'notify' arrays, or null if not found.
287
+ */
288
+ export async function getFederatedConfig(holosphere, spaceId, targetSpaceId, password = null) {
289
+ if (!holosphere || !spaceId || !targetSpaceId) {
290
+ throw new Error('getFederatedConfig: Missing required parameters');
291
+ }
292
+
293
+ try {
294
+ const fedInfo = await getFederation(holosphere, spaceId, password);
295
+
296
+ if (fedInfo && fedInfo.lensConfig && fedInfo.lensConfig[targetSpaceId]) {
297
+ return {
298
+ federate: fedInfo.lensConfig[targetSpaceId].federate || [],
299
+ notify: fedInfo.lensConfig[targetSpaceId].notify || []
300
+ };
301
+ }
302
+ return null; // Or return an empty config: { federate: [], notify: [] }
303
+ } catch (error) {
304
+ console.error(`Error getting federated config for ${spaceId} -> ${targetSpaceId}: ${error.message}`);
305
+ throw error;
306
+ }
307
+ }
308
+
255
309
  /**
256
310
  * Removes a federation relationship between spaces
257
311
  * @param {object} holosphere - The HoloSphere instance
@@ -272,43 +326,59 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
272
326
  try {
273
327
  fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
274
328
  } catch (error) {
275
- console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
329
+ console.error(`Error getting fedInfo1 for ${spaceId1} during unfederate: ${error.message}`);
330
+ // If we can't get fedInfo1, we can't modify it. Decide if this is a critical failure.
331
+ // For now, we'll let it proceed to attempt metadata cleanup, but a throw here might be valid.
276
332
  }
277
333
 
278
- if (!fedInfo1 || !fedInfo1.federation) {
279
- console.warn(`Federation not found for space ${spaceId1}`);
280
- // Continue anyway to clean up any potential metadata
334
+ if (!fedInfo1) {
335
+ // If fedInfo1 doesn't exist, log and proceed to metadata cleanup.
336
+ console.warn(`No federation info found for ${spaceId1}. Skipping its update.`);
281
337
  } else {
282
- // Update first space federation info
338
+ // Ensure arrays exist
339
+ if (!fedInfo1.federation) fedInfo1.federation = [];
340
+ if (!fedInfo1.notify) fedInfo1.notify = [];
341
+
342
+ // Update first space federation info - remove from both federation and notify arrays
343
+ const originalFederationLength = fedInfo1.federation.length;
344
+ const originalNotifyLength = fedInfo1.notify.length;
345
+
283
346
  fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
347
+ fedInfo1.notify = fedInfo1.notify.filter(id => id !== spaceId2);
284
348
  fedInfo1.timestamp = Date.now();
285
349
 
350
+ console.log(`Unfederate: Removed ${spaceId2} from ${spaceId1}: federation ${originalFederationLength} -> ${fedInfo1.federation.length}, notify ${originalNotifyLength} -> ${fedInfo1.notify.length}`);
351
+
286
352
  try {
287
353
  await holosphere.putGlobal('federation', fedInfo1, password1);
288
- console.log(`Updated federation info for ${spaceId1}`);
289
354
  } catch (error) {
290
- console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
355
+ console.error(`Failed to update fedInfo1 for ${spaceId1} during unfederate: ${error.message}`);
356
+ throw error; // RE-THROW to signal failure
291
357
  }
292
358
  }
293
359
 
294
- // Update second space federation info if password provided
295
- if (password2) {
360
+ // Update second space federation info (remove spaceId1 from spaceId2's notify list)
361
+ // This part is usually for full bidirectional unfederation cleanup.
362
+ // The original code only did this if password2 was provided.
363
+ if (password2) { // Retaining original condition for this block
296
364
  let fedInfo2 = null;
297
365
  try {
298
366
  fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
299
367
  } catch (error) {
300
- console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
368
+ console.error(`Error getting fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
301
369
  }
302
370
 
303
- if (fedInfo2 && fedInfo2.notify) {
371
+ if (!fedInfo2 || !fedInfo2.notify) {
372
+ console.warn(`No notify array found for ${spaceId2} or fedInfo2 is null. Skipping its update.`);
373
+ } else {
304
374
  fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
305
375
  fedInfo2.timestamp = Date.now();
306
376
 
307
377
  try {
308
378
  await holosphere.putGlobal('federation', fedInfo2, password2);
309
- console.log(`Updated federation info for ${spaceId2}`);
310
379
  } catch (error) {
311
- console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
380
+ console.error(`Failed to update fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
381
+ throw error; // RE-THROW to signal failure
312
382
  }
313
383
  }
314
384
  }
@@ -324,17 +394,17 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
324
394
  if (meta) {
325
395
  meta.status = 'inactive';
326
396
  meta.endedAt = Date.now();
327
- await holosphere.putGlobal('federationMeta', meta);
328
- console.log(`Updated federation metadata for ${spaceId1} and ${spaceId2}`);
397
+ await holosphere.putGlobal('federationMeta', meta); // Not re-throwing here as it's metadata cleanup
329
398
  }
330
399
  } catch (error) {
331
- console.warn(`Could not update federation metadata: ${error.message}`);
400
+ console.warn(`Failed to update federationMeta during unfederate: ${error.message}`);
332
401
  }
333
402
 
334
403
  return true;
335
404
  } catch (error) {
336
- console.error(`Federation removal failed: ${error.message}`);
337
- throw error;
405
+ // This will catch errors re-thrown from putGlobal or from getGlobal if they occur before specific catches.
406
+ console.error(`Critical error during unfederate operation for ${spaceId1}-${spaceId2}: ${error.message}`);
407
+ throw error; // Ensure the main operation failure is propagated
338
408
  }
339
409
  }
340
410
 
@@ -373,42 +443,42 @@ export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = n
373
443
 
374
444
  // Save updated federation info
375
445
  await holosphere.putGlobal('federation', fedInfo, password1);
376
- console.log(`Removed ${spaceId2} from ${spaceId1}'s notify list`);
377
446
  return true;
378
447
  } else {
379
- console.log(`${spaceId2} not found in ${spaceId1}'s notify list`);
380
448
  return false;
381
449
  }
382
450
  } catch (error) {
383
- console.error(`Remove notification failed: ${error.message}`);
384
451
  throw error;
385
452
  }
386
453
  }
387
454
 
388
455
  /**
389
- * Get and combine data from local and federated sources
456
+ * Get and combine data from local and federated sources.
457
+ * If `options.queryIds` is provided, fetches only those specific IDs using `get()`.
458
+ * Otherwise, falls back to fetching all data using `getAll()` (potentially inefficient).
459
+ *
390
460
  * @param {HoloSphere} holosphere The HoloSphere instance
391
- * @param {string} holon The local holon name
461
+ * @param {string} holon The local holon name (used as the space ID for federation info)
392
462
  * @param {string} lens The lens to query
393
463
  * @param {Object} options Options for data retrieval and aggregation
394
- * @param {boolean} options.aggregate Whether to aggregate results by ID (default: false)
395
- * @param {string} options.idField The field to use as ID (default: '_id')
396
- * @param {string[]} options.sumFields Fields to sum during aggregation (default: [])
397
- * @param {string[]} options.concatArrays Array fields to concatenate during aggregation (default: [])
398
- * @param {boolean} options.removeDuplicates Whether to remove duplicates in concatenated arrays (default: true)
399
- * @param {Function} options.mergeStrategy Custom merge function for aggregation (default: null)
400
- * @param {boolean} options.includeLocal Whether to include local data (default: true)
401
- * @param {boolean} options.includeFederated Whether to include federated data (default: true)
402
- * @param {boolean} options.resolveReferences Whether to resolve federation references (default: true)
403
- * @param {number} options.maxFederatedSpaces Maximum number of federated spaces to query (default: -1 for all)
404
- * @param {number} options.timeout Timeout in milliseconds for federated queries (default: 10000)
464
+ * @param {string[]} [options.queryIds] Optional array of specific item IDs to fetch.
465
+ * @param {boolean} [options.aggregate=false] Whether to aggregate results by ID
466
+ * @param {string} [options.idField='id'] The field to use as ID
467
+ * @param {string[]} [options.sumFields=[]] Fields to sum during aggregation
468
+ * @param {string[]} [options.concatArrays=[]] Array fields to concatenate during aggregation
469
+ * @param {boolean} [options.removeDuplicates=true] Whether to remove duplicates in concatenated arrays
470
+ * @param {Function} [options.mergeStrategy=null] Custom merge function for aggregation
471
+ * @param {boolean} [options.includeLocal=true] Whether to include local data
472
+ * @param {boolean} [options.includeFederated=true] Whether to include federated data
473
+ * @param {boolean} [options.resolveReferences=true] Whether to resolve federation references
474
+ * @param {number} [options.maxFederatedSpaces=-1] Maximum number of federated spaces to query (-1 for all)
475
+ * @param {number} [options.timeout=10000] Timeout in milliseconds for federated queries
405
476
  * @returns {Promise<Array>} Combined array of local and federated data
406
477
  */
407
478
  export async function getFederated(holosphere, holon, lens, options = {}) {
408
- console.log(`getFederated called with options:`, JSON.stringify(options));
409
-
410
- // Set default options
479
+ // Set default options and extract queryIds
411
480
  const {
481
+ queryIds = null, // New option
412
482
  aggregate = false,
413
483
  idField = 'id',
414
484
  sumFields = [],
@@ -417,91 +487,85 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
417
487
  mergeStrategy = null,
418
488
  includeLocal = true,
419
489
  includeFederated = true,
420
- resolveReferences = true, // Default to true
490
+ resolveReferences = true,
421
491
  maxFederatedSpaces = -1,
422
492
  timeout = 10000
423
493
  } = options;
424
494
 
425
495
  console.log(`resolveReferences option: ${resolveReferences}`);
426
-
496
+ console.log(`Querying specific IDs:`, queryIds ? queryIds.join(', ') : 'No (fetching all)');
497
+
427
498
  // Validate required parameters
428
499
  if (!holosphere || !holon || !lens) {
429
500
  throw new Error('Missing required parameters: holosphere, holon, and lens are required');
430
501
  }
431
502
 
432
- // Get federation info for current space
433
- // Use holon as the space ID
503
+ // Get federation info for current space (using holon as spaceId)
434
504
  const spaceId = holon;
435
505
  const fedInfo = await getFederation(holosphere, spaceId);
436
506
 
437
- console.log(`Federation info retrieved:`, JSON.stringify(fedInfo));
438
-
439
- // Initialize result array and track processed IDs to avoid duplicates
440
- const result = [];
441
- const processedIds = new Set();
442
- const references = new Map(); // To keep track of references for resolution
443
-
444
- // Process each federated space first to prioritize federation data
507
+ // Initialize result array and track processed IDs to avoid duplicates/redundant fetches
508
+ const fetchedItems = new Map(); // Use Map to store fetched items by ID
509
+ const processedIds = new Set(); // Track IDs added to the final result
510
+
511
+ const fetchPromises = [];
512
+
513
+ // Determine list of spaces to query (local + federated)
514
+ let spacesToQuery = [];
515
+ if (includeLocal) {
516
+ spacesToQuery.push(holon); // Add local holon first
517
+ }
445
518
  if (includeFederated && fedInfo && fedInfo.federation && fedInfo.federation.length > 0) {
446
- console.log(`Found ${fedInfo.federation.length} federated spaces`);
447
-
448
- // Limit number of federated spaces to query
449
519
  const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.federation : fedInfo.federation.slice(0, maxFederatedSpaces);
450
- console.log(`Will process ${federatedSpaces.length} federated spaces: ${JSON.stringify(federatedSpaces)}`);
451
-
452
- // Process federated spaces
453
- for (const federatedSpace of federatedSpaces) {
454
- try {
455
- console.log(`=== PROCESSING FEDERATED SPACE: ${federatedSpace} ===`);
456
-
457
- // Get all data for this lens from the federated space
458
- const federatedItems = await holosphere.getAll(federatedSpace, lens);
459
- console.log(`Got ${federatedItems.length} items from federated space ${federatedSpace}`);
460
- console.log(`Federated items:`, JSON.stringify(federatedItems));
461
-
462
- // Process each item
463
- for (const item of federatedItems) {
464
- if (!item) {
465
- console.log('Item is null or undefined, skipping');
466
- continue;
467
- }
468
-
469
- console.log(`Checking item for ID field '${idField}':`, item);
470
-
471
- if (!item[idField]) {
472
- console.log(`Item missing ID field '${idField}', available fields:`, Object.keys(item));
473
- continue;
474
- }
475
-
476
- // For now, just add this item to results, we'll resolve references later
477
- result.push(item);
478
- processedIds.add(item[idField]);
479
- }
480
- } catch (error) {
481
- console.warn(`Error processing federated space ${federatedSpace}: ${error.message}`);
482
- }
483
- }
520
+ spacesToQuery = spacesToQuery.concat(federatedSpaces);
484
521
  }
485
-
486
- // Now get local data if requested
487
- if (includeLocal) {
488
- const localData = await holosphere.getAll(holon, lens);
489
- console.log(`Got ${localData.length} local items from holon ${holon}`);
490
-
491
- // Add each local item to results, but only if not already processed
492
- for (const item of localData) {
493
- if (item && item[idField] && !processedIds.has(item[idField])) {
494
- result.push(item);
495
- processedIds.add(item[idField]);
496
- } else if (item && item[idField]) {
497
- console.log(`Local item ${item[idField]} already in result from federation, skipping`);
522
+
523
+ // Fetch data from all relevant spaces
524
+ for (const currentSpace of spacesToQuery) {
525
+ if (queryIds && Array.isArray(queryIds)) {
526
+ // --- Fetch specific IDs using holosphere.get ---
527
+ console.log(`Fetching specific IDs from ${currentSpace}: ${queryIds.join(', ')}`);
528
+ for (const itemId of queryIds) {
529
+ if (fetchedItems.has(itemId)) continue; // Skip if already fetched
530
+ fetchPromises.push(
531
+ holosphere.get(currentSpace, lens, itemId)
532
+ .then(item => {
533
+ if (item) {
534
+ fetchedItems.set(itemId, item);
535
+ }
536
+ })
537
+ .catch(err => console.warn(`Error fetching item ${itemId} from ${currentSpace}: ${err.message}`))
538
+ );
498
539
  }
540
+ } else {
541
+ // --- Fetch all data using holosphere.getAll (Fallback - inefficient) ---
542
+ if(currentSpace === holon && includeLocal) { // Only warn once for local
543
+ console.warn(`getFederated: No queryIds provided. Falling back to fetching ALL items from ${currentSpace} using getAll. This can be inefficient.`);
544
+ }
545
+ console.log(`Fetching ALL items from ${currentSpace}`);
546
+ fetchPromises.push(
547
+ holosphere.getAll(currentSpace, lens)
548
+ .then(items => {
549
+ for (const item of items) {
550
+ if (item && item[idField] && !fetchedItems.has(item[idField])) {
551
+ fetchedItems.set(item[idField], item);
552
+ }
553
+ }
554
+ })
555
+ .catch(err => console.warn(`Error fetching all items from ${currentSpace}: ${err.message}`))
556
+ );
499
557
  }
500
558
  }
501
-
559
+
560
+ // Wait for all fetches to complete
561
+ await Promise.all(fetchPromises);
562
+
563
+ // Convert Map values to array for processing
564
+ const result = Array.from(fetchedItems.values());
565
+
502
566
  // Now resolve references if needed
503
- if (resolveReferences) {
504
- console.log(`Resolving references for ${result.length} items`);
567
+ if (resolveReferences && result.length > 0) {
568
+ console.log(`Resolving references for ${result.length} fetched items`);
505
569
 
506
570
  for (let i = 0; i < result.length; i++) {
507
571
  const item = result[i];
@@ -542,9 +606,7 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
542
606
  timestamp: Date.now()
543
607
  }
544
608
  };
545
- console.log(`Reference resolved successfully via soul path, processed item:`, JSON.stringify(result[i]));
546
609
  } else {
547
- console.warn(`Could not resolve reference: original data not found at extracted path`);
548
610
  // Instead of leaving the original reference, create an error object
549
611
  result[i] = {
550
612
  id: item.id,
@@ -572,7 +634,6 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
572
634
  };
573
635
  }
574
636
  } catch (refError) {
575
- console.warn(`Error resolving reference by soul in getFederated: ${refError.message}`);
576
637
  // Instead of leaving the original reference, create an error object
577
638
  result[i] = {
578
639
  id: item.id,
@@ -615,12 +676,10 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
615
676
  timestamp: Date.now()
616
677
  }
617
678
  };
618
- console.log(`Legacy reference resolved successfully, processed item:`, JSON.stringify(result[i]));
619
679
  } else {
620
680
  console.warn(`Could not resolve legacy reference: original data not found`);
621
681
  }
622
682
  } catch (refError) {
623
- console.warn(`Error resolving legacy reference in getFederated: ${refError.message}`);
624
683
  }
625
684
  }
626
685
  }
@@ -692,9 +751,11 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
692
751
  * @param {string} lens - The lens identifier
693
752
  * @param {object} data - The data to propagate
694
753
  * @param {object} [options] - Propagation options
695
- * @param {boolean} [options.useReferences=true] - Whether to use references instead of duplicating data
754
+ * @param {boolean} [options.useHolograms=true] - Use holograms for propagation (default: true)
696
755
  * @param {string[]} [options.targetSpaces] - Specific target spaces to propagate to (defaults to all federated spaces)
697
756
  * @param {string} [options.password] - Password for accessing the source holon (if needed)
757
+ * @param {boolean} [options.propagateToParents=true] - Whether to automatically propagate to parent hexagons (default: true)
758
+ * @param {number} [options.maxParentLevels=15] - Maximum number of parent levels to propagate to (default: 15)
698
759
  * @returns {Promise<object>} - Result with success count and errors
699
760
  */
700
761
  export async function propagate(holosphere, holon, lens, data, options = {}) {
@@ -702,127 +763,254 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
702
763
  throw new Error('propagate: Missing required parameters');
703
764
  }
704
765
  // Default propagation options
705
- const {
706
- useReferences = true,
707
- targetSpaces = null,
708
- password = null
766
+ const {
767
+ useHolograms = true,
768
+ targetSpaces = null,
769
+ password = null,
770
+ propagateToParents = true,
771
+ maxParentLevels = 15
709
772
  } = options;
710
773
 
711
774
  const result = {
712
775
  success: 0,
713
776
  errors: 0,
714
- errorDetails: [],
715
- propagated: false,
716
- referencesUsed: useReferences
777
+ skipped: 0,
778
+ messages: [],
779
+ parentPropagation: {
780
+ success: 0,
781
+ errors: 0,
782
+ skipped: 0,
783
+ messages: []
784
+ }
717
785
  };
718
786
 
719
787
  try {
788
+ // ================================ FEDERATION PROPAGATION ================================
789
+
720
790
  // Get federation info for this holon using getFederation
721
791
  const fedInfo = await getFederation(holosphere, holon, password);
722
792
 
723
- // If no federation info or no federation list, return with message
724
- if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
725
- return {
726
- ...result,
727
- message: `No federation found for ${holon}`
728
- };
729
- }
730
-
731
- // If no notification list or it's empty, return with message
732
- if (!fedInfo.notify || fedInfo.notify.length === 0) {
733
- return {
734
- ...result,
735
- message: `No notification targets found for ${holon}`
736
- };
737
- }
738
-
739
- // Filter federation spaces to those in notify list
740
- let spaces = fedInfo.notify;
741
-
742
- // Further filter by targetSpaces if provided
743
- if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) {
744
- spaces = spaces.filter(space => targetSpaces.includes(space));
745
- }
746
-
747
- if (spaces.length === 0) {
748
- return {
749
- ...result,
750
- message: 'No valid target spaces found after filtering'
751
- };
793
+ // Only perform federation propagation if there's valid federation info
794
+ if (fedInfo && fedInfo.federation && fedInfo.federation.length > 0 && fedInfo.notify && fedInfo.notify.length > 0) {
795
+ // Filter federation spaces to those in notify list
796
+ let spaces = fedInfo.notify;
797
+
798
+ // Further filter by targetSpaces if provided
799
+ if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) {
800
+ spaces = spaces.filter(space => targetSpaces.includes(space));
801
+ }
802
+
803
+ if (spaces.length > 0) {
804
+ // Filter spaces based on lens configuration
805
+ spaces = spaces.filter(targetSpace => {
806
+ const spaceConfig = fedInfo.lensConfig?.[targetSpace];
807
+ if (!spaceConfig) {
808
+ result.messages.push(`No lens configuration for target space ${targetSpace}. Skipping propagation of lens '${lens}'.`);
809
+ result.skipped++;
810
+ return false;
811
+ }
812
+
813
+ // Ensure .federate is an array before calling .includes
814
+ const federateLenses = Array.isArray(spaceConfig.federate) ? spaceConfig.federate : [];
815
+
816
+ const shouldFederate = federateLenses.includes('*') || federateLenses.includes(lens);
817
+
818
+ // Propagation now only depends on the 'federate' list configuration for the lens
819
+ const shouldPropagate = shouldFederate;
820
+
821
+ if (!shouldPropagate) {
822
+ result.messages.push(`Propagation of lens '${lens}' to target space ${targetSpace} skipped: lens not in 'federate' configuration.`);
823
+ result.skipped++;
824
+ }
825
+
826
+ return shouldPropagate;
827
+ });
828
+
829
+ if (spaces.length > 0) {
830
+ // Check if data is already a hologram
831
+ const isAlreadyHologram = holosphere.isHologram(data);
832
+
833
+ // For each target space, propagate the data
834
+ const propagatePromises = spaces.map(async (targetSpace) => {
835
+ try {
836
+ let payloadToPut;
837
+ const federationMeta = {
838
+ origin: holon, // The space from which this data is being propagated
839
+ sourceLens: lens, // The lens from which this data is being propagated
840
+ propagatedAt: Date.now(),
841
+ originalId: data.id
842
+ };
843
+
844
+ if (useHolograms && !isAlreadyHologram) {
845
+ // Create a new hologram referencing the original data
846
+ const newHologram = holosphere.createHologram(holon, lens, data);
847
+ payloadToPut = {
848
+ ...newHologram, // This will be { id: data.id, soul: 'path/to/original' }
849
+ _federation: federationMeta
850
+ };
851
+ } else {
852
+ // Propagate existing data (could be a full object or an existing hologram)
853
+ // Make a shallow copy and update/add _federation metadata
854
+ payloadToPut = {
855
+ ...data,
856
+ _federation: {
857
+ ...(data._federation || {}), // Preserve existing _federation fields if any
858
+ ...federationMeta // Add/overwrite with current propagation info
859
+ }
860
+ };
861
+ }
862
+
863
+ // Store in the target space with redirection disabled and no further auto-propagation
864
+ await holosphere.put(targetSpace, lens, payloadToPut, null, {
865
+ disableHologramRedirection: true,
866
+ autoPropagate: false
867
+ });
868
+
869
+ result.success++;
870
+ return true;
871
+ } catch (error) {
872
+ result.errors++;
873
+ result.messages.push(`Error propagating ${data.id} to ${targetSpace}: ${error.message}`);
874
+ return false;
875
+ }
876
+ });
877
+
878
+ await Promise.all(propagatePromises);
879
+ } else {
880
+ result.messages.push('No valid target spaces for federation propagation after lens filtering.');
881
+ }
882
+ } else {
883
+ result.messages.push('No valid target spaces found for federation propagation.');
884
+ }
885
+ } else {
886
+ result.messages.push(`No federation found for ${holon} or no notification targets available.`);
752
887
  }
753
888
 
754
- // Check if data is already a reference
755
- const isAlreadyReference = holosphere.isReference(data);
889
+ // ================================ PARENT PROPAGATION ================================
756
890
 
757
- // For each target space, propagate the data
758
- const propagatePromises = spaces.map(async (targetSpace) => {
891
+ // Check if we should propagate to parent hexagons
892
+ if (propagateToParents) {
893
+ console.log(`[Federation] Starting parent propagation for holon: ${holon}`);
759
894
  try {
760
- // If using references and data isn't already a reference, create a reference
761
- if (useReferences && !isAlreadyReference) {
762
- // Create a reference object using the dedicated utility
763
- const reference = holosphere.createReference(holon, lens, data);
764
-
765
- // Add federation metadata
766
- reference._federation = {
767
- origin: holon,
768
- lens: lens,
769
- timestamp: Date.now()
770
- };
771
-
772
- console.log(`Using reference: ${reference.soul} for data: ${data.id}`);
895
+ // Check if the holon is a valid H3 hexagon
896
+ let holonResolution;
897
+ try {
898
+ holonResolution = h3.getResolution(holon);
899
+ console.log(`[Federation] Holon ${holon} is valid H3 hexagon with resolution: ${holonResolution}`);
900
+ } catch (error) {
901
+ // Not a valid H3 hexagon, skip parent propagation
902
+ console.log(`[Federation] Holon ${holon} is not a valid H3 hexagon: ${error.message}`);
903
+ result.parentPropagation.messages.push(`Holon ${holon} is not a valid H3 hexagon. Skipping parent propagation.`);
904
+ result.parentPropagation.skipped++;
905
+ }
906
+
907
+ if (holonResolution !== undefined) {
908
+ // Get all parent hexagons up to the specified max levels
909
+ const parentHexagons = [];
910
+ let currentHolon = holon;
911
+ let currentRes = holonResolution;
912
+ let levelsProcessed = 0;
773
913
 
774
- // Store the reference in the target space without propagation
775
- await holosphere.put(targetSpace, lens, reference, null, { autoPropagate: false });
914
+ console.log(`[Federation] Getting parent hexagons for ${holon} (resolution ${holonResolution}) up to ${maxParentLevels} levels`);
776
915
 
777
- result.success++;
778
- return true;
779
- }
780
- // If already a reference, propagate it as is
781
- else if (isAlreadyReference) {
782
- // Add federation metadata if needed
783
- const referenceToStore = {
784
- ...data,
785
- _federation: data._federation || {
786
- origin: holon,
787
- lens: lens,
788
- timestamp: Date.now()
916
+ while (currentRes > 0 && levelsProcessed < maxParentLevels) {
917
+ try {
918
+ const parent = h3.cellToParent(currentHolon, currentRes - 1);
919
+ parentHexagons.push(parent);
920
+ console.log(`[Federation] Found parent hexagon: ${parent} (resolution ${currentRes - 1})`);
921
+ currentHolon = parent;
922
+ currentRes--;
923
+ levelsProcessed++;
924
+ } catch (error) {
925
+ console.error(`[Federation] Error getting parent for ${currentHolon}: ${error.message}`);
926
+ result.parentPropagation.messages.push(`Error getting parent for ${currentHolon}: ${error.message}`);
927
+ result.parentPropagation.errors++;
928
+ break;
789
929
  }
790
- };
930
+ }
791
931
 
792
- await holosphere.put(targetSpace, lens, referenceToStore, null, { autoPropagate: false });
793
- result.success++;
794
- return true;
795
- }
796
- // Otherwise, store a full copy without propagation
797
- else {
798
- const dataToStore = {
799
- ...data,
800
- _federation: {
801
- origin: holon,
802
- lens: lens,
803
- timestamp: Date.now()
804
- }
805
- };
806
- await holosphere.put(targetSpace, lens, dataToStore, null, { autoPropagate: false });
807
- result.success++;
808
- return true;
932
+ if (parentHexagons.length > 0) {
933
+ console.log(`[Federation] Found ${parentHexagons.length} parent hexagons to propagate to: ${parentHexagons.join(', ')}`);
934
+ result.parentPropagation.messages.push(`Found ${parentHexagons.length} parent hexagons to propagate to: ${parentHexagons.join(', ')}`);
935
+
936
+ // Check if data is already a hologram (reuse from federation section)
937
+ const isAlreadyHologram = holosphere.isHologram(data);
938
+ console.log(`[Federation] Data is already hologram: ${isAlreadyHologram}`);
939
+
940
+ // Propagate to each parent hexagon
941
+ const parentPropagatePromises = parentHexagons.map(async (parentHexagon) => {
942
+ try {
943
+ console.log(`[Federation] Propagating to parent hexagon: ${parentHexagon}`);
944
+ let payloadToPut;
945
+ const parentFederationMeta = {
946
+ origin: holon, // The original holon from which this data is being propagated
947
+ sourceLens: lens, // The lens from which this data is being propagated
948
+ propagatedAt: Date.now(),
949
+ originalId: data.id,
950
+ propagationType: 'parent', // Indicate this is parent propagation
951
+ parentLevel: holonResolution - h3.getResolution(parentHexagon) // How many levels up
952
+ };
953
+
954
+ if (useHolograms && !isAlreadyHologram) {
955
+ // Create a new hologram referencing the original data
956
+ const newHologram = holosphere.createHologram(holon, lens, data);
957
+ console.log(`[Federation] Created hologram for parent propagation:`, newHologram);
958
+ payloadToPut = {
959
+ ...newHologram, // This will be { id: data.id, soul: 'path/to/original' }
960
+ _federation: parentFederationMeta
961
+ };
962
+ } else {
963
+ // Propagate existing data (could be a full object or an existing hologram)
964
+ // Make a shallow copy and update/add _federation metadata
965
+ payloadToPut = {
966
+ ...data,
967
+ _federation: {
968
+ ...(data._federation || {}), // Preserve existing _federation fields if any
969
+ ...parentFederationMeta // Add/overwrite with current propagation info
970
+ }
971
+ };
972
+ }
973
+
974
+ console.log(`[Federation] Storing in parent hexagon ${parentHexagon} with payload:`, payloadToPut);
975
+
976
+ // Store in the parent hexagon with redirection disabled and no further auto-propagation
977
+ await holosphere.put(parentHexagon, lens, payloadToPut, null, {
978
+ disableHologramRedirection: true,
979
+ autoPropagate: false
980
+ });
981
+
982
+ console.log(`[Federation] Successfully propagated to parent hexagon: ${parentHexagon}`);
983
+ result.parentPropagation.success++;
984
+ return true;
985
+ } catch (error) {
986
+ console.error(`[Federation] Error propagating ${data.id} to parent hexagon ${parentHexagon}: ${error.message}`);
987
+ result.parentPropagation.errors++;
988
+ result.parentPropagation.messages.push(`Error propagating ${data.id} to parent hexagon ${parentHexagon}: ${error.message}`);
989
+ return false;
990
+ }
991
+ });
992
+
993
+ await Promise.all(parentPropagatePromises);
994
+ } else {
995
+ console.log(`[Federation] No parent hexagons found for ${holon} (already at resolution 0 or max levels reached)`);
996
+ result.parentPropagation.messages.push(`No parent hexagons found for ${holon} (already at resolution 0 or max levels reached)`);
997
+ result.parentPropagation.skipped++;
998
+ }
809
999
  }
810
1000
  } catch (error) {
811
- result.errors++;
812
- result.errorDetails.push({
813
- space: targetSpace,
814
- error: error.message
815
- });
816
- return false;
1001
+ console.error(`[Federation] Error during parent propagation: ${error.message}`);
1002
+ result.parentPropagation.errors++;
1003
+ result.parentPropagation.messages.push(`Error during parent propagation: ${error.message}`);
817
1004
  }
818
- });
1005
+ } else {
1006
+ console.log(`[Federation] Parent propagation disabled for holon: ${holon}`);
1007
+ }
819
1008
 
820
- await Promise.all(propagatePromises);
1009
+ // ================================ END PARENT PROPAGATION ================================
821
1010
 
822
- result.propagated = result.success > 0;
1011
+ result.propagated = result.success > 0 || result.parentPropagation.success > 0;
823
1012
  return result;
824
1013
  } catch (error) {
825
- console.error('Error in propagate:', error);
826
1014
  return {
827
1015
  ...result,
828
1016
  error: error.message
@@ -894,7 +1082,6 @@ export async function updateFederatedMessages(holosphere, originalChatId, messag
894
1082
  try {
895
1083
  await updateCallback(msg.chatId, msg.messageId);
896
1084
  } catch (error) {
897
- console.warn(`Failed to update federated message in chat ${msg.chatId}:`, error);
898
1085
  }
899
1086
  }
900
1087
  }
@@ -981,13 +1168,11 @@ export async function resetFederation(holosphere, spaceId, password = null, opti
981
1168
 
982
1169
  // Save partner's updated federation info
983
1170
  await holosphere.putGlobal('federation', partnerFedInfo);
984
- console.log(`Updated federation info for partner ${partnerSpace}`);
985
1171
  result.partnersNotified++;
986
1172
  return true;
987
1173
  }
988
1174
  return false;
989
1175
  } catch (error) {
990
- console.warn(`Could not update federation info for partner ${partnerSpace}: ${error.message}`);
991
1176
  result.errors.push({
992
1177
  partner: partnerSpace,
993
1178
  error: error.message
@@ -1013,10 +1198,8 @@ export async function resetFederation(holosphere, spaceId, password = null, opti
1013
1198
  meta.status = 'inactive';
1014
1199
  meta.endedAt = Date.now();
1015
1200
  await holosphere.putGlobal('federationMeta', meta);
1016
- console.log(`Updated federation metadata for ${spaceId} and ${partnerSpace}`);
1017
1201
  }
1018
1202
  } catch (error) {
1019
- console.warn(`Could not update federation metadata for ${partnerSpace}: ${error.message}`);
1020
1203
  }
1021
1204
  }
1022
1205
  }
@@ -1024,7 +1207,6 @@ export async function resetFederation(holosphere, spaceId, password = null, opti
1024
1207
  result.success = true;
1025
1208
  return result;
1026
1209
  } catch (error) {
1027
- console.error(`Federation reset failed: ${error.message}`);
1028
1210
  return {
1029
1211
  ...result,
1030
1212
  success: false,