holosphere 1.1.9 → 1.1.11

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
@@ -14,10 +14,12 @@
14
14
  * @param {string} [password1] - Optional password for the first space
15
15
  * @param {string} [password2] - Optional password for the second space
16
16
  * @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications (default: true)
17
+ * @param {object} [lensConfig] - Optional lens-specific configuration
18
+ * @param {string[]} [lensConfig.federate] - List of lenses to federate (default: all)
19
+ * @param {string[]} [lensConfig.notify] - List of lenses to notify (default: all)
17
20
  * @returns {Promise<boolean>} - True if federation was created successfully
18
21
  */
19
- export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true) {
20
- console.log('FEDERATING', spaceId1, spaceId2, password1, password2, bidirectional)
22
+ export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true, lensConfig = {}) {
21
23
  if (!spaceId1 || !spaceId2) {
22
24
  throw new Error('federate: Missing required space IDs');
23
25
  }
@@ -27,40 +29,59 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
27
29
  throw new Error('Cannot federate a space with itself');
28
30
  }
29
31
 
32
+ // Validate lens configuration
33
+ const { federate = [], notify = [] } = lensConfig;
34
+ if (!Array.isArray(federate) || !Array.isArray(notify)) {
35
+ throw new Error('federate: lensConfig.federate and lensConfig.notify must be arrays');
36
+ }
37
+
38
+ // Use the provided lens configurations directly
39
+ const federateLenses = federate;
40
+ const notifyLenses = notify;
41
+
30
42
  try {
31
43
  // Get or create federation info for first space (A)
32
- let fedInfo1 = null;
44
+ let fedInfo1 = null;
33
45
 
34
46
  try {
35
47
  fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
36
48
  } 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
49
  }
50
+
41
51
  if (fedInfo1 == null) {
42
52
  fedInfo1 = {
43
53
  id: spaceId1,
44
54
  name: spaceId1,
45
55
  federation: [],
46
56
  notify: [],
57
+ lensConfig: {}, // New field for lens-specific settings
47
58
  timestamp: Date.now()
48
59
  };
49
60
  }
50
-
51
61
 
52
- // Ensure arrays exist
62
+ // Ensure arrays and lensConfig exist
53
63
  if (!fedInfo1.federation) fedInfo1.federation = [];
54
64
  if (!fedInfo1.notify) fedInfo1.notify = [];
65
+ if (!fedInfo1.lensConfig) fedInfo1.lensConfig = {};
55
66
 
56
- // Add space2 to space1's federation and notify lists if not already present
67
+ // Add space2 to space1's federation list if not already present
57
68
  if (!fedInfo1.federation.includes(spaceId2)) {
58
69
  fedInfo1.federation.push(spaceId2);
59
70
  }
60
- // // Always add to notify list for the first space (primary direction)
61
- // if (!fedInfo1.notify.includes(spaceId2)) {
62
- // fedInfo1.notify.push(spaceId2);
63
- // }
71
+
72
+ // Add space2 to space1's notify list if not already present
73
+ if (!fedInfo1.notify.includes(spaceId2)) {
74
+ fedInfo1.notify.push(spaceId2);
75
+ }
76
+
77
+ // Store lens configuration for space2
78
+ const newLensConfigsForSpace1 = { ...(fedInfo1.lensConfig || {}) }; // Shallow copy existing lensConfigs for space1
79
+ newLensConfigsForSpace1[spaceId2] = { // Add/update config for the target spaceId2
80
+ federate: [...federateLenses], // federateLenses & notifyLenses are from the main lensConfig parameter
81
+ notify: [...notifyLenses],
82
+ timestamp: Date.now()
83
+ };
84
+ fedInfo1.lensConfig = newLensConfigsForSpace1; // Assign the new/modified object back to fedInfo1.lensConfig
64
85
 
65
86
  // Update timestamp
66
87
  fedInfo1.timestamp = Date.now();
@@ -68,42 +89,50 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
68
89
  // Save updated federation info for space1
69
90
  try {
70
91
  await holosphere.putGlobal('federation', fedInfo1, password1);
71
- console.log(`Updated federation info for ${spaceId1}`);
72
92
  } catch (error) {
73
- console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
74
93
  throw new Error(`Failed to create federation: ${error.message}`);
75
94
  }
76
-
95
+
77
96
  // If bidirectional is true, handle space2 (B) as well
78
- //if (bidirectional && password2) {
79
97
  {
80
98
  let fedInfo2 = null;
81
99
  try {
82
100
  fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
83
101
  } 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
102
  }
103
+
88
104
  if (fedInfo2 == null) {
89
105
  fedInfo2 = {
90
106
  id: spaceId2,
91
107
  name: spaceId2,
92
108
  federation: [],
93
109
  notify: [],
110
+ lensConfig: {}, // New field for lens-specific settings
94
111
  timestamp: Date.now()
95
112
  };
96
113
  }
97
-
98
- // Add nEnsure arrays exist
99
-
114
+
115
+ // Ensure arrays and lensConfig exist
116
+ if (!fedInfo2.federation) fedInfo2.federation = [];
100
117
  if (!fedInfo2.notify) fedInfo2.notify = [];
118
+ if (!fedInfo2.lensConfig) fedInfo2.lensConfig = {};
101
119
 
102
- // Add space1 to space2's federation list if not already present
120
+ // Add space1 to space2's federation list if bidirectional
121
+ if (bidirectional && !fedInfo2.federation.includes(spaceId1)) {
122
+ fedInfo2.federation.push(spaceId1);
123
+ }
124
+
125
+ // Add space1 to space2's notify list if not already present
103
126
  if (!fedInfo2.notify.includes(spaceId1)) {
104
127
  fedInfo2.notify.push(spaceId1);
105
128
  }
106
-
129
+
130
+ // Store lens configuration for space1
131
+ fedInfo2.lensConfig[spaceId1] = {
132
+ federate: bidirectional ? [...federateLenses] : [], // Create a copy of the array
133
+ notify: bidirectional ? [...notifyLenses] : [], // Create a copy of the array
134
+ timestamp: Date.now()
135
+ };
107
136
 
108
137
  // Update timestamp
109
138
  fedInfo2.timestamp = Date.now();
@@ -111,10 +140,7 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
111
140
  // Save updated federation info for space2
112
141
  try {
113
142
  await holosphere.putGlobal('federation', fedInfo2, password2);
114
- console.log(`Updated federation info for ${spaceId2}`);
115
143
  } 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
144
  }
119
145
  }
120
146
 
@@ -125,19 +151,20 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
125
151
  space2: spaceId2,
126
152
  created: Date.now(),
127
153
  status: 'active',
128
- bidirectional: bidirectional
154
+ bidirectional: bidirectional,
155
+ lensConfig: {
156
+ federate: [...federateLenses], // Create a copy of the array
157
+ notify: [...notifyLenses] // Create a copy of the array
158
+ }
129
159
  };
130
-
160
+
131
161
  try {
132
162
  await holosphere.putGlobal('federationMeta', federationMeta);
133
- console.log(`Created federation metadata for ${spaceId1} and ${spaceId2}`);
134
163
  } catch (error) {
135
- console.warn(`Could not create federation metadata: ${error.message}`);
136
164
  }
137
165
 
138
166
  return true;
139
167
  } catch (error) {
140
- console.error(`Federation creation failed: ${error.message}`);
141
168
  throw error;
142
169
  }
143
170
  }
@@ -203,7 +230,6 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
203
230
  // Execute callback with the data
204
231
  await callback(data, federatedSpace, lens);
205
232
  } catch (error) {
206
- console.warn('Federation notification error:', error);
207
233
  }
208
234
  });
209
235
 
@@ -211,7 +237,6 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
211
237
  subscriptions.push(sub);
212
238
  }
213
239
  } catch (error) {
214
- console.warn(`Error creating subscription for ${federatedSpace}/${lens}:`, error);
215
240
  }
216
241
  }
217
242
  }
@@ -226,7 +251,6 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
226
251
  sub.unsubscribe();
227
252
  }
228
253
  } catch (error) {
229
- console.warn('Error unsubscribing:', error);
230
254
  }
231
255
  });
232
256
  // Clear the subscriptions array
@@ -252,6 +276,35 @@ export async function getFederation(holosphere, spaceId, password = null) {
252
276
  return await holosphere.getGlobal('federation', spaceId, password);
253
277
  }
254
278
 
279
+ /**
280
+ * Retrieves the lens-specific configuration for a federation link between two spaces.
281
+ * @param {object} holosphere - The HoloSphere instance
282
+ * @param {string} spaceId - The ID of the source space.
283
+ * @param {string} targetSpaceId - The ID of the target space in the federation link.
284
+ * @param {string} [password] - Optional password for the source space.
285
+ * @returns {Promise<object|null>} - An object with 'federate' and 'notify' arrays, or null if not found.
286
+ */
287
+ export async function getFederatedConfig(holosphere, spaceId, targetSpaceId, password = null) {
288
+ if (!holosphere || !spaceId || !targetSpaceId) {
289
+ throw new Error('getFederatedConfig: Missing required parameters');
290
+ }
291
+
292
+ try {
293
+ const fedInfo = await getFederation(holosphere, spaceId, password);
294
+
295
+ if (fedInfo && fedInfo.lensConfig && fedInfo.lensConfig[targetSpaceId]) {
296
+ return {
297
+ federate: fedInfo.lensConfig[targetSpaceId].federate || [],
298
+ notify: fedInfo.lensConfig[targetSpaceId].notify || []
299
+ };
300
+ }
301
+ return null; // Or return an empty config: { federate: [], notify: [] }
302
+ } catch (error) {
303
+ console.error(`Error getting federated config for ${spaceId} -> ${targetSpaceId}: ${error.message}`);
304
+ throw error;
305
+ }
306
+ }
307
+
255
308
  /**
256
309
  * Removes a federation relationship between spaces
257
310
  * @param {object} holosphere - The HoloSphere instance
@@ -272,12 +325,14 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
272
325
  try {
273
326
  fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
274
327
  } catch (error) {
275
- console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
328
+ console.error(`Error getting fedInfo1 for ${spaceId1} during unfederate: ${error.message}`);
329
+ // If we can't get fedInfo1, we can't modify it. Decide if this is a critical failure.
330
+ // For now, we'll let it proceed to attempt metadata cleanup, but a throw here might be valid.
276
331
  }
277
332
 
278
333
  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 or its federation array doesn't exist, log and proceed to metadata cleanup.
335
+ console.warn(`No federation array found for ${spaceId1} or fedInfo1 is null. Skipping its update.`);
281
336
  } else {
282
337
  // Update first space federation info
283
338
  fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
@@ -285,30 +340,34 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
285
340
 
286
341
  try {
287
342
  await holosphere.putGlobal('federation', fedInfo1, password1);
288
- console.log(`Updated federation info for ${spaceId1}`);
289
343
  } catch (error) {
290
- console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
344
+ console.error(`Failed to update fedInfo1 for ${spaceId1} during unfederate: ${error.message}`);
345
+ throw error; // RE-THROW to signal failure
291
346
  }
292
347
  }
293
348
 
294
- // Update second space federation info if password provided
295
- if (password2) {
349
+ // Update second space federation info (remove spaceId1 from spaceId2's notify list)
350
+ // This part is usually for full bidirectional unfederation cleanup.
351
+ // The original code only did this if password2 was provided.
352
+ if (password2) { // Retaining original condition for this block
296
353
  let fedInfo2 = null;
297
354
  try {
298
355
  fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
299
356
  } catch (error) {
300
- console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
357
+ console.error(`Error getting fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
301
358
  }
302
359
 
303
- if (fedInfo2 && fedInfo2.notify) {
360
+ if (!fedInfo2 || !fedInfo2.notify) {
361
+ console.warn(`No notify array found for ${spaceId2} or fedInfo2 is null. Skipping its update.`);
362
+ } else {
304
363
  fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
305
364
  fedInfo2.timestamp = Date.now();
306
365
 
307
366
  try {
308
367
  await holosphere.putGlobal('federation', fedInfo2, password2);
309
- console.log(`Updated federation info for ${spaceId2}`);
310
368
  } catch (error) {
311
- console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
369
+ console.error(`Failed to update fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
370
+ throw error; // RE-THROW to signal failure
312
371
  }
313
372
  }
314
373
  }
@@ -324,17 +383,17 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
324
383
  if (meta) {
325
384
  meta.status = 'inactive';
326
385
  meta.endedAt = Date.now();
327
- await holosphere.putGlobal('federationMeta', meta);
328
- console.log(`Updated federation metadata for ${spaceId1} and ${spaceId2}`);
386
+ await holosphere.putGlobal('federationMeta', meta); // Not re-throwing here as it's metadata cleanup
329
387
  }
330
388
  } catch (error) {
331
- console.warn(`Could not update federation metadata: ${error.message}`);
389
+ console.warn(`Failed to update federationMeta during unfederate: ${error.message}`);
332
390
  }
333
391
 
334
392
  return true;
335
393
  } catch (error) {
336
- console.error(`Federation removal failed: ${error.message}`);
337
- throw error;
394
+ // This will catch errors re-thrown from putGlobal or from getGlobal if they occur before specific catches.
395
+ console.error(`Critical error during unfederate operation for ${spaceId1}-${spaceId2}: ${error.message}`);
396
+ throw error; // Ensure the main operation failure is propagated
338
397
  }
339
398
  }
340
399
 
@@ -373,42 +432,42 @@ export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = n
373
432
 
374
433
  // Save updated federation info
375
434
  await holosphere.putGlobal('federation', fedInfo, password1);
376
- console.log(`Removed ${spaceId2} from ${spaceId1}'s notify list`);
377
435
  return true;
378
436
  } else {
379
- console.log(`${spaceId2} not found in ${spaceId1}'s notify list`);
380
437
  return false;
381
438
  }
382
439
  } catch (error) {
383
- console.error(`Remove notification failed: ${error.message}`);
384
440
  throw error;
385
441
  }
386
442
  }
387
443
 
388
444
  /**
389
- * Get and combine data from local and federated sources
445
+ * Get and combine data from local and federated sources.
446
+ * If `options.queryIds` is provided, fetches only those specific IDs using `get()`.
447
+ * Otherwise, falls back to fetching all data using `getAll()` (potentially inefficient).
448
+ *
390
449
  * @param {HoloSphere} holosphere The HoloSphere instance
391
- * @param {string} holon The local holon name
450
+ * @param {string} holon The local holon name (used as the space ID for federation info)
392
451
  * @param {string} lens The lens to query
393
452
  * @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)
453
+ * @param {string[]} [options.queryIds] Optional array of specific item IDs to fetch.
454
+ * @param {boolean} [options.aggregate=false] Whether to aggregate results by ID
455
+ * @param {string} [options.idField='id'] The field to use as ID
456
+ * @param {string[]} [options.sumFields=[]] Fields to sum during aggregation
457
+ * @param {string[]} [options.concatArrays=[]] Array fields to concatenate during aggregation
458
+ * @param {boolean} [options.removeDuplicates=true] Whether to remove duplicates in concatenated arrays
459
+ * @param {Function} [options.mergeStrategy=null] Custom merge function for aggregation
460
+ * @param {boolean} [options.includeLocal=true] Whether to include local data
461
+ * @param {boolean} [options.includeFederated=true] Whether to include federated data
462
+ * @param {boolean} [options.resolveReferences=true] Whether to resolve federation references
463
+ * @param {number} [options.maxFederatedSpaces=-1] Maximum number of federated spaces to query (-1 for all)
464
+ * @param {number} [options.timeout=10000] Timeout in milliseconds for federated queries
405
465
  * @returns {Promise<Array>} Combined array of local and federated data
406
466
  */
407
467
  export async function getFederated(holosphere, holon, lens, options = {}) {
408
- console.log(`getFederated called with options:`, JSON.stringify(options));
409
-
410
- // Set default options
468
+ // Set default options and extract queryIds
411
469
  const {
470
+ queryIds = null, // New option
412
471
  aggregate = false,
413
472
  idField = 'id',
414
473
  sumFields = [],
@@ -417,91 +476,85 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
417
476
  mergeStrategy = null,
418
477
  includeLocal = true,
419
478
  includeFederated = true,
420
- resolveReferences = true, // Default to true
479
+ resolveReferences = true,
421
480
  maxFederatedSpaces = -1,
422
481
  timeout = 10000
423
482
  } = options;
424
483
 
425
484
  console.log(`resolveReferences option: ${resolveReferences}`);
426
-
485
+ console.log(`Querying specific IDs:`, queryIds ? queryIds.join(', ') : 'No (fetching all)');
486
+
427
487
  // Validate required parameters
428
488
  if (!holosphere || !holon || !lens) {
429
489
  throw new Error('Missing required parameters: holosphere, holon, and lens are required');
430
490
  }
431
491
 
432
- // Get federation info for current space
433
- // Use holon as the space ID
492
+ // Get federation info for current space (using holon as spaceId)
434
493
  const spaceId = holon;
435
494
  const fedInfo = await getFederation(holosphere, spaceId);
436
495
 
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
496
+ // Initialize result array and track processed IDs to avoid duplicates/redundant fetches
497
+ const fetchedItems = new Map(); // Use Map to store fetched items by ID
498
+ const processedIds = new Set(); // Track IDs added to the final result
499
+
500
+ const fetchPromises = [];
501
+
502
+ // Determine list of spaces to query (local + federated)
503
+ let spacesToQuery = [];
504
+ if (includeLocal) {
505
+ spacesToQuery.push(holon); // Add local holon first
506
+ }
445
507
  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
508
  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
- }
509
+ spacesToQuery = spacesToQuery.concat(federatedSpaces);
484
510
  }
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`);
511
+
512
+ // Fetch data from all relevant spaces
513
+ for (const currentSpace of spacesToQuery) {
514
+ if (queryIds && Array.isArray(queryIds)) {
515
+ // --- Fetch specific IDs using holosphere.get ---
516
+ console.log(`Fetching specific IDs from ${currentSpace}: ${queryIds.join(', ')}`);
517
+ for (const itemId of queryIds) {
518
+ if (fetchedItems.has(itemId)) continue; // Skip if already fetched
519
+ fetchPromises.push(
520
+ holosphere.get(currentSpace, lens, itemId)
521
+ .then(item => {
522
+ if (item) {
523
+ fetchedItems.set(itemId, item);
524
+ }
525
+ })
526
+ .catch(err => console.warn(`Error fetching item ${itemId} from ${currentSpace}: ${err.message}`))
527
+ );
498
528
  }
529
+ } else {
530
+ // --- Fetch all data using holosphere.getAll (Fallback - inefficient) ---
531
+ if(currentSpace === holon && includeLocal) { // Only warn once for local
532
+ console.warn(`getFederated: No queryIds provided. Falling back to fetching ALL items from ${currentSpace} using getAll. This can be inefficient.`);
533
+ }
534
+ console.log(`Fetching ALL items from ${currentSpace}`);
535
+ fetchPromises.push(
536
+ holosphere.getAll(currentSpace, lens)
537
+ .then(items => {
538
+ for (const item of items) {
539
+ if (item && item[idField] && !fetchedItems.has(item[idField])) {
540
+ fetchedItems.set(item[idField], item);
541
+ }
542
+ }
543
+ })
544
+ .catch(err => console.warn(`Error fetching all items from ${currentSpace}: ${err.message}`))
545
+ );
499
546
  }
500
547
  }
501
-
548
+
549
+ // Wait for all fetches to complete
550
+ await Promise.all(fetchPromises);
551
+
552
+ // Convert Map values to array for processing
553
+ const result = Array.from(fetchedItems.values());
554
+
502
555
  // Now resolve references if needed
503
- if (resolveReferences) {
504
- console.log(`Resolving references for ${result.length} items`);
556
+ if (resolveReferences && result.length > 0) {
557
+ console.log(`Resolving references for ${result.length} fetched items`);
505
558
 
506
559
  for (let i = 0; i < result.length; i++) {
507
560
  const item = result[i];
@@ -520,13 +573,13 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
520
573
 
521
574
  console.log(`Extracting from soul - holon: ${originHolon}, lens: ${originLens}, key: ${originKey}`);
522
575
 
523
- // Get original data using the extracted path
576
+ // Get original data using the extracted path - always resolve references
524
577
  const originalData = await holosphere.get(
525
578
  originHolon,
526
579
  originLens,
527
580
  originKey,
528
581
  null,
529
- { resolveReferences: false } // Prevent infinite recursion
582
+ { resolveReferences: true } // Always resolve nested references
530
583
  );
531
584
 
532
585
  console.log(`Original data found via soul path:`, JSON.stringify(originalData));
@@ -542,15 +595,45 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
542
595
  timestamp: Date.now()
543
596
  }
544
597
  };
545
- console.log(`Reference resolved successfully via soul path, processed item:`, JSON.stringify(result[i]));
546
598
  } else {
547
- console.warn(`Could not resolve reference: original data not found at extracted path`);
599
+ // Instead of leaving the original reference, create an error object
600
+ result[i] = {
601
+ id: item.id,
602
+ _federation: {
603
+ isReference: true,
604
+ resolved: false,
605
+ soul: item.soul,
606
+ error: 'Referenced data not found',
607
+ timestamp: Date.now()
608
+ }
609
+ };
548
610
  }
549
611
  } else {
550
612
  console.warn(`Soul doesn't match expected format: ${item.soul}`);
613
+ // Instead of leaving the original reference, create an error object
614
+ result[i] = {
615
+ id: item.id,
616
+ _federation: {
617
+ isReference: true,
618
+ resolved: false,
619
+ soul: item.soul,
620
+ error: 'Invalid soul format',
621
+ timestamp: Date.now()
622
+ }
623
+ };
551
624
  }
552
625
  } catch (refError) {
553
- console.warn(`Error resolving reference by soul in getFederated: ${refError.message}`);
626
+ // Instead of leaving the original reference, create an error object
627
+ result[i] = {
628
+ id: item.id,
629
+ _federation: {
630
+ isReference: true,
631
+ resolved: false,
632
+ soul: item.soul,
633
+ error: refError.message || 'Error resolving reference',
634
+ timestamp: Date.now()
635
+ }
636
+ };
554
637
  }
555
638
  }
556
639
  // For backward compatibility, check for old-style references
@@ -582,12 +665,10 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
582
665
  timestamp: Date.now()
583
666
  }
584
667
  };
585
- console.log(`Legacy reference resolved successfully, processed item:`, JSON.stringify(result[i]));
586
668
  } else {
587
669
  console.warn(`Could not resolve legacy reference: original data not found`);
588
670
  }
589
671
  } catch (refError) {
590
- console.warn(`Error resolving legacy reference in getFederated: ${refError.message}`);
591
672
  }
592
673
  }
593
674
  }
@@ -659,7 +740,7 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
659
740
  * @param {string} lens - The lens identifier
660
741
  * @param {object} data - The data to propagate
661
742
  * @param {object} [options] - Propagation options
662
- * @param {boolean} [options.useReferences=true] - Whether to use references instead of duplicating data
743
+ * @param {boolean} [options.useHolograms=true] - Use holograms for propagation (default: true)
663
744
  * @param {string[]} [options.targetSpaces] - Specific target spaces to propagate to (defaults to all federated spaces)
664
745
  * @param {string} [options.password] - Password for accessing the source holon (if needed)
665
746
  * @returns {Promise<object>} - Result with success count and errors
@@ -669,18 +750,13 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
669
750
  throw new Error('propagate: Missing required parameters');
670
751
  }
671
752
  // Default propagation options
672
- const {
673
- useReferences = true,
674
- targetSpaces = null,
675
- password = null
676
- } = options;
753
+ const { useHolograms = true, targetSpaces = null, password = null } = options;
677
754
 
678
755
  const result = {
679
756
  success: 0,
680
757
  errors: 0,
681
- errorDetails: [],
682
- propagated: false,
683
- referencesUsed: useReferences
758
+ skipped: 0,
759
+ messages: []
684
760
  };
685
761
 
686
762
  try {
@@ -717,56 +793,97 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
717
793
  message: 'No valid target spaces found after filtering'
718
794
  };
719
795
  }
796
+
797
+ // Filter spaces based on lens configuration
798
+ spaces = spaces.filter(targetSpace => {
799
+ const spaceConfig = fedInfo.lensConfig?.[targetSpace];
800
+ if (!spaceConfig) {
801
+ result.messages.push(`No lens configuration for target space ${targetSpace}. Skipping propagation of lens '${lens}'.`);
802
+ result.skipped++;
803
+ return false;
804
+ }
805
+
806
+ // Ensure .federate is an array before calling .includes
807
+ const federateLenses = Array.isArray(spaceConfig.federate) ? spaceConfig.federate : [];
808
+
809
+ const shouldFederate = federateLenses.includes('*') || federateLenses.includes(lens);
810
+
811
+ // Propagation now only depends on the 'federate' list configuration for the lens
812
+ const shouldPropagate = shouldFederate;
813
+
814
+ if (!shouldPropagate) {
815
+ result.messages.push(`Propagation of lens '${lens}' to target space ${targetSpace} skipped: lens not in 'federate' configuration.`);
816
+ result.skipped++;
817
+ }
818
+
819
+ return shouldPropagate;
820
+ });
821
+
822
+ if (spaces.length === 0) {
823
+ // If no specific skip messages were added and spaces is empty, add a general one.
824
+ if (result.skipped === 0 && fedInfo.notify && fedInfo.notify.length > 0) {
825
+ result.messages.push('No target spaces were configured for federation of this specific lens, out of available notification partners.');
826
+ } else if (fedInfo.notify && fedInfo.notify.length === 0) {
827
+ result.messages.push('No notification partners available to filter for lens propagation.');
828
+ }
829
+ return {
830
+ ...result,
831
+ message: result.messages.join('; ') || 'No valid target spaces for propagation after lens filtering.'
832
+ };
833
+ }
834
+
835
+ // Check if data is already a hologram
836
+ const isAlreadyHologram = holosphere.isHologram(data);
837
+
838
+ // If data is already a hologram, don't re-wrap it
839
+ if (isAlreadyHologram && useHolograms) {
840
+ }
841
+
842
+ // If propagating a non-hologram and useHolograms is false, add warning
843
+ if (!isAlreadyHologram && !useHolograms) {
844
+ }
720
845
 
721
846
  // For each target space, propagate the data
722
847
  const propagatePromises = spaces.map(async (targetSpace) => {
723
848
  try {
724
- // Get federation info for target space using getFederation
725
- const targetFedInfo = await getFederation(holosphere, targetSpace);
726
-
727
- // If using references, create a soul reference instead of duplicating the data
728
- if (useReferences) {
729
- // Create a soul path that points to the original data
730
- const soul = `${holosphere.appname}/${holon}/${lens}/${data.id}`;
731
-
732
- // Create a minimal reference object with just id and soul
733
- const reference = {
734
- id: data.id,
735
- soul: soul,
736
- _federation: {
737
- origin: holon,
738
- lens: lens,
739
- timestamp: Date.now()
740
- }
849
+ let payloadToPut;
850
+ const federationMeta = {
851
+ origin: holon, // The space from which this data is being propagated
852
+ sourceLens: lens, // The lens from which this data is being propagated
853
+ propagatedAt: Date.now(),
854
+ originalId: data.id
855
+ };
856
+
857
+ if (useHolograms && !isAlreadyHologram) {
858
+ // Create a new hologram referencing the original data
859
+ const newHologram = holosphere.createHologram(holon, lens, data);
860
+ payloadToPut = {
861
+ ...newHologram, // This will be { id: data.id, soul: 'path/to/original' }
862
+ _federation: federationMeta
741
863
  };
742
-
743
- console.log(`Using soul reference: ${soul} for data: ${data.id}`);
744
-
745
- // Store the reference in the target space without propagation
746
- await holosphere.put(targetSpace, lens, reference, null, { autoPropagate: false });
747
-
748
- result.success++;
749
- return true;
750
864
  } else {
751
- // If not using references, store a full copy without propagation
752
- const dataToStore = {
753
- ...data,
865
+ // Propagate existing data (could be a full object or an existing hologram)
866
+ // Make a shallow copy and update/add _federation metadata
867
+ payloadToPut = {
868
+ ...data,
754
869
  _federation: {
755
- origin: holon,
756
- lens: lens,
757
- timestamp: Date.now()
870
+ ...(data._federation || {}), // Preserve existing _federation fields if any
871
+ ...federationMeta // Add/overwrite with current propagation info
758
872
  }
759
873
  };
760
- await holosphere.put(targetSpace, lens, dataToStore, null, { autoPropagate: false });
761
- result.success++;
762
- return true;
763
874
  }
875
+
876
+ // Store in the target space with redirection disabled and no further auto-propagation
877
+ await holosphere.put(targetSpace, lens, payloadToPut, null, {
878
+ disableHologramRedirection: true,
879
+ autoPropagate: false
880
+ });
881
+
882
+ result.success++;
883
+ return true;
764
884
  } catch (error) {
765
885
  result.errors++;
766
- result.errorDetails.push({
767
- space: targetSpace,
768
- error: error.message
769
- });
886
+ result.messages.push(`Error propagating ${data.id} to ${targetSpace}: ${error.message}`);
770
887
  return false;
771
888
  }
772
889
  });
@@ -776,7 +893,6 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
776
893
  result.propagated = result.success > 0;
777
894
  return result;
778
895
  } catch (error) {
779
- console.error('Error in propagate:', error);
780
896
  return {
781
897
  ...result,
782
898
  error: error.message
@@ -848,7 +964,6 @@ export async function updateFederatedMessages(holosphere, originalChatId, messag
848
964
  try {
849
965
  await updateCallback(msg.chatId, msg.messageId);
850
966
  } catch (error) {
851
- console.warn(`Failed to update federated message in chat ${msg.chatId}:`, error);
852
967
  }
853
968
  }
854
969
  }
@@ -935,13 +1050,11 @@ export async function resetFederation(holosphere, spaceId, password = null, opti
935
1050
 
936
1051
  // Save partner's updated federation info
937
1052
  await holosphere.putGlobal('federation', partnerFedInfo);
938
- console.log(`Updated federation info for partner ${partnerSpace}`);
939
1053
  result.partnersNotified++;
940
1054
  return true;
941
1055
  }
942
1056
  return false;
943
1057
  } catch (error) {
944
- console.warn(`Could not update federation info for partner ${partnerSpace}: ${error.message}`);
945
1058
  result.errors.push({
946
1059
  partner: partnerSpace,
947
1060
  error: error.message
@@ -967,10 +1080,8 @@ export async function resetFederation(holosphere, spaceId, password = null, opti
967
1080
  meta.status = 'inactive';
968
1081
  meta.endedAt = Date.now();
969
1082
  await holosphere.putGlobal('federationMeta', meta);
970
- console.log(`Updated federation metadata for ${spaceId} and ${partnerSpace}`);
971
1083
  }
972
1084
  } catch (error) {
973
- console.warn(`Could not update federation metadata for ${partnerSpace}: ${error.message}`);
974
1085
  }
975
1086
  }
976
1087
  }
@@ -978,7 +1089,6 @@ export async function resetFederation(holosphere, spaceId, password = null, opti
978
1089
  result.success = true;
979
1090
  return result;
980
1091
  } catch (error) {
981
- console.error(`Federation reset failed: ${error.message}`);
982
1092
  return {
983
1093
  ...result,
984
1094
  success: false,