holosphere 1.1.6 → 1.1.8

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,16 +3,21 @@
3
3
  * Provides methods for creating, managing, and using federated spaces
4
4
  */
5
5
 
6
+
6
7
  /**
7
8
  * Creates a federation relationship between two spaces
9
+ * Federation is bidirectional by default, and data propagation uses soul references by default.
10
+ *
8
11
  * @param {object} holosphere - The HoloSphere instance
9
12
  * @param {string} spaceId1 - The first space ID
10
13
  * @param {string} spaceId2 - The second space ID
11
14
  * @param {string} [password1] - Optional password for the first space
12
15
  * @param {string} [password2] - Optional password for the second space
16
+ * @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications (default: true)
13
17
  * @returns {Promise<boolean>} - True if federation was created successfully
14
18
  */
15
- export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) {
19
+ export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true) {
20
+ console.log('FEDERATING', spaceId1, spaceId2, password1, password2, bidirectional)
16
21
  if (!spaceId1 || !spaceId2) {
17
22
  throw new Error('federate: Missing required space IDs');
18
23
  }
@@ -22,120 +27,18 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
22
27
  throw new Error('Cannot federate a space with itself');
23
28
  }
24
29
 
25
- // Verify access to both spaces before proceeding
26
- let canAccessSpace1 = false;
27
- let canAccessSpace2 = false;
28
-
29
30
  try {
30
- // Test authentication for first space
31
- if (password1) {
32
- try {
33
- await new Promise((resolve, reject) => {
34
- const user = holosphere.gun.user();
35
- user.auth(holosphere.userName(spaceId1), password1, (ack) => {
36
- if (ack.err) {
37
- console.warn(`Authentication test for ${spaceId1} failed: ${ack.err}`);
38
- reject(new Error(`Authentication failed for ${spaceId1}: ${ack.err}`));
39
- } else {
40
- canAccessSpace1 = true;
41
- resolve();
42
- }
43
- });
44
- });
45
- } catch (error) {
46
- // Try to create the user if authentication fails
47
- try {
48
- await new Promise((resolve, reject) => {
49
- const user = holosphere.gun.user();
50
- user.create(holosphere.userName(spaceId1), password1, (ack) => {
51
- if (ack.err && !ack.err.includes('already created')) {
52
- reject(new Error(`User creation failed for ${spaceId1}: ${ack.err}`));
53
- } else {
54
- // Try to authenticate again
55
- user.auth(holosphere.userName(spaceId1), password1, (authAck) => {
56
- if (authAck.err) {
57
- reject(new Error(`Authentication failed after creation for ${spaceId1}: ${authAck.err}`));
58
- } else {
59
- canAccessSpace1 = true;
60
- resolve();
61
- }
62
- });
63
- }
64
- });
65
- });
66
- } catch (createError) {
67
- console.warn(`Could not create or authenticate user for ${spaceId1}: ${createError.message}`);
68
- // Continue with limited functionality
69
- }
70
- }
71
- } else {
72
- // No password required, assume we can access
73
- canAccessSpace1 = true;
74
- }
75
-
76
- // Test authentication for second space if password provided
77
- if (password2) {
78
- try {
79
- await new Promise((resolve, reject) => {
80
- const user = holosphere.gun.user();
81
- user.auth(holosphere.userName(spaceId2), password2, (ack) => {
82
- if (ack.err) {
83
- console.warn(`Authentication test for ${spaceId2} failed: ${ack.err}`);
84
- reject(new Error(`Authentication failed for ${spaceId2}: ${ack.err}`));
85
- } else {
86
- canAccessSpace2 = true;
87
- resolve();
88
- }
89
- });
90
- });
91
- } catch (error) {
92
- // Try to create the user if authentication fails
93
- try {
94
- await new Promise((resolve, reject) => {
95
- const user = holosphere.gun.user();
96
- user.create(holosphere.userName(spaceId2), password2, (ack) => {
97
- if (ack.err && !ack.err.includes('already created')) {
98
- reject(new Error(`User creation failed for ${spaceId2}: ${ack.err}`));
99
- } else {
100
- // Try to authenticate again
101
- user.auth(holosphere.userName(spaceId2), password2, (authAck) => {
102
- if (authAck.err) {
103
- reject(new Error(`Authentication failed after creation for ${spaceId2}: ${authAck.err}`));
104
- } else {
105
- canAccessSpace2 = true;
106
- resolve();
107
- }
108
- });
109
- }
110
- });
111
- });
112
- } catch (createError) {
113
- console.warn(`Could not create or authenticate user for ${spaceId2}: ${createError.message}`);
114
- // Continue with limited functionality
115
- }
116
- }
117
- } else {
118
- // No password required, assume we can access
119
- canAccessSpace2 = true;
120
- }
121
-
122
- // Warn if we can't access one or both spaces
123
- if (!canAccessSpace1) {
124
- console.warn(`Limited access to space ${spaceId1} - federation may be incomplete`);
125
- }
126
- if (password2 && !canAccessSpace2) {
127
- console.warn(`Limited access to space ${spaceId2} - federation may be incomplete`);
128
- }
31
+ // Get or create federation info for first space (A)
32
+ let fedInfo1 = null;
129
33
 
130
- // Get existing federation info for both spaces
131
- let fedInfo1 = null;
132
- let fedInfo2 = null;
133
-
134
34
  try {
135
35
  fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
136
36
  } catch (error) {
137
37
  console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
138
- // Create new federation info if we couldn't get existing
38
+ // Create new federation info if it doesn't exist
39
+
40
+ }
41
+ if (fedInfo1 == null) {
139
42
  fedInfo1 = {
140
43
  id: spaceId1,
141
44
  name: spaceId1,
@@ -145,47 +48,44 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
145
48
  };
146
49
  }
147
50
 
148
- if (password2) {
149
- try {
150
- fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
151
- } catch (error) {
152
- console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
153
- // Create new federation info if we couldn't get existing
154
- fedInfo2 = {
155
- id: spaceId2,
156
- name: spaceId2,
157
- federation: [],
158
- notify: [],
159
- timestamp: Date.now()
160
- };
161
- }
162
- }
163
51
 
164
- // Check if federation already exists
165
- if (fedInfo1 && fedInfo1.federation && fedInfo1.federation.includes(spaceId2)) {
166
- console.log(`Federation already exists between ${spaceId1} and ${spaceId2}`);
167
- return true;
168
- }
169
-
170
- // Create or update federation info for first space
171
- if (!fedInfo1) {
172
- fedInfo1 = {
173
- id: spaceId1,
174
- name: spaceId1,
175
- federation: [],
176
- notify: [],
177
- timestamp: Date.now()
178
- };
179
- }
52
+ // Ensure arrays exist
180
53
  if (!fedInfo1.federation) fedInfo1.federation = [];
54
+ if (!fedInfo1.notify) fedInfo1.notify = [];
55
+
56
+ // Add space2 to space1's federation and notify lists if not already present
181
57
  if (!fedInfo1.federation.includes(spaceId2)) {
182
58
  fedInfo1.federation.push(spaceId2);
183
59
  }
60
+ // // Always add to notify list for the first space (primary direction)
61
+ // if (!fedInfo1.notify.includes(spaceId2)) {
62
+ // fedInfo1.notify.push(spaceId2);
63
+ // }
64
+
65
+ // Update timestamp
184
66
  fedInfo1.timestamp = Date.now();
185
67
 
186
- // Create or update federation info for second space
187
- if (password2 && canAccessSpace2) {
188
- if (!fedInfo2) {
68
+ // Save updated federation info for space1
69
+ try {
70
+ await holosphere.putGlobal('federation', fedInfo1, password1);
71
+ console.log(`Updated federation info for ${spaceId1}`);
72
+ } catch (error) {
73
+ console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
74
+ throw new Error(`Failed to create federation: ${error.message}`);
75
+ }
76
+
77
+ // If bidirectional is true, handle space2 (B) as well
78
+ //if (bidirectional && password2) {
79
+ {
80
+ let fedInfo2 = null;
81
+ try {
82
+ fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
83
+ } 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
+ }
88
+ if (fedInfo2 == null) {
189
89
  fedInfo2 = {
190
90
  id: spaceId2,
191
91
  name: spaceId2,
@@ -194,37 +94,38 @@ export async function federate(holosphere, spaceId1, spaceId2, password1 = null,
194
94
  timestamp: Date.now()
195
95
  };
196
96
  }
97
+
98
+ // Add nEnsure arrays exist
99
+
197
100
  if (!fedInfo2.notify) fedInfo2.notify = [];
101
+
102
+ // Add space1 to space2's federation list if not already present
198
103
  if (!fedInfo2.notify.includes(spaceId1)) {
199
104
  fedInfo2.notify.push(spaceId1);
200
105
  }
106
+
107
+
108
+ // Update timestamp
201
109
  fedInfo2.timestamp = Date.now();
202
-
203
- // Save second federation record if we have password and access
110
+
111
+ // Save updated federation info for space2
204
112
  try {
205
113
  await holosphere.putGlobal('federation', fedInfo2, password2);
206
114
  console.log(`Updated federation info for ${spaceId2}`);
207
115
  } catch (error) {
208
116
  console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
117
+ // Don't throw here as the main federation was successful
209
118
  }
210
119
  }
211
120
 
212
- // Save first federation record
213
- try {
214
- await holosphere.putGlobal('federation', fedInfo1, password1);
215
- console.log(`Updated federation info for ${spaceId1}`);
216
- } catch (error) {
217
- console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
218
- throw new Error(`Failed to create federation: ${error.message}`);
219
- }
220
-
221
121
  // Create federation metadata record
222
122
  const federationMeta = {
223
123
  id: `${spaceId1}_${spaceId2}`,
224
124
  space1: spaceId1,
225
125
  space2: spaceId2,
226
126
  created: Date.now(),
227
- status: 'active'
127
+ status: 'active',
128
+ bidirectional: bidirectional
228
129
  };
229
130
 
230
131
  try {
@@ -438,29 +339,76 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
438
339
  }
439
340
 
440
341
  /**
441
- * Gets data from a holon and lens, including data from federated spaces with optional aggregation
342
+ * Removes a notification relationship between two spaces
343
+ * This removes spaceId2 from the notify list of spaceId1
344
+ *
442
345
  * @param {object} holosphere - The HoloSphere instance
443
- * @param {string} holon - The holon identifier
444
- * @param {string} lens - The lens identifier
445
- * @param {object} options - Options for data retrieval and aggregation
446
- * @param {boolean} [options.aggregate=false] - Whether to aggregate data
447
- * @param {string} [options.idField='id'] - Field to use as unique identifier
448
- * @param {string[]} [options.sumFields=[]] - Fields to sum when aggregating
449
- * @param {string[]} [options.concatArrays=[]] - Array fields to concatenate
450
- * @param {boolean} [options.removeDuplicates=true] - Whether to remove duplicates
451
- * @param {function} [options.mergeStrategy=null] - Custom merge function
452
- * @param {boolean} [options.includeLocal=true] - Whether to include local data
453
- * @param {boolean} [options.includeFederated=true] - Whether to include federated data
454
- * @param {string} [password] - Optional password for accessing private data
455
- * @returns {Promise<Array>} - Combined array of local and federated data
346
+ * @param {string} spaceId1 - The space to modify (remove from its notify list)
347
+ * @param {string} spaceId2 - The space to be removed from notifications
348
+ * @param {string} [password1] - Optional password for the first space
349
+ * @returns {Promise<boolean>} - True if notification was removed successfully
456
350
  */
457
- export async function getFederated(holosphere, holon, lens, options = {}, password = null) {
458
- // Validate required parameters
459
- if (!holon || !lens) {
460
- throw new Error('getFederated: Missing required parameters');
351
+ export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = null) {
352
+ if (!spaceId1 || !spaceId2) {
353
+ throw new Error('removeNotify: Missing required space IDs');
461
354
  }
462
355
 
463
- const {
356
+ try {
357
+ // Get federation info for space
358
+ let fedInfo = await holosphere.getGlobal('federation', spaceId1, password1);
359
+
360
+ if (!fedInfo) {
361
+ throw new Error(`No federation info found for ${spaceId1}`);
362
+ }
363
+
364
+ // Ensure notify array exists
365
+ if (!fedInfo.notify) fedInfo.notify = [];
366
+
367
+ // Remove space2 from space1's notify list if present
368
+ if (fedInfo.notify.includes(spaceId2)) {
369
+ fedInfo.notify = fedInfo.notify.filter(id => id !== spaceId2);
370
+
371
+ // Update timestamp
372
+ fedInfo.timestamp = Date.now();
373
+
374
+ // Save updated federation info
375
+ await holosphere.putGlobal('federation', fedInfo, password1);
376
+ console.log(`Removed ${spaceId2} from ${spaceId1}'s notify list`);
377
+ return true;
378
+ } else {
379
+ console.log(`${spaceId2} not found in ${spaceId1}'s notify list`);
380
+ return false;
381
+ }
382
+ } catch (error) {
383
+ console.error(`Remove notification failed: ${error.message}`);
384
+ throw error;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Get and combine data from local and federated sources
390
+ * @param {HoloSphere} holosphere The HoloSphere instance
391
+ * @param {string} holon The local holon name
392
+ * @param {string} lens The lens to query
393
+ * @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)
405
+ * @returns {Promise<Array>} Combined array of local and federated data
406
+ */
407
+ export async function getFederated(holosphere, holon, lens, options = {}) {
408
+ console.log(`getFederated called with options:`, JSON.stringify(options));
409
+
410
+ // Set default options
411
+ const {
464
412
  aggregate = false,
465
413
  idField = 'id',
466
414
  sumFields = [],
@@ -469,152 +417,239 @@ export async function getFederated(holosphere, holon, lens, options = {}, passwo
469
417
  mergeStrategy = null,
470
418
  includeLocal = true,
471
419
  includeFederated = true,
472
- maxFederatedSpaces = 10,
473
- timeout = 5000
420
+ resolveReferences = true, // Default to true
421
+ maxFederatedSpaces = -1,
422
+ timeout = 10000
474
423
  } = options;
424
+
425
+ console.log(`resolveReferences option: ${resolveReferences}`);
426
+
427
+ // Validate required parameters
428
+ if (!holosphere || !holon || !lens) {
429
+ throw new Error('Missing required parameters: holosphere, holon, and lens are required');
430
+ }
475
431
 
476
432
  // Get federation info for current space
477
433
  // Use holon as the space ID
478
434
  const spaceId = holon;
479
- const fedInfo = await getFederation(holosphere, spaceId, password);
435
+ const fedInfo = await getFederation(holosphere, spaceId);
480
436
 
481
- // Initialize result array
482
- let allData = [];
437
+ console.log(`Federation info retrieved:`, JSON.stringify(fedInfo));
483
438
 
484
- // Get local data if requested
485
- if (includeLocal) {
486
- const localData = await holosphere.getAll(holon, lens, password);
487
- allData = [...localData];
488
- }
489
-
490
- // If federation is disabled or no federation exists, return local data only
491
- if (!includeFederated || !fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
492
- return allData;
493
- }
494
-
495
- // Limit number of federated spaces to query
496
- const federatedSpaces = fedInfo.federation.slice(0, maxFederatedSpaces);
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
497
443
 
498
- // Get data from each federated space with timeout
499
- const federatedDataPromises = federatedSpaces.map(async (federatedSpace) => {
500
- try {
501
- // Create a promise that rejects after timeout
502
- const timeoutPromise = new Promise((_, reject) => {
503
- setTimeout(() => reject(new Error('Federation request timed out')), timeout);
504
- });
505
-
506
- // Create the actual data fetch promise
507
- const dataPromise = holosphere.getAll(federatedSpace, lens, password);
508
-
509
- // Race the promises
510
- const data = await Promise.race([dataPromise, timeoutPromise]);
511
-
512
- // Add federation metadata to each item
513
- return (data || []).map(item => ({
514
- ...item,
515
- federation: {
516
- ...item.federation,
517
- origin: federatedSpace,
518
- timestamp: item.federation?.timestamp || Date.now()
444
+ // Process each federated space first to prioritize federation data
445
+ 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
+ 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]);
519
479
  }
520
- }));
521
- } catch (error) {
522
- console.warn(`Error getting data from federated space ${federatedSpace}:`, error);
523
- return [];
480
+ } catch (error) {
481
+ console.warn(`Error processing federated space ${federatedSpace}: ${error.message}`);
482
+ }
524
483
  }
525
- });
526
-
527
- // Wait for all federated data requests to complete
528
- const federatedResults = await Promise.allSettled(federatedDataPromises);
484
+ }
529
485
 
530
- // Add successful results to allData
531
- federatedResults.forEach(result => {
532
- if (result.status === 'fulfilled') {
533
- allData = [...allData, ...result.value];
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`);
498
+ }
534
499
  }
535
- });
536
-
537
- // If aggregating, use enhanced aggregation logic
538
- if (aggregate) {
539
- const aggregated = new Map();
540
-
541
- for (const item of allData) {
542
- const itemId = item[idField];
543
- if (!itemId) continue;
544
-
545
- const existing = aggregated.get(itemId);
546
- if (!existing) {
547
- aggregated.set(itemId, { ...item });
548
- } else {
549
- // If custom merge strategy is provided, use it
550
- if (mergeStrategy && typeof mergeStrategy === 'function') {
551
- aggregated.set(itemId, mergeStrategy(existing, item));
552
- continue;
553
- }
554
-
555
- // Enhanced default merge strategy
556
- const merged = { ...existing };
557
-
558
- // Sum numeric fields
559
- for (const field of sumFields) {
560
- if (typeof item[field] === 'number') {
561
- merged[field] = (merged[field] || 0) + (item[field] || 0);
500
+ }
501
+
502
+ // Now resolve references if needed
503
+ if (resolveReferences) {
504
+ console.log(`Resolving references for ${result.length} items`);
505
+
506
+ for (let i = 0; i < result.length; i++) {
507
+ const item = result[i];
508
+
509
+ // Check for simplified reference (item with id and soul)
510
+ if (item.soul && item.id) {
511
+ console.log(`Found simple reference with soul: ${item.soul}`);
512
+
513
+ try {
514
+ // Parse the soul to get the components
515
+ const soulParts = item.soul.split('/');
516
+ if (soulParts.length >= 4) {
517
+ const originHolon = soulParts[1];
518
+ const originLens = soulParts[2];
519
+ const originKey = soulParts[3];
520
+
521
+ console.log(`Extracting from soul - holon: ${originHolon}, lens: ${originLens}, key: ${originKey}`);
522
+
523
+ // Get original data using the extracted path
524
+ const originalData = await holosphere.get(
525
+ originHolon,
526
+ originLens,
527
+ originKey,
528
+ null,
529
+ { resolveReferences: false } // Prevent infinite recursion
530
+ );
531
+
532
+ console.log(`Original data found via soul path:`, JSON.stringify(originalData));
533
+
534
+ if (originalData) {
535
+ // Replace the reference with the original data
536
+ result[i] = {
537
+ ...originalData,
538
+ _federation: {
539
+ isReference: true,
540
+ resolved: true,
541
+ soul: item.soul,
542
+ timestamp: Date.now()
543
+ }
544
+ };
545
+ console.log(`Reference resolved successfully via soul path, processed item:`, JSON.stringify(result[i]));
546
+ } else {
547
+ console.warn(`Could not resolve reference: original data not found at extracted path`);
548
+ }
549
+ } else {
550
+ console.warn(`Soul doesn't match expected format: ${item.soul}`);
562
551
  }
552
+ } catch (refError) {
553
+ console.warn(`Error resolving reference by soul in getFederated: ${refError.message}`);
563
554
  }
564
-
565
- // Concatenate and deduplicate array fields
566
- for (const field of concatArrays) {
567
- if (Array.isArray(item[field])) {
568
- const combinedArray = [
569
- ...(merged[field] || []),
570
- ...(item[field] || [])
571
- ];
572
- // Remove duplicates if elements are primitive
573
- merged[field] = Array.from(new Set(combinedArray));
555
+ }
556
+ // For backward compatibility, check for old-style references
557
+ else if (item._federation && item._federation.isReference) {
558
+ console.log(`Found legacy reference: ${item._federation.origin}/${item._federation.lens}/${item[idField]}`);
559
+
560
+ try {
561
+ const reference = item._federation;
562
+ console.log(`Getting original data from ${reference.origin} / ${reference.lens} / ${item[idField]}`);
563
+
564
+ // Get original data
565
+ const originalData = await holosphere.get(
566
+ reference.origin,
567
+ reference.lens,
568
+ item[idField],
569
+ null,
570
+ { resolveReferences: false } // Prevent infinite recursion
571
+ );
572
+
573
+ console.log(`Original data found:`, JSON.stringify(originalData));
574
+
575
+ if (originalData) {
576
+ // Add federation information to the resolved data
577
+ result[i] = {
578
+ ...originalData,
579
+ _federation: {
580
+ ...reference,
581
+ resolved: true,
582
+ timestamp: Date.now()
583
+ }
584
+ };
585
+ console.log(`Legacy reference resolved successfully, processed item:`, JSON.stringify(result[i]));
586
+ } else {
587
+ console.warn(`Could not resolve legacy reference: original data not found`);
574
588
  }
589
+ } catch (refError) {
590
+ console.warn(`Error resolving legacy reference in getFederated: ${refError.message}`);
575
591
  }
576
-
577
- // Update federation metadata
578
- merged.federation = {
579
- ...merged.federation,
580
- timestamp: Math.max(
581
- merged.federation?.timestamp || 0,
582
- item.federation?.timestamp || 0
583
- ),
584
- origins: Array.from(new Set([
585
- ...(Array.isArray(merged.federation?.origins) ? merged.federation.origins :
586
- (merged.federation?.origin ? [merged.federation.origin] : [])),
587
- ...(Array.isArray(item.federation?.origins) ? item.federation.origins :
588
- (item.federation?.origin ? [item.federation.origin] : []))
589
- ]))
590
- };
591
-
592
- // Update the aggregated item
593
- aggregated.set(itemId, merged);
594
592
  }
595
593
  }
596
-
597
- return Array.from(aggregated.values());
598
594
  }
599
-
600
- // If not aggregating, optionally remove duplicates based on idField
601
- if (!removeDuplicates) {
602
- return allData;
595
+
596
+ // Apply aggregation if requested
597
+ if (aggregate && result.length > 0) {
598
+ // Group items by ID
599
+ const groupedById = result.reduce((acc, item) => {
600
+ const id = item[idField];
601
+ if (!acc[id]) {
602
+ acc[id] = [];
603
+ }
604
+ acc[id].push(item);
605
+ return acc;
606
+ }, {});
607
+
608
+ // Aggregate each group
609
+ const aggregatedData = Object.values(groupedById).map(group => {
610
+ // If only one item in group, no aggregation needed
611
+ if (group.length === 1) return group[0];
612
+
613
+ // Use custom merge strategy if provided
614
+ if (mergeStrategy && typeof mergeStrategy === 'function') {
615
+ return mergeStrategy(group);
616
+ }
617
+
618
+ // Default aggregation strategy
619
+ const base = { ...group[0] };
620
+
621
+ // Sum numeric fields
622
+ for (const field of sumFields) {
623
+ if (typeof base[field] === 'number') {
624
+ base[field] = group.reduce((sum, item) => sum + (Number(item[field]) || 0), 0);
625
+ }
626
+ }
627
+
628
+ // Concatenate array fields
629
+ for (const field of concatArrays) {
630
+ if (Array.isArray(base[field])) {
631
+ const allValues = group.reduce((all, item) => {
632
+ return Array.isArray(item[field]) ? [...all, ...item[field]] : all;
633
+ }, []);
634
+
635
+ // Remove duplicates if requested
636
+ base[field] = removeDuplicates ? Array.from(new Set(allValues)) : allValues;
637
+ }
638
+ }
639
+
640
+ // Add aggregation metadata
641
+ base._aggregated = {
642
+ count: group.length,
643
+ timestamp: Date.now()
644
+ };
645
+
646
+ return base;
647
+ });
648
+
649
+ return aggregatedData;
603
650
  }
604
-
605
- // Remove duplicates keeping the most recent version
606
- const uniqueMap = new Map();
607
- allData.forEach(item => {
608
- const id = item[idField];
609
- if (!id) return;
610
-
611
- const existing = uniqueMap.get(id);
612
- if (!existing ||
613
- (item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
614
- uniqueMap.set(id, item);
615
- }
616
- });
617
- return Array.from(uniqueMap.values());
651
+
652
+ return result;
618
653
  }
619
654
 
620
655
  /**
@@ -624,100 +659,330 @@ export async function getFederated(holosphere, holon, lens, options = {}, passwo
624
659
  * @param {string} lens - The lens identifier
625
660
  * @param {object} data - The data to propagate
626
661
  * @param {object} [options] - Propagation options
627
- * @param {string[]} [options.targetSpaces] - Specific spaces to propagate to (default: all federated)
628
- * @param {boolean} [options.addFederationMetadata=true] - Whether to add federation metadata
662
+ * @param {boolean} [options.useReferences=true] - Whether to use references instead of duplicating data
663
+ * @param {string[]} [options.targetSpaces] - Specific target spaces to propagate to (defaults to all federated spaces)
664
+ * @param {string} [options.password] - Password for accessing the source holon (if needed)
629
665
  * @returns {Promise<object>} - Result with success count and errors
630
666
  */
631
- export async function propagateToFederation(holosphere, holon, lens, data, options = {}) {
632
- if (!holon || !lens || !data) {
633
- throw new Error('propagateToFederation: Missing required parameters');
667
+ export async function propagate(holosphere, holon, lens, data, options = {}) {
668
+ if (!holosphere || !holon || !lens || !data) {
669
+ throw new Error('propagate: Missing required parameters');
634
670
  }
635
-
636
- if (!data.id) {
637
- data.id = holosphere.generateId();
638
- }
639
-
671
+ // Default propagation options
640
672
  const {
673
+ useReferences = true,
641
674
  targetSpaces = null,
642
- addFederationMetadata = true
675
+ password = null
643
676
  } = options;
644
-
677
+
678
+ const result = {
679
+ success: 0,
680
+ errors: 0,
681
+ errorDetails: [],
682
+ propagated: false,
683
+ referencesUsed: useReferences
684
+ };
685
+
645
686
  try {
646
- // Get federation info for current space
647
- // Use holon as the space ID
648
- const spaceId = holon;
649
- const fedInfo = await getFederation(holosphere, spaceId);
650
- if (!fedInfo || !fedInfo.notify || fedInfo.notify.length === 0) {
651
- return { success: 0, errors: 0, message: 'No federation to propagate to' };
687
+ // Get federation info for this holon using getFederation
688
+ const fedInfo = await getFederation(holosphere, holon, password);
689
+
690
+ // If no federation info or no federation list, return with message
691
+ if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
692
+ return {
693
+ ...result,
694
+ message: `No federation found for ${holon}`
695
+ };
652
696
  }
653
697
 
654
- // Determine which spaces to propagate to
655
- const spacesToNotify = targetSpaces ?
656
- fedInfo.notify.filter(spaceId => targetSpaces.includes(spaceId)) :
657
- fedInfo.notify;
658
-
659
- if (spacesToNotify.length === 0) {
660
- return { success: 0, errors: 0, message: 'No matching spaces to propagate to' };
698
+ // If no notification list or it's empty, return with message
699
+ if (!fedInfo.notify || fedInfo.notify.length === 0) {
700
+ return {
701
+ ...result,
702
+ message: `No notification targets found for ${holon}`
703
+ };
661
704
  }
662
705
 
663
- // Add federation metadata if requested
664
- const dataToPropagate = { ...data };
665
- if (addFederationMetadata) {
666
- dataToPropagate.federation = {
667
- ...dataToPropagate.federation,
668
- origin: spaceId,
669
- timestamp: Date.now()
706
+ // Filter federation spaces to those in notify list
707
+ let spaces = fedInfo.notify;
708
+
709
+ // Further filter by targetSpaces if provided
710
+ if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) {
711
+ spaces = spaces.filter(space => targetSpaces.includes(space));
712
+ }
713
+
714
+ if (spaces.length === 0) {
715
+ return {
716
+ ...result,
717
+ message: 'No valid target spaces found after filtering'
670
718
  };
671
719
  }
672
720
 
673
- // Track results
674
- const results = {
675
- success: 0,
676
- errors: 0,
677
- errorDetails: []
721
+ // For each target space, propagate the data
722
+ const propagatePromises = spaces.map(async (targetSpace) => {
723
+ 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
+ }
741
+ };
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
+ } else {
751
+ // If not using references, store a full copy without propagation
752
+ const dataToStore = {
753
+ ...data,
754
+ _federation: {
755
+ origin: holon,
756
+ lens: lens,
757
+ timestamp: Date.now()
758
+ }
759
+ };
760
+ await holosphere.put(targetSpace, lens, dataToStore, null, { autoPropagate: false });
761
+ result.success++;
762
+ return true;
763
+ }
764
+ } catch (error) {
765
+ result.errors++;
766
+ result.errorDetails.push({
767
+ space: targetSpace,
768
+ error: error.message
769
+ });
770
+ return false;
771
+ }
772
+ });
773
+
774
+ await Promise.all(propagatePromises);
775
+
776
+ result.propagated = result.success > 0;
777
+ return result;
778
+ } catch (error) {
779
+ console.error('Error in propagate:', error);
780
+ return {
781
+ ...result,
782
+ error: error.message
783
+ };
784
+ }
785
+ }
786
+
787
+ /**
788
+ * Tracks a federated message across different chats
789
+ * @param {object} holosphere - The HoloSphere instance
790
+ * @param {string} originalChatId - The ID of the original chat
791
+ * @param {string} messageId - The ID of the original message
792
+ * @param {string} federatedChatId - The ID of the federated chat
793
+ * @param {string} federatedMessageId - The ID of the message in the federated chat
794
+ * @param {string} type - The type of message (e.g., 'quest', 'announcement')
795
+ * @returns {Promise<void>}
796
+ */
797
+ export async function federateMessage(holosphere, originalChatId, messageId, federatedChatId, federatedMessageId, type = 'generic') {
798
+ const trackingKey = `${originalChatId}_${messageId}_fedmsgs`;
799
+ const tracking = await holosphere.getGlobal('federation_messages', trackingKey) || {
800
+ id: trackingKey,
801
+ originalChatId,
802
+ originalMessageId: messageId,
803
+ type,
804
+ messages: []
805
+ };
806
+
807
+ // Update or add the federated message info
808
+ const existingMsg = tracking.messages.find(m => m.chatId === federatedChatId);
809
+ if (existingMsg) {
810
+ existingMsg.messageId = federatedMessageId;
811
+ existingMsg.timestamp = Date.now();
812
+ } else {
813
+ tracking.messages.push({
814
+ chatId: federatedChatId,
815
+ messageId: federatedMessageId,
816
+ timestamp: Date.now()
817
+ });
818
+ }
819
+
820
+ await holosphere.putGlobal('federation_messages', tracking);
821
+ }
822
+
823
+ /**
824
+ * Gets all federated messages for a given original message
825
+ * @param {object} holosphere - The HoloSphere instance
826
+ * @param {string} originalChatId - The ID of the original chat
827
+ * @param {string} messageId - The ID of the original message
828
+ * @returns {Promise<Object|null>} The tracking information for the message
829
+ */
830
+ export async function getFederatedMessages(holosphere, originalChatId, messageId) {
831
+ const trackingKey = `${originalChatId}_${messageId}_fedmsgs`;
832
+ return await holosphere.getGlobal('federation_messages', trackingKey);
833
+ }
834
+
835
+ /**
836
+ * Updates a federated message across all federated chats
837
+ * @param {object} holosphere - The HoloSphere instance
838
+ * @param {string} originalChatId - The ID of the original chat
839
+ * @param {string} messageId - The ID of the original message
840
+ * @param {Function} updateCallback - Function to update the message in each chat
841
+ * @returns {Promise<void>}
842
+ */
843
+ export async function updateFederatedMessages(holosphere, originalChatId, messageId, updateCallback) {
844
+ const tracking = await getFederatedMessages(holosphere, originalChatId, messageId);
845
+ if (!tracking?.messages) return;
846
+
847
+ for (const msg of tracking.messages) {
848
+ try {
849
+ await updateCallback(msg.chatId, msg.messageId);
850
+ } catch (error) {
851
+ console.warn(`Failed to update federated message in chat ${msg.chatId}:`, error);
852
+ }
853
+ }
854
+ }
855
+
856
+ /**
857
+ * Resets all federation relationships for a space
858
+ * @param {object} holosphere - The HoloSphere instance
859
+ * @param {string} spaceId - The ID of the space to reset federation for
860
+ * @param {string} [password] - Optional password for the space
861
+ * @param {object} [options] - Reset options
862
+ * @param {boolean} [options.notifyPartners=true] - Whether to notify federation partners about the reset
863
+ * @param {string} [options.spaceName] - Optional name for the space (defaults to spaceId if not provided)
864
+ * @returns {Promise<object>} - Result object with success/error info
865
+ */
866
+ export async function resetFederation(holosphere, spaceId, password = null, options = {}) {
867
+ if (!spaceId) {
868
+ throw new Error('resetFederation: Missing required space ID');
869
+ }
870
+
871
+ const {
872
+ notifyPartners = true,
873
+ spaceName = null
874
+ } = options;
875
+
876
+ const result = {
877
+ success: false,
878
+ federatedCount: 0,
879
+ notifyCount: 0,
880
+ partnersNotified: 0,
881
+ errors: []
882
+ };
883
+
884
+ try {
885
+ // Get current federation info to know what we're clearing
886
+ const fedInfo = await getFederation(holosphere, spaceId, password);
887
+
888
+ if (!fedInfo) {
889
+ return {
890
+ ...result,
891
+ success: true,
892
+ message: 'No federation configuration found for this space'
893
+ };
894
+ }
895
+
896
+ // Store counts for reporting
897
+ result.federatedCount = fedInfo.federation?.length || 0;
898
+ result.notifyCount = fedInfo.notify?.length || 0;
899
+
900
+ // Create empty federation record
901
+ const emptyFedInfo = {
902
+ id: spaceId,
903
+ name: spaceName || spaceId,
904
+ federation: [],
905
+ notify: [],
906
+ timestamp: Date.now()
678
907
  };
679
908
 
680
- // Propagate to each federated space
681
- const propagationPromises = spacesToNotify.map(spaceId =>
682
- new Promise((resolve) => {
909
+ // Update federation record with empty lists
910
+ await holosphere.putGlobal('federation', emptyFedInfo, password);
911
+
912
+ // Notify federation partners if requested
913
+ if (notifyPartners && fedInfo.federation && fedInfo.federation.length > 0) {
914
+ const updatePromises = fedInfo.federation.map(async (partnerSpace) => {
683
915
  try {
684
- // Store data in the federated space's lens
685
- holosphere.gun.get(holosphere.appname)
686
- .get(spaceId)
687
- .get(lens)
688
- .get(dataToPropagate.id)
689
- .put(JSON.stringify(dataToPropagate), ack => {
690
- if (ack.err) {
691
- results.errors++;
692
- results.errorDetails.push({
693
- space: spaceId,
694
- error: ack.err
695
- });
696
- } else {
697
- results.success++;
698
- }
699
- resolve();
700
- });
916
+ // Get partner's federation info
917
+ const partnerFedInfo = await getFederation(holosphere, partnerSpace);
918
+
919
+ if (partnerFedInfo) {
920
+ // Remove this space from partner's federation list
921
+ if (partnerFedInfo.federation) {
922
+ partnerFedInfo.federation = partnerFedInfo.federation.filter(
923
+ id => id !== spaceId.toString()
924
+ );
925
+ }
926
+
927
+ // Remove this space from partner's notify list
928
+ if (partnerFedInfo.notify) {
929
+ partnerFedInfo.notify = partnerFedInfo.notify.filter(
930
+ id => id !== spaceId.toString()
931
+ );
932
+ }
933
+
934
+ partnerFedInfo.timestamp = Date.now();
935
+
936
+ // Save partner's updated federation info
937
+ await holosphere.putGlobal('federation', partnerFedInfo);
938
+ console.log(`Updated federation info for partner ${partnerSpace}`);
939
+ result.partnersNotified++;
940
+ return true;
941
+ }
942
+ return false;
701
943
  } catch (error) {
702
- results.errors++;
703
- results.errorDetails.push({
704
- space: spaceId,
944
+ console.warn(`Could not update federation info for partner ${partnerSpace}: ${error.message}`);
945
+ result.errors.push({
946
+ partner: partnerSpace,
705
947
  error: error.message
706
948
  });
707
- resolve();
949
+ return false;
708
950
  }
709
- })
710
- );
711
-
712
- await Promise.all(propagationPromises);
713
- return results;
951
+ });
952
+
953
+ await Promise.all(updatePromises);
954
+ }
955
+
956
+ // Update federation metadata records if they exist
957
+ if (fedInfo.federation && fedInfo.federation.length > 0) {
958
+ for (const partnerSpace of fedInfo.federation) {
959
+ try {
960
+ const metaId = `${spaceId}_${partnerSpace}`;
961
+ const altMetaId = `${partnerSpace}_${spaceId}`;
962
+
963
+ const meta = await holosphere.getGlobal('federationMeta', metaId) ||
964
+ await holosphere.getGlobal('federationMeta', altMetaId);
965
+
966
+ if (meta) {
967
+ meta.status = 'inactive';
968
+ meta.endedAt = Date.now();
969
+ await holosphere.putGlobal('federationMeta', meta);
970
+ console.log(`Updated federation metadata for ${spaceId} and ${partnerSpace}`);
971
+ }
972
+ } catch (error) {
973
+ console.warn(`Could not update federation metadata for ${partnerSpace}: ${error.message}`);
974
+ }
975
+ }
976
+ }
977
+
978
+ result.success = true;
979
+ return result;
714
980
  } catch (error) {
715
- console.warn('Federation propagation error:', error);
716
- return {
717
- success: 0,
718
- errors: 1,
719
- message: error.message,
720
- errorDetails: [{ error: error.message }]
981
+ console.error(`Federation reset failed: ${error.message}`);
982
+ return {
983
+ ...result,
984
+ success: false,
985
+ error: error.message
721
986
  };
722
987
  }
723
988
  }