holosphere 1.1.21 → 1.3.0-alpha3

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
@@ -4,164 +4,115 @@
4
4
  */
5
5
 
6
6
  import * as h3 from 'h3-js';
7
+ import { attachHologramMeta } from './hologram.js';
7
8
 
8
9
  /**
9
- * Creates a federation relationship between two spaces
10
- * Federation is bidirectional by default, and data propagation uses soul references by default.
11
- *
10
+ * Creates a directional federation relationship between two spaces.
11
+ *
12
+ * Directions are stated from spaceId1's perspective:
13
+ * - `lensConfig.inbound`: lenses spaceId1 receives FROM spaceId2.
14
+ * - `lensConfig.outbound`: lenses spaceId1 sends TO spaceId2.
15
+ *
16
+ * When `bidirectional` is true (default), spaceId2's record is mirrored with
17
+ * inverted directions so it agrees with the relationship from its own view.
18
+ *
12
19
  * @param {object} holosphere - The HoloSphere instance
13
20
  * @param {string} spaceId1 - The first space ID
14
21
  * @param {string} spaceId2 - The second space ID
15
22
  * @param {string} [password1] - Optional password for the first space
16
23
  * @param {string} [password2] - Optional password for the second space
17
- * @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications (default: true)
18
- * @param {object} [lensConfig] - Optional lens-specific configuration
19
- * @param {string[]} [lensConfig.federate] - List of lenses to federate (default: all)
20
- * @param {string[]} [lensConfig.notify] - List of lenses to notify (default: all)
24
+ * @param {boolean} [bidirectional=true] - Mirror the relationship onto spaceId2
25
+ * @param {object} [lensConfig] - Lens-direction config from spaceId1's perspective
26
+ * @param {string[]} [lensConfig.inbound] - Lenses spaceId1 receives from spaceId2
27
+ * @param {string[]} [lensConfig.outbound] - Lenses spaceId1 sends to spaceId2
21
28
  * @returns {Promise<boolean>} - True if federation was created successfully
22
29
  */
23
30
  export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true, lensConfig = {}) {
24
31
  if (!spaceId1 || !spaceId2) {
25
32
  throw new Error('federate: Missing required space IDs');
26
33
  }
27
-
28
- // Prevent self-federation
29
34
  if (spaceId1 === spaceId2) {
30
35
  throw new Error('Cannot federate a space with itself');
31
36
  }
32
37
 
33
- // Validate lens configuration
34
- const { federate = [], notify = [] } = lensConfig;
35
- if (!Array.isArray(federate) || !Array.isArray(notify)) {
36
- throw new Error('federate: lensConfig.federate and lensConfig.notify must be arrays');
38
+ const { inbound = [], outbound = [] } = lensConfig;
39
+ if (!Array.isArray(inbound) || !Array.isArray(outbound)) {
40
+ throw new Error('federate: lensConfig.inbound and lensConfig.outbound must be arrays');
37
41
  }
38
42
 
39
- // Use the provided lens configurations directly
40
- const federateLenses = federate;
41
- const notifyLenses = notify;
42
-
43
- try {
44
- // Get or create federation info for first space (A)
45
- let fedInfo1 = null;
43
+ const applyDirection = (fedInfo, partnerId, partnerInbound, partnerOutbound) => {
44
+ if (!fedInfo.federated) fedInfo.federated = [];
45
+ if (!fedInfo.inbound) fedInfo.inbound = [];
46
+ if (!fedInfo.outbound) fedInfo.outbound = [];
47
+ if (!fedInfo.lensConfig) fedInfo.lensConfig = {};
46
48
 
47
- try {
48
- fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
49
- } catch (error) {
49
+ // `federated` is the canonical list — partner is recorded regardless of
50
+ // whether any lenses flow yet.
51
+ if (!fedInfo.federated.includes(partnerId)) {
52
+ fedInfo.federated.push(partnerId);
50
53
  }
51
54
 
52
- if (fedInfo1 == null) {
53
- fedInfo1 = {
54
- id: spaceId1,
55
- name: spaceId1,
56
- federation: [],
57
- notify: [],
58
- lensConfig: {}, // New field for lens-specific settings
59
- timestamp: Date.now()
60
- };
61
- }
62
-
63
- // Ensure arrays and lensConfig exist
64
- if (!fedInfo1.federation) fedInfo1.federation = [];
65
- if (!fedInfo1.notify) fedInfo1.notify = [];
66
- if (!fedInfo1.lensConfig) fedInfo1.lensConfig = {};
55
+ const hasInbound = partnerInbound.length > 0;
56
+ const hasOutbound = partnerOutbound.length > 0;
67
57
 
68
- // Add space2 to space1's federation list if not already present
69
- if (!fedInfo1.federation.includes(spaceId2)) {
70
- fedInfo1.federation.push(spaceId2);
58
+ if (hasInbound && !fedInfo.inbound.includes(partnerId)) {
59
+ fedInfo.inbound.push(partnerId);
60
+ } else if (!hasInbound) {
61
+ fedInfo.inbound = fedInfo.inbound.filter(id => id !== partnerId);
71
62
  }
72
-
73
- // Add space2 to space1's notify list if not already present
74
- if (!fedInfo1.notify.includes(spaceId2)) {
75
- fedInfo1.notify.push(spaceId2);
63
+ if (hasOutbound && !fedInfo.outbound.includes(partnerId)) {
64
+ fedInfo.outbound.push(partnerId);
65
+ } else if (!hasOutbound) {
66
+ fedInfo.outbound = fedInfo.outbound.filter(id => id !== partnerId);
76
67
  }
77
68
 
78
- // Store lens configuration for space2
79
- const newLensConfigsForSpace1 = { ...(fedInfo1.lensConfig || {}) }; // Shallow copy existing lensConfigs for space1
80
- newLensConfigsForSpace1[spaceId2] = { // Add/update config for the target spaceId2
81
- federate: [...federateLenses], // federateLenses & notifyLenses are from the main lensConfig parameter
82
- notify: [...notifyLenses],
69
+ fedInfo.lensConfig[partnerId] = {
70
+ inbound: [...partnerInbound],
71
+ outbound: [...partnerOutbound],
83
72
  timestamp: Date.now()
84
73
  };
85
- fedInfo1.lensConfig = newLensConfigsForSpace1; // Assign the new/modified object back to fedInfo1.lensConfig
74
+ fedInfo.timestamp = Date.now();
75
+ };
86
76
 
87
- // Update timestamp
88
- fedInfo1.timestamp = Date.now();
77
+ try {
78
+ // Space 1: directions as given.
79
+ let fedInfo1 = null;
80
+ try { fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1); } catch {}
81
+ if (fedInfo1 == null) {
82
+ fedInfo1 = {
83
+ id: spaceId1, name: spaceId1,
84
+ federated: [], inbound: [], outbound: [],
85
+ lensConfig: {}, partnerNames: {},
86
+ timestamp: Date.now()
87
+ };
88
+ }
89
+ applyDirection(fedInfo1, spaceId2, inbound, outbound);
89
90
 
90
- // Save updated federation info for space1
91
91
  try {
92
92
  await holosphere.putGlobal('federation', fedInfo1, password1);
93
93
  } catch (error) {
94
94
  throw new Error(`Failed to create federation: ${error.message}`);
95
95
  }
96
96
 
97
- // If bidirectional is true, handle space2 (B) as well
98
- {
97
+ // Space 2 (mirrored): directions are inverted from its perspective.
98
+ if (bidirectional) {
99
99
  let fedInfo2 = null;
100
- try {
101
- fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
102
- } catch (error) {
103
- }
104
-
100
+ try { fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2); } catch {}
105
101
  if (fedInfo2 == null) {
106
102
  fedInfo2 = {
107
- id: spaceId2,
108
- name: spaceId2,
109
- federation: [],
110
- notify: [],
111
- lensConfig: {}, // New field for lens-specific settings
103
+ id: spaceId2, name: spaceId2,
104
+ federated: [], inbound: [], outbound: [],
105
+ lensConfig: {}, partnerNames: {},
112
106
  timestamp: Date.now()
113
107
  };
114
108
  }
109
+ // From spaceId2's view, spaceId1's outbound lenses arrive as inbound,
110
+ // and spaceId1's inbound lenses go out as outbound.
111
+ applyDirection(fedInfo2, spaceId1, outbound, inbound);
115
112
 
116
- // Ensure arrays and lensConfig exist
117
- if (!fedInfo2.federation) fedInfo2.federation = [];
118
- if (!fedInfo2.notify) fedInfo2.notify = [];
119
- if (!fedInfo2.lensConfig) fedInfo2.lensConfig = {};
120
-
121
- // Add space1 to space2's federation list if bidirectional
122
- if (bidirectional && !fedInfo2.federation.includes(spaceId1)) {
123
- fedInfo2.federation.push(spaceId1);
124
- }
125
-
126
- // Add space1 to space2's notify list if not already present
127
- if (!fedInfo2.notify.includes(spaceId1)) {
128
- fedInfo2.notify.push(spaceId1);
129
- }
130
-
131
- // Store lens configuration for space1
132
- fedInfo2.lensConfig[spaceId1] = {
133
- federate: bidirectional ? [...federateLenses] : [], // Create a copy of the array
134
- notify: bidirectional ? [...notifyLenses] : [], // Create a copy of the array
135
- timestamp: Date.now()
136
- };
137
-
138
- // Update timestamp
139
- fedInfo2.timestamp = Date.now();
140
-
141
- // Save updated federation info for space2
142
113
  try {
143
114
  await holosphere.putGlobal('federation', fedInfo2, password2);
144
- } catch (error) {
145
- }
146
- }
147
-
148
- // Create federation metadata record
149
- const federationMeta = {
150
- id: `${spaceId1}_${spaceId2}`,
151
- space1: spaceId1,
152
- space2: spaceId2,
153
- created: Date.now(),
154
- status: 'active',
155
- bidirectional: bidirectional,
156
- lensConfig: {
157
- federate: [...federateLenses], // Create a copy of the array
158
- notify: [...notifyLenses] // Create a copy of the array
159
- }
160
- };
161
-
162
- try {
163
- await holosphere.putGlobal('federationMeta', federationMeta);
164
- } catch (error) {
115
+ } catch {}
165
116
  }
166
117
 
167
118
  return true;
@@ -198,8 +149,8 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
198
149
  const subscriptions = [];
199
150
  let lastNotificationTime = {};
200
151
 
201
- if (fedInfo.federation && fedInfo.federation.length > 0) {
202
- for (const federatedSpace of fedInfo.federation) {
152
+ if (fedInfo.inbound && fedInfo.inbound.length > 0) {
153
+ for (const federatedSpace of fedInfo.inbound) {
203
154
  // For each lens specified (or all if '*')
204
155
  for (const lens of lenses) {
205
156
  try {
@@ -283,7 +234,7 @@ export async function getFederation(holosphere, spaceId, password = null) {
283
234
  * @param {string} spaceId - The ID of the source space.
284
235
  * @param {string} targetSpaceId - The ID of the target space in the federation link.
285
236
  * @param {string} [password] - Optional password for the source space.
286
- * @returns {Promise<object|null>} - An object with 'federate' and 'notify' arrays, or null if not found.
237
+ * @returns {Promise<object|null>} - An object with 'inbound' and 'outbound' arrays, or null if not found.
287
238
  */
288
239
  export async function getFederatedConfig(holosphere, spaceId, targetSpaceId, password = null) {
289
240
  if (!holosphere || !spaceId || !targetSpaceId) {
@@ -295,11 +246,11 @@ export async function getFederatedConfig(holosphere, spaceId, targetSpaceId, pas
295
246
 
296
247
  if (fedInfo && fedInfo.lensConfig && fedInfo.lensConfig[targetSpaceId]) {
297
248
  return {
298
- federate: fedInfo.lensConfig[targetSpaceId].federate || [],
299
- notify: fedInfo.lensConfig[targetSpaceId].notify || []
249
+ inbound: fedInfo.lensConfig[targetSpaceId].inbound || [],
250
+ outbound: fedInfo.lensConfig[targetSpaceId].outbound || []
300
251
  };
301
252
  }
302
- return null; // Or return an empty config: { federate: [], notify: [] }
253
+ return null;
303
254
  } catch (error) {
304
255
  console.error(`Error getting federated config for ${spaceId} -> ${targetSpaceId}: ${error.message}`);
305
256
  throw error;
@@ -332,53 +283,64 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
332
283
  }
333
284
 
334
285
  if (!fedInfo1) {
335
- // If fedInfo1 doesn't exist, log and proceed to metadata cleanup.
336
286
  console.warn(`No federation info found for ${spaceId1}. Skipping its update.`);
337
287
  } else {
338
- // Ensure arrays exist
339
- if (!fedInfo1.federation) fedInfo1.federation = [];
340
- if (!fedInfo1.notify) fedInfo1.notify = [];
341
-
342
- // Update first space federation info - remove from both federation and notify arrays
343
- const originalFederationLength = fedInfo1.federation.length;
344
- const originalNotifyLength = fedInfo1.notify.length;
345
-
346
- fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
347
- fedInfo1.notify = fedInfo1.notify.filter(id => id !== spaceId2);
288
+ if (!fedInfo1.federated) fedInfo1.federated = [];
289
+ if (!fedInfo1.inbound) fedInfo1.inbound = [];
290
+ if (!fedInfo1.outbound) fedInfo1.outbound = [];
291
+ if (!fedInfo1.lensConfig) fedInfo1.lensConfig = {};
292
+ if (!fedInfo1.partnerNames) fedInfo1.partnerNames = {};
293
+
294
+ const beforeIn = fedInfo1.inbound.length;
295
+ const beforeOut = fedInfo1.outbound.length;
296
+
297
+ fedInfo1.federated = fedInfo1.federated.filter(id => id !== spaceId2);
298
+ fedInfo1.inbound = fedInfo1.inbound.filter(id => id !== spaceId2);
299
+ fedInfo1.outbound = fedInfo1.outbound.filter(id => id !== spaceId2);
300
+ delete fedInfo1.lensConfig[spaceId2];
301
+ delete fedInfo1.partnerNames[spaceId2];
348
302
  fedInfo1.timestamp = Date.now();
349
-
350
- console.log(`Unfederate: Removed ${spaceId2} from ${spaceId1}: federation ${originalFederationLength} -> ${fedInfo1.federation.length}, notify ${originalNotifyLength} -> ${fedInfo1.notify.length}`);
351
-
303
+
304
+ console.log(`Unfederate: removed ${spaceId2} from ${spaceId1}: inbound ${beforeIn} -> ${fedInfo1.inbound.length}, outbound ${beforeOut} -> ${fedInfo1.outbound.length}`);
305
+
352
306
  try {
353
307
  await holosphere.putGlobal('federation', fedInfo1, password1);
354
308
  } catch (error) {
355
309
  console.error(`Failed to update fedInfo1 for ${spaceId1} during unfederate: ${error.message}`);
356
- throw error; // RE-THROW to signal failure
310
+ throw error;
357
311
  }
358
312
  }
359
313
 
360
- // Update second space federation info (remove spaceId1 from spaceId2's notify list)
361
- // This part is usually for full bidirectional unfederation cleanup.
362
- // The original code only did this if password2 was provided.
363
- if (password2) { // Retaining original condition for this block
314
+ // Mirror the removal on space2 if password provided.
315
+ if (password2) {
364
316
  let fedInfo2 = null;
365
317
  try {
366
318
  fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
367
319
  } catch (error) {
368
320
  console.error(`Error getting fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
369
321
  }
370
-
371
- if (!fedInfo2 || !fedInfo2.notify) {
372
- console.warn(`No notify array found for ${spaceId2} or fedInfo2 is null. Skipping its update.`);
322
+
323
+ if (!fedInfo2) {
324
+ console.warn(`No federation info found for ${spaceId2}. Skipping its update.`);
373
325
  } else {
374
- fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
326
+ if (!fedInfo2.federated) fedInfo2.federated = [];
327
+ if (!fedInfo2.inbound) fedInfo2.inbound = [];
328
+ if (!fedInfo2.outbound) fedInfo2.outbound = [];
329
+ if (!fedInfo2.lensConfig) fedInfo2.lensConfig = {};
330
+ if (!fedInfo2.partnerNames) fedInfo2.partnerNames = {};
331
+
332
+ fedInfo2.federated = fedInfo2.federated.filter(id => id !== spaceId1);
333
+ fedInfo2.inbound = fedInfo2.inbound.filter(id => id !== spaceId1);
334
+ fedInfo2.outbound = fedInfo2.outbound.filter(id => id !== spaceId1);
335
+ delete fedInfo2.lensConfig[spaceId1];
336
+ delete fedInfo2.partnerNames[spaceId1];
375
337
  fedInfo2.timestamp = Date.now();
376
-
338
+
377
339
  try {
378
340
  await holosphere.putGlobal('federation', fedInfo2, password2);
379
341
  } catch (error) {
380
342
  console.error(`Failed to update fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
381
- throw error; // RE-THROW to signal failure
343
+ throw error;
382
344
  }
383
345
  }
384
346
  }
@@ -409,14 +371,15 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
409
371
  }
410
372
 
411
373
  /**
412
- * Removes a notification relationship between two spaces
413
- * This removes spaceId2 from the notify list of spaceId1
414
- *
374
+ * Stops outbound propagation from spaceId1 to spaceId2.
375
+ * Removes spaceId2 from spaceId1's `outbound` list and clears the outbound
376
+ * lens config for that partner. Inbound subscriptions are untouched.
377
+ *
415
378
  * @param {object} holosphere - The HoloSphere instance
416
- * @param {string} spaceId1 - The space to modify (remove from its notify list)
417
- * @param {string} spaceId2 - The space to be removed from notifications
379
+ * @param {string} spaceId1 - The space to modify
380
+ * @param {string} spaceId2 - The partner to stop sending to
418
381
  * @param {string} [password1] - Optional password for the first space
419
- * @returns {Promise<boolean>} - True if notification was removed successfully
382
+ * @returns {Promise<boolean>} - True if outbound was removed
420
383
  */
421
384
  export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = null) {
422
385
  if (!spaceId1 || !spaceId2) {
@@ -424,29 +387,22 @@ export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = n
424
387
  }
425
388
 
426
389
  try {
427
- // Get federation info for space
428
- let fedInfo = await holosphere.getGlobal('federation', spaceId1, password1);
429
-
390
+ const fedInfo = await holosphere.getGlobal('federation', spaceId1, password1);
430
391
  if (!fedInfo) {
431
392
  throw new Error(`No federation info found for ${spaceId1}`);
432
393
  }
394
+ if (!fedInfo.outbound) fedInfo.outbound = [];
433
395
 
434
- // Ensure notify array exists
435
- if (!fedInfo.notify) fedInfo.notify = [];
436
-
437
- // Remove space2 from space1's notify list if present
438
- if (fedInfo.notify.includes(spaceId2)) {
439
- fedInfo.notify = fedInfo.notify.filter(id => id !== spaceId2);
440
-
441
- // Update timestamp
442
- fedInfo.timestamp = Date.now();
443
-
444
- // Save updated federation info
445
- await holosphere.putGlobal('federation', fedInfo, password1);
446
- return true;
447
- } else {
448
- return false;
396
+ if (!fedInfo.outbound.includes(spaceId2)) return false;
397
+
398
+ fedInfo.outbound = fedInfo.outbound.filter(id => id !== spaceId2);
399
+ if (fedInfo.lensConfig?.[spaceId2]) {
400
+ fedInfo.lensConfig[spaceId2].outbound = [];
449
401
  }
402
+ fedInfo.timestamp = Date.now();
403
+
404
+ await holosphere.putGlobal('federation', fedInfo, password1);
405
+ return true;
450
406
  } catch (error) {
451
407
  throw error;
452
408
  }
@@ -515,8 +471,8 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
515
471
  if (includeLocal) {
516
472
  spacesToQuery.push(holon); // Add local holon first
517
473
  }
518
- if (includeFederated && fedInfo && fedInfo.federation && fedInfo.federation.length > 0) {
519
- const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.federation : fedInfo.federation.slice(0, maxFederatedSpaces);
474
+ if (includeFederated && fedInfo && fedInfo.inbound && fedInfo.inbound.length > 0) {
475
+ const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.inbound : fedInfo.inbound.slice(0, maxFederatedSpaces);
520
476
  spacesToQuery = spacesToQuery.concat(federatedSpaces);
521
477
  }
522
478
 
@@ -596,91 +552,45 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
596
552
  console.log(`Original data found via soul path:`, JSON.stringify(originalData));
597
553
 
598
554
  if (originalData) {
599
- // Replace the reference with the original data
600
- result[i] = {
601
- ...originalData,
602
- _federation: {
603
- isReference: true,
604
- resolved: true,
605
- soul: item.soul,
606
- timestamp: Date.now()
607
- }
608
- };
555
+ // Replace the reference with the resolved data, attaching
556
+ // the canonical _hologram envelope (single source of truth).
557
+ result[i] = attachHologramMeta(originalData, item.soul);
609
558
  } else {
610
- // Instead of leaving the original reference, create an error object
559
+ // Original data not found keep the id so callers can
560
+ // identify the broken reference, and surface the error.
611
561
  result[i] = {
612
562
  id: item.id,
613
- _federation: {
614
- isReference: true,
615
- resolved: false,
563
+ _hologram: {
564
+ isHologram: false,
616
565
  soul: item.soul,
617
566
  error: 'Referenced data not found',
618
- timestamp: Date.now()
567
+ resolvedAt: Date.now()
619
568
  }
620
569
  };
621
570
  }
622
571
  } else {
623
572
  console.warn(`Soul doesn't match expected format: ${item.soul}`);
624
- // Instead of leaving the original reference, create an error object
625
573
  result[i] = {
626
574
  id: item.id,
627
- _federation: {
628
- isReference: true,
629
- resolved: false,
575
+ _hologram: {
576
+ isHologram: false,
630
577
  soul: item.soul,
631
578
  error: 'Invalid soul format',
632
- timestamp: Date.now()
579
+ resolvedAt: Date.now()
633
580
  }
634
581
  };
635
582
  }
636
583
  } catch (refError) {
637
- // Instead of leaving the original reference, create an error object
638
584
  result[i] = {
639
585
  id: item.id,
640
- _federation: {
641
- isReference: true,
642
- resolved: false,
586
+ _hologram: {
587
+ isHologram: false,
643
588
  soul: item.soul,
644
589
  error: refError.message || 'Error resolving reference',
645
- timestamp: Date.now()
590
+ resolvedAt: Date.now()
646
591
  }
647
592
  };
648
593
  }
649
- }
650
- // For backward compatibility, check for old-style references
651
- else if (item._federation && item._federation.isReference) {
652
- console.log(`Found legacy reference: ${item._federation.origin}/${item._federation.lens}/${item[idField]}`);
653
-
654
- try {
655
- const reference = item._federation;
656
- console.log(`Getting original data from ${reference.origin} / ${reference.lens} / ${item[idField]}`);
657
-
658
- // Get original data
659
- const originalData = await holosphere.get(
660
- reference.origin,
661
- reference.lens,
662
- item[idField],
663
- null,
664
- { resolveReferences: false } // Prevent infinite recursion
665
- );
666
-
667
- console.log(`Original data found:`, JSON.stringify(originalData));
668
-
669
- if (originalData) {
670
- // Add federation information to the resolved data
671
- result[i] = {
672
- ...originalData,
673
- _federation: {
674
- ...reference,
675
- resolved: true,
676
- timestamp: Date.now()
677
- }
678
- };
679
- } else {
680
- console.warn(`Could not resolve legacy reference: original data not found`);
681
- }
682
- } catch (refError) {
683
- }
684
594
  }
685
595
  }
686
596
  }
@@ -790,18 +700,16 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
790
700
  // Get federation info for this holon using getFederation
791
701
  const fedInfo = await getFederation(holosphere, holon, password);
792
702
 
793
- // Only perform federation propagation if there's valid federation info
794
- if (fedInfo && fedInfo.federation && fedInfo.federation.length > 0 && fedInfo.notify && fedInfo.notify.length > 0) {
795
- // Filter federation spaces to those in notify list
796
- let spaces = fedInfo.notify;
797
-
798
- // Further filter by targetSpaces if provided
703
+ // Only propagate if we have outbound partners configured.
704
+ if (fedInfo && fedInfo.outbound && fedInfo.outbound.length > 0) {
705
+ let spaces = fedInfo.outbound;
706
+
799
707
  if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) {
800
708
  spaces = spaces.filter(space => targetSpaces.includes(space));
801
709
  }
802
-
710
+
803
711
  if (spaces.length > 0) {
804
- // Filter spaces based on lens configuration
712
+ // Keep only partners whose outbound lens config includes this lens.
805
713
  spaces = spaces.filter(targetSpace => {
806
714
  const spaceConfig = fedInfo.lensConfig?.[targetSpace];
807
715
  if (!spaceConfig) {
@@ -810,19 +718,13 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
810
718
  return false;
811
719
  }
812
720
 
813
- // Ensure .federate is an array before calling .includes
814
- const federateLenses = Array.isArray(spaceConfig.federate) ? spaceConfig.federate : [];
721
+ const outboundLenses = Array.isArray(spaceConfig.outbound) ? spaceConfig.outbound : [];
722
+ const shouldPropagate = outboundLenses.includes('*') || outboundLenses.includes(lens);
815
723
 
816
- const shouldFederate = federateLenses.includes('*') || federateLenses.includes(lens);
817
-
818
- // Propagation now only depends on the 'federate' list configuration for the lens
819
- const shouldPropagate = shouldFederate;
820
-
821
724
  if (!shouldPropagate) {
822
- result.messages.push(`Propagation of lens '${lens}' to target space ${targetSpace} skipped: lens not in 'federate' configuration.`);
725
+ result.messages.push(`Propagation of lens '${lens}' to target space ${targetSpace} skipped: lens not in 'outbound' configuration.`);
823
726
  result.skipped++;
824
727
  }
825
-
826
728
  return shouldPropagate;
827
729
  });
828
730
 
@@ -1128,82 +1030,76 @@ export async function resetFederation(holosphere, spaceId, password = null, opti
1128
1030
  };
1129
1031
  }
1130
1032
 
1131
- // Store counts for reporting
1132
- result.federatedCount = fedInfo.federation?.length || 0;
1133
- result.notifyCount = fedInfo.notify?.length || 0;
1033
+ const allPartners = fedInfo.federated || [];
1034
+ result.federatedCount = allPartners.length;
1035
+ result.inboundCount = fedInfo.inbound?.length || 0;
1036
+ result.outboundCount = fedInfo.outbound?.length || 0;
1134
1037
 
1135
- // Create empty federation record
1038
+ // Reset federation record to empty.
1136
1039
  const emptyFedInfo = {
1137
1040
  id: spaceId,
1138
1041
  name: spaceName || spaceId,
1139
- federation: [],
1140
- notify: [],
1042
+ federated: [],
1043
+ inbound: [],
1044
+ outbound: [],
1045
+ lensConfig: {},
1046
+ partnerNames: {},
1141
1047
  timestamp: Date.now()
1142
1048
  };
1143
-
1144
- // Update federation record with empty lists
1145
1049
  await holosphere.putGlobal('federation', emptyFedInfo, password);
1146
1050
 
1147
- // Notify federation partners if requested
1148
- if (notifyPartners && fedInfo.federation && fedInfo.federation.length > 0) {
1149
- const updatePromises = fedInfo.federation.map(async (partnerSpace) => {
1051
+ // Tell each partner to drop us from their lists.
1052
+ if (notifyPartners && allPartners.length > 0) {
1053
+ const updatePromises = allPartners.map(async (partnerSpace) => {
1150
1054
  try {
1151
- // Get partner's federation info
1152
1055
  const partnerFedInfo = await getFederation(holosphere, partnerSpace);
1153
-
1154
- if (partnerFedInfo) {
1155
- // Remove this space from partner's federation list
1156
- if (partnerFedInfo.federation) {
1157
- partnerFedInfo.federation = partnerFedInfo.federation.filter(
1158
- id => id !== spaceId.toString()
1159
- );
1160
- }
1161
-
1162
- // Remove this space from partner's notify list
1163
- if (partnerFedInfo.notify) {
1164
- partnerFedInfo.notify = partnerFedInfo.notify.filter(
1165
- id => id !== spaceId.toString()
1166
- );
1167
- }
1168
-
1169
- partnerFedInfo.timestamp = Date.now();
1170
-
1171
- // Save partner's updated federation info
1172
- await holosphere.putGlobal('federation', partnerFedInfo);
1173
- result.partnersNotified++;
1174
- return true;
1056
+ if (!partnerFedInfo) return false;
1057
+
1058
+ const sid = spaceId.toString();
1059
+ if (partnerFedInfo.federated) {
1060
+ partnerFedInfo.federated = partnerFedInfo.federated.filter(id => id !== sid);
1175
1061
  }
1176
- return false;
1062
+ if (partnerFedInfo.inbound) {
1063
+ partnerFedInfo.inbound = partnerFedInfo.inbound.filter(id => id !== sid);
1064
+ }
1065
+ if (partnerFedInfo.outbound) {
1066
+ partnerFedInfo.outbound = partnerFedInfo.outbound.filter(id => id !== sid);
1067
+ }
1068
+ if (partnerFedInfo.lensConfig) {
1069
+ delete partnerFedInfo.lensConfig[sid];
1070
+ }
1071
+ if (partnerFedInfo.partnerNames) {
1072
+ delete partnerFedInfo.partnerNames[sid];
1073
+ }
1074
+ partnerFedInfo.timestamp = Date.now();
1075
+
1076
+ await holosphere.putGlobal('federation', partnerFedInfo);
1077
+ result.partnersNotified++;
1078
+ return true;
1177
1079
  } catch (error) {
1178
- result.errors.push({
1179
- partner: partnerSpace,
1180
- error: error.message
1181
- });
1080
+ result.errors.push({ partner: partnerSpace, error: error.message });
1182
1081
  return false;
1183
1082
  }
1184
1083
  });
1185
-
1084
+
1186
1085
  await Promise.all(updatePromises);
1187
1086
  }
1188
-
1189
- // Update federation metadata records if they exist
1190
- if (fedInfo.federation && fedInfo.federation.length > 0) {
1191
- for (const partnerSpace of fedInfo.federation) {
1192
- try {
1193
- const metaId = `${spaceId}_${partnerSpace}`;
1194
- const altMetaId = `${partnerSpace}_${spaceId}`;
1195
-
1196
- const meta = await holosphere.getGlobal('federationMeta', metaId) ||
1197
- await holosphere.getGlobal('federationMeta', altMetaId);
1198
-
1199
- if (meta) {
1200
- meta.status = 'inactive';
1201
- meta.endedAt = Date.now();
1202
- await holosphere.putGlobal('federationMeta', meta);
1203
- }
1204
- } catch (error) {
1087
+
1088
+ // Mark any federationMeta records inactive.
1089
+ for (const partnerSpace of allPartners) {
1090
+ try {
1091
+ const metaId = `${spaceId}_${partnerSpace}`;
1092
+ const altMetaId = `${partnerSpace}_${spaceId}`;
1093
+
1094
+ const meta = await holosphere.getGlobal('federationMeta', metaId) ||
1095
+ await holosphere.getGlobal('federationMeta', altMetaId);
1096
+
1097
+ if (meta) {
1098
+ meta.status = 'inactive';
1099
+ meta.endedAt = Date.now();
1100
+ await holosphere.putGlobal('federationMeta', meta);
1205
1101
  }
1206
- }
1102
+ } catch {}
1207
1103
  }
1208
1104
 
1209
1105
  result.success = true;