holosphere 1.1.5 → 1.1.6

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,723 @@
1
+ /**
2
+ * Federation functionality for HoloSphere
3
+ * Provides methods for creating, managing, and using federated spaces
4
+ */
5
+
6
+ /**
7
+ * Creates a federation relationship between two spaces
8
+ * @param {object} holosphere - The HoloSphere instance
9
+ * @param {string} spaceId1 - The first space ID
10
+ * @param {string} spaceId2 - The second space ID
11
+ * @param {string} [password1] - Optional password for the first space
12
+ * @param {string} [password2] - Optional password for the second space
13
+ * @returns {Promise<boolean>} - True if federation was created successfully
14
+ */
15
+ export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) {
16
+ if (!spaceId1 || !spaceId2) {
17
+ throw new Error('federate: Missing required space IDs');
18
+ }
19
+
20
+ // Prevent self-federation
21
+ if (spaceId1 === spaceId2) {
22
+ throw new Error('Cannot federate a space with itself');
23
+ }
24
+
25
+ // Verify access to both spaces before proceeding
26
+ let canAccessSpace1 = false;
27
+ let canAccessSpace2 = false;
28
+
29
+ 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
+ }
129
+
130
+ // Get existing federation info for both spaces
131
+ let fedInfo1 = null;
132
+ let fedInfo2 = null;
133
+
134
+ try {
135
+ fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
136
+ } catch (error) {
137
+ console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
138
+ // Create new federation info if we couldn't get existing
139
+ fedInfo1 = {
140
+ id: spaceId1,
141
+ name: spaceId1,
142
+ federation: [],
143
+ notify: [],
144
+ timestamp: Date.now()
145
+ };
146
+ }
147
+
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
+
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
+ }
180
+ if (!fedInfo1.federation) fedInfo1.federation = [];
181
+ if (!fedInfo1.federation.includes(spaceId2)) {
182
+ fedInfo1.federation.push(spaceId2);
183
+ }
184
+ fedInfo1.timestamp = Date.now();
185
+
186
+ // Create or update federation info for second space
187
+ if (password2 && canAccessSpace2) {
188
+ if (!fedInfo2) {
189
+ fedInfo2 = {
190
+ id: spaceId2,
191
+ name: spaceId2,
192
+ federation: [],
193
+ notify: [],
194
+ timestamp: Date.now()
195
+ };
196
+ }
197
+ if (!fedInfo2.notify) fedInfo2.notify = [];
198
+ if (!fedInfo2.notify.includes(spaceId1)) {
199
+ fedInfo2.notify.push(spaceId1);
200
+ }
201
+ fedInfo2.timestamp = Date.now();
202
+
203
+ // Save second federation record if we have password and access
204
+ try {
205
+ await holosphere.putGlobal('federation', fedInfo2, password2);
206
+ console.log(`Updated federation info for ${spaceId2}`);
207
+ } catch (error) {
208
+ console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
209
+ }
210
+ }
211
+
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
+ // Create federation metadata record
222
+ const federationMeta = {
223
+ id: `${spaceId1}_${spaceId2}`,
224
+ space1: spaceId1,
225
+ space2: spaceId2,
226
+ created: Date.now(),
227
+ status: 'active'
228
+ };
229
+
230
+ try {
231
+ await holosphere.putGlobal('federationMeta', federationMeta);
232
+ console.log(`Created federation metadata for ${spaceId1} and ${spaceId2}`);
233
+ } catch (error) {
234
+ console.warn(`Could not create federation metadata: ${error.message}`);
235
+ }
236
+
237
+ return true;
238
+ } catch (error) {
239
+ console.error(`Federation creation failed: ${error.message}`);
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Subscribes to federation notifications for a space
246
+ * @param {object} holosphere - The HoloSphere instance
247
+ * @param {string} spaceId - The space ID to subscribe to
248
+ * @param {string} [password] - Optional password for the space
249
+ * @param {function} callback - The callback to execute on notifications
250
+ * @param {object} [options] - Subscription options
251
+ * @param {string[]} [options.lenses] - Specific lenses to subscribe to (default: all)
252
+ * @param {number} [options.throttle] - Throttle notifications in ms (default: 0)
253
+ * @returns {Promise<object>} - Subscription object with unsubscribe() method
254
+ */
255
+ export async function subscribeFederation(holosphere, spaceId, password = null, callback, options = {}) {
256
+ if (!spaceId || !callback) {
257
+ throw new Error('subscribeFederation: Missing required parameters');
258
+ }
259
+
260
+ const { lenses = ['*'], throttle = 0 } = options;
261
+
262
+ // Get federation info
263
+ const fedInfo = await holosphere.getGlobal('federation', spaceId, password);
264
+ if (!fedInfo) {
265
+ throw new Error('No federation info found for space');
266
+ }
267
+
268
+ // Create subscription for each federated space
269
+ const subscriptions = [];
270
+ let lastNotificationTime = {};
271
+
272
+ if (fedInfo.federation && fedInfo.federation.length > 0) {
273
+ for (const federatedSpace of fedInfo.federation) {
274
+ // For each lens specified (or all if '*')
275
+ for (const lens of lenses) {
276
+ try {
277
+ const sub = await holosphere.subscribe(federatedSpace, lens, async (data) => {
278
+ try {
279
+ // Skip if data is missing or not from federated space
280
+ if (!data || !data.id) return;
281
+
282
+ // Apply throttling if configured
283
+ const now = Date.now();
284
+ const key = `${federatedSpace}_${lens}_${data.id}`;
285
+
286
+ if (throttle > 0) {
287
+ if (lastNotificationTime[key] &&
288
+ (now - lastNotificationTime[key]) < throttle) {
289
+ return; // Skip this notification (throttled)
290
+ }
291
+ lastNotificationTime[key] = now;
292
+ }
293
+
294
+ // Add federation metadata if not present
295
+ if (!data.federation) {
296
+ data.federation = {
297
+ origin: federatedSpace,
298
+ timestamp: now
299
+ };
300
+ }
301
+
302
+ // Execute callback with the data
303
+ await callback(data, federatedSpace, lens);
304
+ } catch (error) {
305
+ console.warn('Federation notification error:', error);
306
+ }
307
+ });
308
+
309
+ if (sub && typeof sub.unsubscribe === 'function') {
310
+ subscriptions.push(sub);
311
+ }
312
+ } catch (error) {
313
+ console.warn(`Error creating subscription for ${federatedSpace}/${lens}:`, error);
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ // Return combined subscription object
320
+ return {
321
+ unsubscribe: () => {
322
+ subscriptions.forEach(sub => {
323
+ try {
324
+ if (sub && typeof sub.unsubscribe === 'function') {
325
+ sub.unsubscribe();
326
+ }
327
+ } catch (error) {
328
+ console.warn('Error unsubscribing:', error);
329
+ }
330
+ });
331
+ // Clear the subscriptions array
332
+ subscriptions.length = 0;
333
+ // Clear throttling data
334
+ lastNotificationTime = {};
335
+ },
336
+ getSubscriptionCount: () => subscriptions.length
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Gets federation info for a space
342
+ * @param {object} holosphere - The HoloSphere instance
343
+ * @param {string} spaceId - The space ID
344
+ * @param {string} [password] - Optional password for the space
345
+ * @returns {Promise<object|null>} - Federation info or null if not found
346
+ */
347
+ export async function getFederation(holosphere, spaceId, password = null) {
348
+ if (!spaceId) {
349
+ throw new Error('getFederation: Missing space ID');
350
+ }
351
+ return await holosphere.getGlobal('federation', spaceId, password);
352
+ }
353
+
354
+ /**
355
+ * Removes a federation relationship between spaces
356
+ * @param {object} holosphere - The HoloSphere instance
357
+ * @param {string} spaceId1 - The first space ID
358
+ * @param {string} spaceId2 - The second space ID
359
+ * @param {string} [password1] - Optional password for the first space
360
+ * @param {string} [password2] - Optional password for the second space
361
+ * @returns {Promise<boolean>} - True if federation was removed successfully
362
+ */
363
+ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) {
364
+ if (!spaceId1 || !spaceId2) {
365
+ throw new Error('unfederate: Missing required space IDs');
366
+ }
367
+
368
+ try {
369
+ // Get federation info for first space
370
+ let fedInfo1 = null;
371
+ try {
372
+ fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
373
+ } catch (error) {
374
+ console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
375
+ }
376
+
377
+ if (!fedInfo1 || !fedInfo1.federation) {
378
+ console.warn(`Federation not found for space ${spaceId1}`);
379
+ // Continue anyway to clean up any potential metadata
380
+ } else {
381
+ // Update first space federation info
382
+ fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
383
+ fedInfo1.timestamp = Date.now();
384
+
385
+ try {
386
+ await holosphere.putGlobal('federation', fedInfo1, password1);
387
+ console.log(`Updated federation info for ${spaceId1}`);
388
+ } catch (error) {
389
+ console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
390
+ }
391
+ }
392
+
393
+ // Update second space federation info if password provided
394
+ if (password2) {
395
+ let fedInfo2 = null;
396
+ try {
397
+ fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
398
+ } catch (error) {
399
+ console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
400
+ }
401
+
402
+ if (fedInfo2 && fedInfo2.notify) {
403
+ fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
404
+ fedInfo2.timestamp = Date.now();
405
+
406
+ try {
407
+ await holosphere.putGlobal('federation', fedInfo2, password2);
408
+ console.log(`Updated federation info for ${spaceId2}`);
409
+ } catch (error) {
410
+ console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
411
+ }
412
+ }
413
+ }
414
+
415
+ // Update federation metadata
416
+ const metaId = `${spaceId1}_${spaceId2}`;
417
+ const altMetaId = `${spaceId2}_${spaceId1}`;
418
+
419
+ try {
420
+ const meta = await holosphere.getGlobal('federationMeta', metaId) ||
421
+ await holosphere.getGlobal('federationMeta', altMetaId);
422
+
423
+ if (meta) {
424
+ meta.status = 'inactive';
425
+ meta.endedAt = Date.now();
426
+ await holosphere.putGlobal('federationMeta', meta);
427
+ console.log(`Updated federation metadata for ${spaceId1} and ${spaceId2}`);
428
+ }
429
+ } catch (error) {
430
+ console.warn(`Could not update federation metadata: ${error.message}`);
431
+ }
432
+
433
+ return true;
434
+ } catch (error) {
435
+ console.error(`Federation removal failed: ${error.message}`);
436
+ throw error;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Gets data from a holon and lens, including data from federated spaces with optional aggregation
442
+ * @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
456
+ */
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');
461
+ }
462
+
463
+ const {
464
+ aggregate = false,
465
+ idField = 'id',
466
+ sumFields = [],
467
+ concatArrays = [],
468
+ removeDuplicates = true,
469
+ mergeStrategy = null,
470
+ includeLocal = true,
471
+ includeFederated = true,
472
+ maxFederatedSpaces = 10,
473
+ timeout = 5000
474
+ } = options;
475
+
476
+ // Get federation info for current space
477
+ // Use holon as the space ID
478
+ const spaceId = holon;
479
+ const fedInfo = await getFederation(holosphere, spaceId, password);
480
+
481
+ // Initialize result array
482
+ let allData = [];
483
+
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);
497
+
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()
519
+ }
520
+ }));
521
+ } catch (error) {
522
+ console.warn(`Error getting data from federated space ${federatedSpace}:`, error);
523
+ return [];
524
+ }
525
+ });
526
+
527
+ // Wait for all federated data requests to complete
528
+ const federatedResults = await Promise.allSettled(federatedDataPromises);
529
+
530
+ // Add successful results to allData
531
+ federatedResults.forEach(result => {
532
+ if (result.status === 'fulfilled') {
533
+ allData = [...allData, ...result.value];
534
+ }
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);
562
+ }
563
+ }
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));
574
+ }
575
+ }
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
+ }
595
+ }
596
+
597
+ return Array.from(aggregated.values());
598
+ }
599
+
600
+ // If not aggregating, optionally remove duplicates based on idField
601
+ if (!removeDuplicates) {
602
+ return allData;
603
+ }
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());
618
+ }
619
+
620
+ /**
621
+ * Propagates data to federated spaces
622
+ * @param {object} holosphere - The HoloSphere instance
623
+ * @param {string} holon - The holon identifier
624
+ * @param {string} lens - The lens identifier
625
+ * @param {object} data - The data to propagate
626
+ * @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
629
+ * @returns {Promise<object>} - Result with success count and errors
630
+ */
631
+ export async function propagateToFederation(holosphere, holon, lens, data, options = {}) {
632
+ if (!holon || !lens || !data) {
633
+ throw new Error('propagateToFederation: Missing required parameters');
634
+ }
635
+
636
+ if (!data.id) {
637
+ data.id = holosphere.generateId();
638
+ }
639
+
640
+ const {
641
+ targetSpaces = null,
642
+ addFederationMetadata = true
643
+ } = options;
644
+
645
+ 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' };
652
+ }
653
+
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' };
661
+ }
662
+
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()
670
+ };
671
+ }
672
+
673
+ // Track results
674
+ const results = {
675
+ success: 0,
676
+ errors: 0,
677
+ errorDetails: []
678
+ };
679
+
680
+ // Propagate to each federated space
681
+ const propagationPromises = spacesToNotify.map(spaceId =>
682
+ new Promise((resolve) => {
683
+ 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
+ });
701
+ } catch (error) {
702
+ results.errors++;
703
+ results.errorDetails.push({
704
+ space: spaceId,
705
+ error: error.message
706
+ });
707
+ resolve();
708
+ }
709
+ })
710
+ );
711
+
712
+ await Promise.all(propagationPromises);
713
+ return results;
714
+ } 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 }]
721
+ };
722
+ }
723
+ }