holosphere 1.1.5 → 1.1.7

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 ADDED
@@ -0,0 +1,988 @@
1
+ /**
2
+ * Federation functionality for HoloSphere
3
+ * Provides methods for creating, managing, and using federated spaces
4
+ */
5
+
6
+
7
+ /**
8
+ * Creates a federation relationship between two spaces
9
+ * Federation is bidirectional by default, and data propagation uses soul references by default.
10
+ *
11
+ * @param {object} holosphere - The HoloSphere instance
12
+ * @param {string} spaceId1 - The first space ID
13
+ * @param {string} spaceId2 - The second space ID
14
+ * @param {string} [password1] - Optional password for the first space
15
+ * @param {string} [password2] - Optional password for the second space
16
+ * @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications (default: true)
17
+ * @returns {Promise<boolean>} - True if federation was created successfully
18
+ */
19
+ export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true) {
20
+ console.log('FEDERATING', spaceId1, spaceId2, password1, password2, bidirectional)
21
+ if (!spaceId1 || !spaceId2) {
22
+ throw new Error('federate: Missing required space IDs');
23
+ }
24
+
25
+ // Prevent self-federation
26
+ if (spaceId1 === spaceId2) {
27
+ throw new Error('Cannot federate a space with itself');
28
+ }
29
+
30
+ try {
31
+ // Get or create federation info for first space (A)
32
+ let fedInfo1 = null;
33
+
34
+ try {
35
+ fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
36
+ } 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
+ }
41
+ if (fedInfo1 == null) {
42
+ fedInfo1 = {
43
+ id: spaceId1,
44
+ name: spaceId1,
45
+ federation: [],
46
+ notify: [],
47
+ timestamp: Date.now()
48
+ };
49
+ }
50
+
51
+
52
+ // Ensure arrays exist
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
57
+ if (!fedInfo1.federation.includes(spaceId2)) {
58
+ fedInfo1.federation.push(spaceId2);
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
66
+ fedInfo1.timestamp = Date.now();
67
+
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) {
89
+ fedInfo2 = {
90
+ id: spaceId2,
91
+ name: spaceId2,
92
+ federation: [],
93
+ notify: [],
94
+ timestamp: Date.now()
95
+ };
96
+ }
97
+
98
+ // Add nEnsure arrays exist
99
+
100
+ if (!fedInfo2.notify) fedInfo2.notify = [];
101
+
102
+ // Add space1 to space2's federation list if not already present
103
+ if (!fedInfo2.notify.includes(spaceId1)) {
104
+ fedInfo2.notify.push(spaceId1);
105
+ }
106
+
107
+
108
+ // Update timestamp
109
+ fedInfo2.timestamp = Date.now();
110
+
111
+ // Save updated federation info for space2
112
+ try {
113
+ await holosphere.putGlobal('federation', fedInfo2, password2);
114
+ console.log(`Updated federation info for ${spaceId2}`);
115
+ } 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
+ }
119
+ }
120
+
121
+ // Create federation metadata record
122
+ const federationMeta = {
123
+ id: `${spaceId1}_${spaceId2}`,
124
+ space1: spaceId1,
125
+ space2: spaceId2,
126
+ created: Date.now(),
127
+ status: 'active',
128
+ bidirectional: bidirectional
129
+ };
130
+
131
+ try {
132
+ await holosphere.putGlobal('federationMeta', federationMeta);
133
+ console.log(`Created federation metadata for ${spaceId1} and ${spaceId2}`);
134
+ } catch (error) {
135
+ console.warn(`Could not create federation metadata: ${error.message}`);
136
+ }
137
+
138
+ return true;
139
+ } catch (error) {
140
+ console.error(`Federation creation failed: ${error.message}`);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Subscribes to federation notifications for a space
147
+ * @param {object} holosphere - The HoloSphere instance
148
+ * @param {string} spaceId - The space ID to subscribe to
149
+ * @param {string} [password] - Optional password for the space
150
+ * @param {function} callback - The callback to execute on notifications
151
+ * @param {object} [options] - Subscription options
152
+ * @param {string[]} [options.lenses] - Specific lenses to subscribe to (default: all)
153
+ * @param {number} [options.throttle] - Throttle notifications in ms (default: 0)
154
+ * @returns {Promise<object>} - Subscription object with unsubscribe() method
155
+ */
156
+ export async function subscribeFederation(holosphere, spaceId, password = null, callback, options = {}) {
157
+ if (!spaceId || !callback) {
158
+ throw new Error('subscribeFederation: Missing required parameters');
159
+ }
160
+
161
+ const { lenses = ['*'], throttle = 0 } = options;
162
+
163
+ // Get federation info
164
+ const fedInfo = await holosphere.getGlobal('federation', spaceId, password);
165
+ if (!fedInfo) {
166
+ throw new Error('No federation info found for space');
167
+ }
168
+
169
+ // Create subscription for each federated space
170
+ const subscriptions = [];
171
+ let lastNotificationTime = {};
172
+
173
+ if (fedInfo.federation && fedInfo.federation.length > 0) {
174
+ for (const federatedSpace of fedInfo.federation) {
175
+ // For each lens specified (or all if '*')
176
+ for (const lens of lenses) {
177
+ try {
178
+ const sub = await holosphere.subscribe(federatedSpace, lens, async (data) => {
179
+ try {
180
+ // Skip if data is missing or not from federated space
181
+ if (!data || !data.id) return;
182
+
183
+ // Apply throttling if configured
184
+ const now = Date.now();
185
+ const key = `${federatedSpace}_${lens}_${data.id}`;
186
+
187
+ if (throttle > 0) {
188
+ if (lastNotificationTime[key] &&
189
+ (now - lastNotificationTime[key]) < throttle) {
190
+ return; // Skip this notification (throttled)
191
+ }
192
+ lastNotificationTime[key] = now;
193
+ }
194
+
195
+ // Add federation metadata if not present
196
+ if (!data.federation) {
197
+ data.federation = {
198
+ origin: federatedSpace,
199
+ timestamp: now
200
+ };
201
+ }
202
+
203
+ // Execute callback with the data
204
+ await callback(data, federatedSpace, lens);
205
+ } catch (error) {
206
+ console.warn('Federation notification error:', error);
207
+ }
208
+ });
209
+
210
+ if (sub && typeof sub.unsubscribe === 'function') {
211
+ subscriptions.push(sub);
212
+ }
213
+ } catch (error) {
214
+ console.warn(`Error creating subscription for ${federatedSpace}/${lens}:`, error);
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // Return combined subscription object
221
+ return {
222
+ unsubscribe: () => {
223
+ subscriptions.forEach(sub => {
224
+ try {
225
+ if (sub && typeof sub.unsubscribe === 'function') {
226
+ sub.unsubscribe();
227
+ }
228
+ } catch (error) {
229
+ console.warn('Error unsubscribing:', error);
230
+ }
231
+ });
232
+ // Clear the subscriptions array
233
+ subscriptions.length = 0;
234
+ // Clear throttling data
235
+ lastNotificationTime = {};
236
+ },
237
+ getSubscriptionCount: () => subscriptions.length
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Gets federation info for a space
243
+ * @param {object} holosphere - The HoloSphere instance
244
+ * @param {string} spaceId - The space ID
245
+ * @param {string} [password] - Optional password for the space
246
+ * @returns {Promise<object|null>} - Federation info or null if not found
247
+ */
248
+ export async function getFederation(holosphere, spaceId, password = null) {
249
+ if (!spaceId) {
250
+ throw new Error('getFederation: Missing space ID');
251
+ }
252
+ return await holosphere.getGlobal('federation', spaceId, password);
253
+ }
254
+
255
+ /**
256
+ * Removes a federation relationship between spaces
257
+ * @param {object} holosphere - The HoloSphere instance
258
+ * @param {string} spaceId1 - The first space ID
259
+ * @param {string} spaceId2 - The second space ID
260
+ * @param {string} [password1] - Optional password for the first space
261
+ * @param {string} [password2] - Optional password for the second space
262
+ * @returns {Promise<boolean>} - True if federation was removed successfully
263
+ */
264
+ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) {
265
+ if (!spaceId1 || !spaceId2) {
266
+ throw new Error('unfederate: Missing required space IDs');
267
+ }
268
+
269
+ try {
270
+ // Get federation info for first space
271
+ let fedInfo1 = null;
272
+ try {
273
+ fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
274
+ } catch (error) {
275
+ console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
276
+ }
277
+
278
+ if (!fedInfo1 || !fedInfo1.federation) {
279
+ console.warn(`Federation not found for space ${spaceId1}`);
280
+ // Continue anyway to clean up any potential metadata
281
+ } else {
282
+ // Update first space federation info
283
+ fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
284
+ fedInfo1.timestamp = Date.now();
285
+
286
+ try {
287
+ await holosphere.putGlobal('federation', fedInfo1, password1);
288
+ console.log(`Updated federation info for ${spaceId1}`);
289
+ } catch (error) {
290
+ console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
291
+ }
292
+ }
293
+
294
+ // Update second space federation info if password provided
295
+ if (password2) {
296
+ let fedInfo2 = null;
297
+ try {
298
+ fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
299
+ } catch (error) {
300
+ console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
301
+ }
302
+
303
+ if (fedInfo2 && fedInfo2.notify) {
304
+ fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
305
+ fedInfo2.timestamp = Date.now();
306
+
307
+ try {
308
+ await holosphere.putGlobal('federation', fedInfo2, password2);
309
+ console.log(`Updated federation info for ${spaceId2}`);
310
+ } catch (error) {
311
+ console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
312
+ }
313
+ }
314
+ }
315
+
316
+ // Update federation metadata
317
+ const metaId = `${spaceId1}_${spaceId2}`;
318
+ const altMetaId = `${spaceId2}_${spaceId1}`;
319
+
320
+ try {
321
+ const meta = await holosphere.getGlobal('federationMeta', metaId) ||
322
+ await holosphere.getGlobal('federationMeta', altMetaId);
323
+
324
+ if (meta) {
325
+ meta.status = 'inactive';
326
+ meta.endedAt = Date.now();
327
+ await holosphere.putGlobal('federationMeta', meta);
328
+ console.log(`Updated federation metadata for ${spaceId1} and ${spaceId2}`);
329
+ }
330
+ } catch (error) {
331
+ console.warn(`Could not update federation metadata: ${error.message}`);
332
+ }
333
+
334
+ return true;
335
+ } catch (error) {
336
+ console.error(`Federation removal failed: ${error.message}`);
337
+ throw error;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Removes a notification relationship between two spaces
343
+ * This removes spaceId2 from the notify list of spaceId1
344
+ *
345
+ * @param {object} holosphere - The HoloSphere instance
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
350
+ */
351
+ export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = null) {
352
+ if (!spaceId1 || !spaceId2) {
353
+ throw new Error('removeNotify: Missing required space IDs');
354
+ }
355
+
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 {
412
+ aggregate = false,
413
+ idField = 'id',
414
+ sumFields = [],
415
+ concatArrays = [],
416
+ removeDuplicates = true,
417
+ mergeStrategy = null,
418
+ includeLocal = true,
419
+ includeFederated = true,
420
+ resolveReferences = true, // Default to true
421
+ maxFederatedSpaces = -1,
422
+ timeout = 10000
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
+ }
431
+
432
+ // Get federation info for current space
433
+ // Use holon as the space ID
434
+ const spaceId = holon;
435
+ const fedInfo = await getFederation(holosphere, spaceId);
436
+
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
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]);
479
+ }
480
+ } catch (error) {
481
+ console.warn(`Error processing federated space ${federatedSpace}: ${error.message}`);
482
+ }
483
+ }
484
+ }
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`);
498
+ }
499
+ }
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}`);
551
+ }
552
+ } catch (refError) {
553
+ console.warn(`Error resolving reference by soul in getFederated: ${refError.message}`);
554
+ }
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`);
588
+ }
589
+ } catch (refError) {
590
+ console.warn(`Error resolving legacy reference in getFederated: ${refError.message}`);
591
+ }
592
+ }
593
+ }
594
+ }
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;
650
+ }
651
+
652
+ return result;
653
+ }
654
+
655
+ /**
656
+ * Propagates data to federated spaces
657
+ * @param {object} holosphere - The HoloSphere instance
658
+ * @param {string} holon - The holon identifier
659
+ * @param {string} lens - The lens identifier
660
+ * @param {object} data - The data to propagate
661
+ * @param {object} [options] - Propagation options
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)
665
+ * @returns {Promise<object>} - Result with success count and errors
666
+ */
667
+ export async function propagate(holosphere, holon, lens, data, options = {}) {
668
+ if (!holosphere || !holon || !lens || !data) {
669
+ throw new Error('propagate: Missing required parameters');
670
+ }
671
+ // Default propagation options
672
+ const {
673
+ useReferences = true,
674
+ targetSpaces = null,
675
+ password = null
676
+ } = options;
677
+
678
+ const result = {
679
+ success: 0,
680
+ errors: 0,
681
+ errorDetails: [],
682
+ propagated: false,
683
+ referencesUsed: useReferences
684
+ };
685
+
686
+ try {
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
+ };
696
+ }
697
+
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
+ };
704
+ }
705
+
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'
718
+ };
719
+ }
720
+
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()
907
+ };
908
+
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) => {
915
+ try {
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;
943
+ } catch (error) {
944
+ console.warn(`Could not update federation info for partner ${partnerSpace}: ${error.message}`);
945
+ result.errors.push({
946
+ partner: partnerSpace,
947
+ error: error.message
948
+ });
949
+ return false;
950
+ }
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;
980
+ } catch (error) {
981
+ console.error(`Federation reset failed: ${error.message}`);
982
+ return {
983
+ ...result,
984
+ success: false,
985
+ error: error.message
986
+ };
987
+ }
988
+ }