holosphere 1.3.0-alpha0 → 1.3.0-alpha4
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/content.js +147 -65
- package/federation.js +319 -319
- package/global.js +58 -47
- package/hologram.js +38 -14
- package/holosphere-bundle.esm.js +525 -441
- package/holosphere-bundle.js +525 -441
- package/holosphere-bundle.min.js +8 -8
- package/holosphere.d.ts +50 -8
- package/holosphere.js +24 -19
- package/package.json +1 -1
package/federation.js
CHANGED
|
@@ -4,164 +4,144 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as h3 from 'h3-js';
|
|
7
|
+
import { attachHologramMeta } from './hologram.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Look up a holon's display name from its `settings` lens.
|
|
11
|
+
*
|
|
12
|
+
* Returns the `name` field of the holon's settings document, or null if
|
|
13
|
+
* settings are missing or unnamed. Callers should fall back to the bare
|
|
14
|
+
* holon id when this returns null.
|
|
15
|
+
*
|
|
16
|
+
* @param {HoloSphere} holosphere
|
|
17
|
+
* @param {string} space - holon id
|
|
18
|
+
* @returns {Promise<string|null>}
|
|
19
|
+
*/
|
|
20
|
+
async function getHolonName(holosphere, space) {
|
|
21
|
+
if (!holosphere || !space) return null;
|
|
22
|
+
try {
|
|
23
|
+
const settings = await holosphere.get(space, 'settings', space);
|
|
24
|
+
if (!settings) return null;
|
|
25
|
+
if (Array.isArray(settings)) {
|
|
26
|
+
const found = settings.find(s => s && typeof s.name === 'string' && s.name.trim() !== '');
|
|
27
|
+
return found ? found.name : null;
|
|
28
|
+
}
|
|
29
|
+
if (typeof settings.name === 'string' && settings.name.trim() !== '') {
|
|
30
|
+
return settings.name;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a directional federation relationship between two spaces.
|
|
40
|
+
*
|
|
41
|
+
* Directions are stated from spaceId1's perspective:
|
|
42
|
+
* - `lensConfig.inbound`: lenses spaceId1 receives FROM spaceId2.
|
|
43
|
+
* - `lensConfig.outbound`: lenses spaceId1 sends TO spaceId2.
|
|
44
|
+
*
|
|
45
|
+
* When `bidirectional` is true (default), spaceId2's record is mirrored with
|
|
46
|
+
* inverted directions so it agrees with the relationship from its own view.
|
|
47
|
+
*
|
|
12
48
|
* @param {object} holosphere - The HoloSphere instance
|
|
13
49
|
* @param {string} spaceId1 - The first space ID
|
|
14
50
|
* @param {string} spaceId2 - The second space ID
|
|
15
51
|
* @param {string} [password1] - Optional password for the first space
|
|
16
52
|
* @param {string} [password2] - Optional password for the second space
|
|
17
|
-
* @param {boolean} [bidirectional=true] -
|
|
18
|
-
* @param {object} [lensConfig] -
|
|
19
|
-
* @param {string[]} [lensConfig.
|
|
20
|
-
* @param {string[]} [lensConfig.
|
|
53
|
+
* @param {boolean} [bidirectional=true] - Mirror the relationship onto spaceId2
|
|
54
|
+
* @param {object} [lensConfig] - Lens-direction config from spaceId1's perspective
|
|
55
|
+
* @param {string[]} [lensConfig.inbound] - Lenses spaceId1 receives from spaceId2
|
|
56
|
+
* @param {string[]} [lensConfig.outbound] - Lenses spaceId1 sends to spaceId2
|
|
21
57
|
* @returns {Promise<boolean>} - True if federation was created successfully
|
|
22
58
|
*/
|
|
23
59
|
export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true, lensConfig = {}) {
|
|
24
60
|
if (!spaceId1 || !spaceId2) {
|
|
25
61
|
throw new Error('federate: Missing required space IDs');
|
|
26
62
|
}
|
|
27
|
-
|
|
28
|
-
// Prevent self-federation
|
|
29
63
|
if (spaceId1 === spaceId2) {
|
|
30
64
|
throw new Error('Cannot federate a space with itself');
|
|
31
65
|
}
|
|
32
66
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
throw new Error('federate: lensConfig.federate and lensConfig.notify must be arrays');
|
|
67
|
+
const { inbound = [], outbound = [] } = lensConfig;
|
|
68
|
+
if (!Array.isArray(inbound) || !Array.isArray(outbound)) {
|
|
69
|
+
throw new Error('federate: lensConfig.inbound and lensConfig.outbound must be arrays');
|
|
37
70
|
}
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Get or create federation info for first space (A)
|
|
45
|
-
let fedInfo1 = null;
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
}
|
|
72
|
+
const applyDirection = (fedInfo, partnerId, partnerInbound, partnerOutbound) => {
|
|
73
|
+
if (!fedInfo.federated) fedInfo.federated = [];
|
|
74
|
+
if (!fedInfo.inbound) fedInfo.inbound = [];
|
|
75
|
+
if (!fedInfo.outbound) fedInfo.outbound = [];
|
|
76
|
+
if (!fedInfo.lensConfig) fedInfo.lensConfig = {};
|
|
51
77
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
federation: [],
|
|
57
|
-
notify: [],
|
|
58
|
-
lensConfig: {}, // New field for lens-specific settings
|
|
59
|
-
timestamp: Date.now()
|
|
60
|
-
};
|
|
78
|
+
// `federated` is the canonical list — partner is recorded regardless of
|
|
79
|
+
// whether any lenses flow yet.
|
|
80
|
+
if (!fedInfo.federated.includes(partnerId)) {
|
|
81
|
+
fedInfo.federated.push(partnerId);
|
|
61
82
|
}
|
|
62
83
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!fedInfo1.notify) fedInfo1.notify = [];
|
|
66
|
-
if (!fedInfo1.lensConfig) fedInfo1.lensConfig = {};
|
|
84
|
+
const hasInbound = partnerInbound.length > 0;
|
|
85
|
+
const hasOutbound = partnerOutbound.length > 0;
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
if (hasInbound && !fedInfo.inbound.includes(partnerId)) {
|
|
88
|
+
fedInfo.inbound.push(partnerId);
|
|
89
|
+
} else if (!hasInbound) {
|
|
90
|
+
fedInfo.inbound = fedInfo.inbound.filter(id => id !== partnerId);
|
|
71
91
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!
|
|
75
|
-
|
|
92
|
+
if (hasOutbound && !fedInfo.outbound.includes(partnerId)) {
|
|
93
|
+
fedInfo.outbound.push(partnerId);
|
|
94
|
+
} else if (!hasOutbound) {
|
|
95
|
+
fedInfo.outbound = fedInfo.outbound.filter(id => id !== partnerId);
|
|
76
96
|
}
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
federate: [...federateLenses], // federateLenses & notifyLenses are from the main lensConfig parameter
|
|
82
|
-
notify: [...notifyLenses],
|
|
98
|
+
fedInfo.lensConfig[partnerId] = {
|
|
99
|
+
inbound: [...partnerInbound],
|
|
100
|
+
outbound: [...partnerOutbound],
|
|
83
101
|
timestamp: Date.now()
|
|
84
102
|
};
|
|
85
|
-
|
|
103
|
+
fedInfo.timestamp = Date.now();
|
|
104
|
+
};
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
try {
|
|
107
|
+
// Space 1: directions as given.
|
|
108
|
+
let fedInfo1 = null;
|
|
109
|
+
try { fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1); } catch {}
|
|
110
|
+
if (fedInfo1 == null) {
|
|
111
|
+
fedInfo1 = {
|
|
112
|
+
id: spaceId1, name: spaceId1,
|
|
113
|
+
federated: [], inbound: [], outbound: [],
|
|
114
|
+
lensConfig: {}, partnerNames: {},
|
|
115
|
+
timestamp: Date.now()
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
applyDirection(fedInfo1, spaceId2, inbound, outbound);
|
|
89
119
|
|
|
90
|
-
// Save updated federation info for space1
|
|
91
120
|
try {
|
|
92
121
|
await holosphere.putGlobal('federation', fedInfo1, password1);
|
|
93
122
|
} catch (error) {
|
|
94
123
|
throw new Error(`Failed to create federation: ${error.message}`);
|
|
95
124
|
}
|
|
96
125
|
|
|
97
|
-
//
|
|
98
|
-
{
|
|
126
|
+
// Space 2 (mirrored): directions are inverted from its perspective.
|
|
127
|
+
if (bidirectional) {
|
|
99
128
|
let fedInfo2 = null;
|
|
100
|
-
try {
|
|
101
|
-
fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
}
|
|
104
|
-
|
|
129
|
+
try { fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2); } catch {}
|
|
105
130
|
if (fedInfo2 == null) {
|
|
106
131
|
fedInfo2 = {
|
|
107
|
-
id: spaceId2,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
notify: [],
|
|
111
|
-
lensConfig: {}, // New field for lens-specific settings
|
|
132
|
+
id: spaceId2, name: spaceId2,
|
|
133
|
+
federated: [], inbound: [], outbound: [],
|
|
134
|
+
lensConfig: {}, partnerNames: {},
|
|
112
135
|
timestamp: Date.now()
|
|
113
136
|
};
|
|
114
137
|
}
|
|
138
|
+
// From spaceId2's view, spaceId1's outbound lenses arrive as inbound,
|
|
139
|
+
// and spaceId1's inbound lenses go out as outbound.
|
|
140
|
+
applyDirection(fedInfo2, spaceId1, outbound, inbound);
|
|
115
141
|
|
|
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
142
|
try {
|
|
143
143
|
await holosphere.putGlobal('federation', fedInfo2, password2);
|
|
144
|
-
} catch
|
|
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) {
|
|
144
|
+
} catch {}
|
|
165
145
|
}
|
|
166
146
|
|
|
167
147
|
return true;
|
|
@@ -198,8 +178,19 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
|
|
|
198
178
|
const subscriptions = [];
|
|
199
179
|
let lastNotificationTime = {};
|
|
200
180
|
|
|
201
|
-
|
|
202
|
-
|
|
181
|
+
// Cache partner display names once at setup. Each callback firing for
|
|
182
|
+
// a given partner uses the same name without re-reading settings.
|
|
183
|
+
const partnerNames = new Map();
|
|
184
|
+
|
|
185
|
+
if (fedInfo.inbound && fedInfo.inbound.length > 0) {
|
|
186
|
+
await Promise.all(
|
|
187
|
+
fedInfo.inbound.map(async space => {
|
|
188
|
+
const name = await getHolonName(holosphere, space);
|
|
189
|
+
partnerNames.set(space, name);
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
for (const federatedSpace of fedInfo.inbound) {
|
|
203
194
|
// For each lens specified (or all if '*')
|
|
204
195
|
for (const lens of lenses) {
|
|
205
196
|
try {
|
|
@@ -207,27 +198,34 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
|
|
|
207
198
|
try {
|
|
208
199
|
// Skip if data is missing or not from federated space
|
|
209
200
|
if (!data || !data.id) return;
|
|
210
|
-
|
|
201
|
+
|
|
211
202
|
// Apply throttling if configured
|
|
212
203
|
const now = Date.now();
|
|
213
204
|
const key = `${federatedSpace}_${lens}_${data.id}`;
|
|
214
|
-
|
|
205
|
+
|
|
215
206
|
if (throttle > 0) {
|
|
216
|
-
if (lastNotificationTime[key] &&
|
|
207
|
+
if (lastNotificationTime[key] &&
|
|
217
208
|
(now - lastNotificationTime[key]) < throttle) {
|
|
218
209
|
return; // Skip this notification (throttled)
|
|
219
210
|
}
|
|
220
211
|
lastNotificationTime[key] = now;
|
|
221
212
|
}
|
|
222
|
-
|
|
223
|
-
// Add federation metadata if not present
|
|
224
|
-
|
|
225
|
-
|
|
213
|
+
|
|
214
|
+
// Add federation metadata if not present.
|
|
215
|
+
// Use the canonical `_federation` envelope so it
|
|
216
|
+
// matches what propagate() and getFederated()
|
|
217
|
+
// produce, and what consumers (UI badges, etc.)
|
|
218
|
+
// already check for.
|
|
219
|
+
if (!data._federation) {
|
|
220
|
+
const partnerName = partnerNames.get(federatedSpace);
|
|
221
|
+
data._federation = {
|
|
226
222
|
origin: federatedSpace,
|
|
227
|
-
|
|
223
|
+
sourceLens: lens,
|
|
224
|
+
timestamp: now,
|
|
225
|
+
...(partnerName ? { originName: partnerName } : {})
|
|
228
226
|
};
|
|
229
227
|
}
|
|
230
|
-
|
|
228
|
+
|
|
231
229
|
// Execute callback with the data
|
|
232
230
|
await callback(data, federatedSpace, lens);
|
|
233
231
|
} catch (error) {
|
|
@@ -283,7 +281,7 @@ export async function getFederation(holosphere, spaceId, password = null) {
|
|
|
283
281
|
* @param {string} spaceId - The ID of the source space.
|
|
284
282
|
* @param {string} targetSpaceId - The ID of the target space in the federation link.
|
|
285
283
|
* @param {string} [password] - Optional password for the source space.
|
|
286
|
-
* @returns {Promise<object|null>} - An object with '
|
|
284
|
+
* @returns {Promise<object|null>} - An object with 'inbound' and 'outbound' arrays, or null if not found.
|
|
287
285
|
*/
|
|
288
286
|
export async function getFederatedConfig(holosphere, spaceId, targetSpaceId, password = null) {
|
|
289
287
|
if (!holosphere || !spaceId || !targetSpaceId) {
|
|
@@ -295,11 +293,11 @@ export async function getFederatedConfig(holosphere, spaceId, targetSpaceId, pas
|
|
|
295
293
|
|
|
296
294
|
if (fedInfo && fedInfo.lensConfig && fedInfo.lensConfig[targetSpaceId]) {
|
|
297
295
|
return {
|
|
298
|
-
|
|
299
|
-
|
|
296
|
+
inbound: fedInfo.lensConfig[targetSpaceId].inbound || [],
|
|
297
|
+
outbound: fedInfo.lensConfig[targetSpaceId].outbound || []
|
|
300
298
|
};
|
|
301
299
|
}
|
|
302
|
-
return null;
|
|
300
|
+
return null;
|
|
303
301
|
} catch (error) {
|
|
304
302
|
console.error(`Error getting federated config for ${spaceId} -> ${targetSpaceId}: ${error.message}`);
|
|
305
303
|
throw error;
|
|
@@ -332,53 +330,64 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
|
|
|
332
330
|
}
|
|
333
331
|
|
|
334
332
|
if (!fedInfo1) {
|
|
335
|
-
// If fedInfo1 doesn't exist, log and proceed to metadata cleanup.
|
|
336
333
|
console.warn(`No federation info found for ${spaceId1}. Skipping its update.`);
|
|
337
334
|
} else {
|
|
338
|
-
|
|
339
|
-
if (!fedInfo1.
|
|
340
|
-
if (!fedInfo1.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
fedInfo1.
|
|
335
|
+
if (!fedInfo1.federated) fedInfo1.federated = [];
|
|
336
|
+
if (!fedInfo1.inbound) fedInfo1.inbound = [];
|
|
337
|
+
if (!fedInfo1.outbound) fedInfo1.outbound = [];
|
|
338
|
+
if (!fedInfo1.lensConfig) fedInfo1.lensConfig = {};
|
|
339
|
+
if (!fedInfo1.partnerNames) fedInfo1.partnerNames = {};
|
|
340
|
+
|
|
341
|
+
const beforeIn = fedInfo1.inbound.length;
|
|
342
|
+
const beforeOut = fedInfo1.outbound.length;
|
|
343
|
+
|
|
344
|
+
fedInfo1.federated = fedInfo1.federated.filter(id => id !== spaceId2);
|
|
345
|
+
fedInfo1.inbound = fedInfo1.inbound.filter(id => id !== spaceId2);
|
|
346
|
+
fedInfo1.outbound = fedInfo1.outbound.filter(id => id !== spaceId2);
|
|
347
|
+
delete fedInfo1.lensConfig[spaceId2];
|
|
348
|
+
delete fedInfo1.partnerNames[spaceId2];
|
|
348
349
|
fedInfo1.timestamp = Date.now();
|
|
349
|
-
|
|
350
|
-
console.log(`Unfederate:
|
|
351
|
-
|
|
350
|
+
|
|
351
|
+
console.log(`Unfederate: removed ${spaceId2} from ${spaceId1}: inbound ${beforeIn} -> ${fedInfo1.inbound.length}, outbound ${beforeOut} -> ${fedInfo1.outbound.length}`);
|
|
352
|
+
|
|
352
353
|
try {
|
|
353
354
|
await holosphere.putGlobal('federation', fedInfo1, password1);
|
|
354
355
|
} catch (error) {
|
|
355
356
|
console.error(`Failed to update fedInfo1 for ${spaceId1} during unfederate: ${error.message}`);
|
|
356
|
-
throw error;
|
|
357
|
+
throw error;
|
|
357
358
|
}
|
|
358
359
|
}
|
|
359
360
|
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
// The original code only did this if password2 was provided.
|
|
363
|
-
if (password2) { // Retaining original condition for this block
|
|
361
|
+
// Mirror the removal on space2 if password provided.
|
|
362
|
+
if (password2) {
|
|
364
363
|
let fedInfo2 = null;
|
|
365
364
|
try {
|
|
366
365
|
fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
|
|
367
366
|
} catch (error) {
|
|
368
367
|
console.error(`Error getting fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
|
|
369
368
|
}
|
|
370
|
-
|
|
371
|
-
if (!fedInfo2
|
|
372
|
-
console.warn(`No
|
|
369
|
+
|
|
370
|
+
if (!fedInfo2) {
|
|
371
|
+
console.warn(`No federation info found for ${spaceId2}. Skipping its update.`);
|
|
373
372
|
} else {
|
|
374
|
-
fedInfo2.
|
|
373
|
+
if (!fedInfo2.federated) fedInfo2.federated = [];
|
|
374
|
+
if (!fedInfo2.inbound) fedInfo2.inbound = [];
|
|
375
|
+
if (!fedInfo2.outbound) fedInfo2.outbound = [];
|
|
376
|
+
if (!fedInfo2.lensConfig) fedInfo2.lensConfig = {};
|
|
377
|
+
if (!fedInfo2.partnerNames) fedInfo2.partnerNames = {};
|
|
378
|
+
|
|
379
|
+
fedInfo2.federated = fedInfo2.federated.filter(id => id !== spaceId1);
|
|
380
|
+
fedInfo2.inbound = fedInfo2.inbound.filter(id => id !== spaceId1);
|
|
381
|
+
fedInfo2.outbound = fedInfo2.outbound.filter(id => id !== spaceId1);
|
|
382
|
+
delete fedInfo2.lensConfig[spaceId1];
|
|
383
|
+
delete fedInfo2.partnerNames[spaceId1];
|
|
375
384
|
fedInfo2.timestamp = Date.now();
|
|
376
|
-
|
|
385
|
+
|
|
377
386
|
try {
|
|
378
387
|
await holosphere.putGlobal('federation', fedInfo2, password2);
|
|
379
388
|
} catch (error) {
|
|
380
389
|
console.error(`Failed to update fedInfo2 for ${spaceId2} during unfederate: ${error.message}`);
|
|
381
|
-
throw error;
|
|
390
|
+
throw error;
|
|
382
391
|
}
|
|
383
392
|
}
|
|
384
393
|
}
|
|
@@ -409,14 +418,15 @@ export async function unfederate(holosphere, spaceId1, spaceId2, password1 = nul
|
|
|
409
418
|
}
|
|
410
419
|
|
|
411
420
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
421
|
+
* Stops outbound propagation from spaceId1 to spaceId2.
|
|
422
|
+
* Removes spaceId2 from spaceId1's `outbound` list and clears the outbound
|
|
423
|
+
* lens config for that partner. Inbound subscriptions are untouched.
|
|
424
|
+
*
|
|
415
425
|
* @param {object} holosphere - The HoloSphere instance
|
|
416
|
-
* @param {string} spaceId1 - The space to modify
|
|
417
|
-
* @param {string} spaceId2 - The
|
|
426
|
+
* @param {string} spaceId1 - The space to modify
|
|
427
|
+
* @param {string} spaceId2 - The partner to stop sending to
|
|
418
428
|
* @param {string} [password1] - Optional password for the first space
|
|
419
|
-
* @returns {Promise<boolean>} - True if
|
|
429
|
+
* @returns {Promise<boolean>} - True if outbound was removed
|
|
420
430
|
*/
|
|
421
431
|
export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = null) {
|
|
422
432
|
if (!spaceId1 || !spaceId2) {
|
|
@@ -424,29 +434,22 @@ export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = n
|
|
|
424
434
|
}
|
|
425
435
|
|
|
426
436
|
try {
|
|
427
|
-
|
|
428
|
-
let fedInfo = await holosphere.getGlobal('federation', spaceId1, password1);
|
|
429
|
-
|
|
437
|
+
const fedInfo = await holosphere.getGlobal('federation', spaceId1, password1);
|
|
430
438
|
if (!fedInfo) {
|
|
431
439
|
throw new Error(`No federation info found for ${spaceId1}`);
|
|
432
440
|
}
|
|
441
|
+
if (!fedInfo.outbound) fedInfo.outbound = [];
|
|
433
442
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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;
|
|
443
|
+
if (!fedInfo.outbound.includes(spaceId2)) return false;
|
|
444
|
+
|
|
445
|
+
fedInfo.outbound = fedInfo.outbound.filter(id => id !== spaceId2);
|
|
446
|
+
if (fedInfo.lensConfig?.[spaceId2]) {
|
|
447
|
+
fedInfo.lensConfig[spaceId2].outbound = [];
|
|
449
448
|
}
|
|
449
|
+
fedInfo.timestamp = Date.now();
|
|
450
|
+
|
|
451
|
+
await holosphere.putGlobal('federation', fedInfo, password1);
|
|
452
|
+
return true;
|
|
450
453
|
} catch (error) {
|
|
451
454
|
throw error;
|
|
452
455
|
}
|
|
@@ -515,15 +518,42 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
|
|
|
515
518
|
if (includeLocal) {
|
|
516
519
|
spacesToQuery.push(holon); // Add local holon first
|
|
517
520
|
}
|
|
518
|
-
if (includeFederated && fedInfo && fedInfo.
|
|
519
|
-
const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.
|
|
521
|
+
if (includeFederated && fedInfo && fedInfo.inbound && fedInfo.inbound.length > 0) {
|
|
522
|
+
const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.inbound : fedInfo.inbound.slice(0, maxFederatedSpaces);
|
|
520
523
|
spacesToQuery = spacesToQuery.concat(federatedSpaces);
|
|
521
524
|
}
|
|
522
525
|
|
|
526
|
+
// Resolve display names for federated partner spaces in parallel, so
|
|
527
|
+
// every tagged item can carry the holon's name. Compute once per call.
|
|
528
|
+
const remoteSpaces = spacesToQuery.filter(s => s !== holon);
|
|
529
|
+
const spaceNames = new Map();
|
|
530
|
+
await Promise.all(
|
|
531
|
+
remoteSpaces.map(async space => {
|
|
532
|
+
const name = await getHolonName(holosphere, space);
|
|
533
|
+
spaceNames.set(space, name);
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// Tag items pulled from a federated partner with the space they came
|
|
538
|
+
// from (and its resolved display name, if any). Local items are left
|
|
539
|
+
// untouched so consumers can distinguish own vs. external by absence
|
|
540
|
+
// or presence of `_federation`.
|
|
541
|
+
const tagWithSource = (item, space) => {
|
|
542
|
+
if (!item || space === holon) return item;
|
|
543
|
+
const originName = spaceNames.get(space);
|
|
544
|
+
const fed = {
|
|
545
|
+
...(item._federation || {}),
|
|
546
|
+
origin: space,
|
|
547
|
+
sourceLens: lens
|
|
548
|
+
};
|
|
549
|
+
if (originName) fed.originName = originName;
|
|
550
|
+
return { ...item, _federation: fed };
|
|
551
|
+
};
|
|
552
|
+
|
|
523
553
|
// Fetch data from all relevant spaces
|
|
524
554
|
for (const currentSpace of spacesToQuery) {
|
|
525
555
|
if (queryIds && Array.isArray(queryIds)) {
|
|
526
|
-
// --- Fetch specific IDs using holosphere.get ---
|
|
556
|
+
// --- Fetch specific IDs using holosphere.get ---
|
|
527
557
|
console.log(`Fetching specific IDs from ${currentSpace}: ${queryIds.join(', ')}`);
|
|
528
558
|
for (const itemId of queryIds) {
|
|
529
559
|
if (fetchedItems.has(itemId)) continue; // Skip if already fetched
|
|
@@ -531,7 +561,7 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
|
|
|
531
561
|
holosphere.get(currentSpace, lens, itemId)
|
|
532
562
|
.then(item => {
|
|
533
563
|
if (item) {
|
|
534
|
-
fetchedItems.set(itemId, item);
|
|
564
|
+
fetchedItems.set(itemId, tagWithSource(item, currentSpace));
|
|
535
565
|
}
|
|
536
566
|
})
|
|
537
567
|
.catch(err => console.warn(`Error fetching item ${itemId} from ${currentSpace}: ${err.message}`))
|
|
@@ -548,7 +578,7 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
|
|
|
548
578
|
.then(items => {
|
|
549
579
|
for (const item of items) {
|
|
550
580
|
if (item && item[idField] && !fetchedItems.has(item[idField])) {
|
|
551
|
-
fetchedItems.set(item[idField], item);
|
|
581
|
+
fetchedItems.set(item[idField], tagWithSource(item, currentSpace));
|
|
552
582
|
}
|
|
553
583
|
}
|
|
554
584
|
})
|
|
@@ -596,91 +626,64 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
|
|
|
596
626
|
console.log(`Original data found via soul path:`, JSON.stringify(originalData));
|
|
597
627
|
|
|
598
628
|
if (originalData) {
|
|
599
|
-
// Replace the reference with the
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
629
|
+
// Replace the reference with the resolved data, attaching
|
|
630
|
+
// the canonical _hologram envelope (single source of truth).
|
|
631
|
+
const withMeta = attachHologramMeta(originalData, item.soul);
|
|
632
|
+
// Stamp the source holon's display name so consumers
|
|
633
|
+
// don't need a second round-trip to render it. Use
|
|
634
|
+
// the per-call cache when possible to avoid duplicate
|
|
635
|
+
// settings reads across many holograms from the same
|
|
636
|
+
// source.
|
|
637
|
+
if (withMeta._hologram?.sourceHolon) {
|
|
638
|
+
let sourceHolonName = spaceNames.get(withMeta._hologram.sourceHolon);
|
|
639
|
+
if (sourceHolonName === undefined) {
|
|
640
|
+
sourceHolonName = await getHolonName(holosphere, withMeta._hologram.sourceHolon);
|
|
641
|
+
spaceNames.set(withMeta._hologram.sourceHolon, sourceHolonName);
|
|
607
642
|
}
|
|
608
|
-
|
|
643
|
+
if (sourceHolonName) {
|
|
644
|
+
withMeta._hologram = {
|
|
645
|
+
...withMeta._hologram,
|
|
646
|
+
sourceHolonName
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
result[i] = withMeta;
|
|
609
651
|
} else {
|
|
610
|
-
//
|
|
652
|
+
// Original data not found — keep the id so callers can
|
|
653
|
+
// identify the broken reference, and surface the error.
|
|
611
654
|
result[i] = {
|
|
612
655
|
id: item.id,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
resolved: false,
|
|
656
|
+
_hologram: {
|
|
657
|
+
isHologram: false,
|
|
616
658
|
soul: item.soul,
|
|
617
659
|
error: 'Referenced data not found',
|
|
618
|
-
|
|
660
|
+
resolvedAt: Date.now()
|
|
619
661
|
}
|
|
620
662
|
};
|
|
621
663
|
}
|
|
622
664
|
} else {
|
|
623
665
|
console.warn(`Soul doesn't match expected format: ${item.soul}`);
|
|
624
|
-
// Instead of leaving the original reference, create an error object
|
|
625
666
|
result[i] = {
|
|
626
667
|
id: item.id,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
resolved: false,
|
|
668
|
+
_hologram: {
|
|
669
|
+
isHologram: false,
|
|
630
670
|
soul: item.soul,
|
|
631
671
|
error: 'Invalid soul format',
|
|
632
|
-
|
|
672
|
+
resolvedAt: Date.now()
|
|
633
673
|
}
|
|
634
674
|
};
|
|
635
675
|
}
|
|
636
676
|
} catch (refError) {
|
|
637
|
-
// Instead of leaving the original reference, create an error object
|
|
638
677
|
result[i] = {
|
|
639
678
|
id: item.id,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
resolved: false,
|
|
679
|
+
_hologram: {
|
|
680
|
+
isHologram: false,
|
|
643
681
|
soul: item.soul,
|
|
644
682
|
error: refError.message || 'Error resolving reference',
|
|
645
|
-
|
|
683
|
+
resolvedAt: Date.now()
|
|
646
684
|
}
|
|
647
685
|
};
|
|
648
686
|
}
|
|
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
687
|
}
|
|
685
688
|
}
|
|
686
689
|
}
|
|
@@ -790,18 +793,16 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
|
|
|
790
793
|
// Get federation info for this holon using getFederation
|
|
791
794
|
const fedInfo = await getFederation(holosphere, holon, password);
|
|
792
795
|
|
|
793
|
-
// Only
|
|
794
|
-
if (fedInfo && fedInfo.
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
// Further filter by targetSpaces if provided
|
|
796
|
+
// Only propagate if we have outbound partners configured.
|
|
797
|
+
if (fedInfo && fedInfo.outbound && fedInfo.outbound.length > 0) {
|
|
798
|
+
let spaces = fedInfo.outbound;
|
|
799
|
+
|
|
799
800
|
if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) {
|
|
800
801
|
spaces = spaces.filter(space => targetSpaces.includes(space));
|
|
801
802
|
}
|
|
802
|
-
|
|
803
|
+
|
|
803
804
|
if (spaces.length > 0) {
|
|
804
|
-
//
|
|
805
|
+
// Keep only partners whose outbound lens config includes this lens.
|
|
805
806
|
spaces = spaces.filter(targetSpace => {
|
|
806
807
|
const spaceConfig = fedInfo.lensConfig?.[targetSpace];
|
|
807
808
|
if (!spaceConfig) {
|
|
@@ -810,19 +811,13 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
|
|
|
810
811
|
return false;
|
|
811
812
|
}
|
|
812
813
|
|
|
813
|
-
|
|
814
|
-
const
|
|
814
|
+
const outboundLenses = Array.isArray(spaceConfig.outbound) ? spaceConfig.outbound : [];
|
|
815
|
+
const shouldPropagate = outboundLenses.includes('*') || outboundLenses.includes(lens);
|
|
815
816
|
|
|
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
817
|
if (!shouldPropagate) {
|
|
822
|
-
result.messages.push(`Propagation of lens '${lens}' to target space ${targetSpace} skipped: lens not in '
|
|
818
|
+
result.messages.push(`Propagation of lens '${lens}' to target space ${targetSpace} skipped: lens not in 'outbound' configuration.`);
|
|
823
819
|
result.skipped++;
|
|
824
820
|
}
|
|
825
|
-
|
|
826
821
|
return shouldPropagate;
|
|
827
822
|
});
|
|
828
823
|
|
|
@@ -830,6 +825,11 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
|
|
|
830
825
|
// Check if data is already a hologram
|
|
831
826
|
const isAlreadyHologram = holosphere.isHologram(data);
|
|
832
827
|
|
|
828
|
+
// Resolve our own holon's name once so every propagated
|
|
829
|
+
// payload carries it. Falls back to undefined (the field
|
|
830
|
+
// is omitted) so consumers can use the bare holon id.
|
|
831
|
+
const ownName = await getHolonName(holosphere, holon);
|
|
832
|
+
|
|
833
833
|
// For each target space, propagate the data
|
|
834
834
|
const propagatePromises = spaces.map(async (targetSpace) => {
|
|
835
835
|
try {
|
|
@@ -838,7 +838,8 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
|
|
|
838
838
|
origin: holon, // The space from which this data is being propagated
|
|
839
839
|
sourceLens: lens, // The lens from which this data is being propagated
|
|
840
840
|
propagatedAt: Date.now(),
|
|
841
|
-
originalId: data.id
|
|
841
|
+
originalId: data.id,
|
|
842
|
+
...(ownName ? { originName: ownName } : {})
|
|
842
843
|
};
|
|
843
844
|
|
|
844
845
|
if (useHolograms && !isAlreadyHologram) {
|
|
@@ -945,7 +946,11 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
|
|
|
945
946
|
|
|
946
947
|
// Check if data is already a hologram (reuse from federation section)
|
|
947
948
|
const isAlreadyHologram = holosphere.isHologram(data);
|
|
948
|
-
|
|
949
|
+
|
|
950
|
+
// Resolve our own holon's name once for parent propagation
|
|
951
|
+
// (same as the federation block above).
|
|
952
|
+
const ownNameParent = await getHolonName(holosphere, holon);
|
|
953
|
+
|
|
949
954
|
// Propagate to each parent hexagon
|
|
950
955
|
const parentPropagatePromises = parentHexagons.map(async (parentHexagon) => {
|
|
951
956
|
try {
|
|
@@ -956,7 +961,8 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
|
|
|
956
961
|
propagatedAt: Date.now(),
|
|
957
962
|
originalId: data.id,
|
|
958
963
|
propagationType: 'parent', // Indicate this is parent propagation
|
|
959
|
-
parentLevel: holonResolution - h3.getResolution(parentHexagon) // How many levels up
|
|
964
|
+
parentLevel: holonResolution - h3.getResolution(parentHexagon), // How many levels up
|
|
965
|
+
...(ownNameParent ? { originName: ownNameParent } : {})
|
|
960
966
|
};
|
|
961
967
|
|
|
962
968
|
if (useHolograms && !isAlreadyHologram) {
|
|
@@ -1128,82 +1134,76 @@ export async function resetFederation(holosphere, spaceId, password = null, opti
|
|
|
1128
1134
|
};
|
|
1129
1135
|
}
|
|
1130
1136
|
|
|
1131
|
-
|
|
1132
|
-
result.federatedCount =
|
|
1133
|
-
result.
|
|
1137
|
+
const allPartners = fedInfo.federated || [];
|
|
1138
|
+
result.federatedCount = allPartners.length;
|
|
1139
|
+
result.inboundCount = fedInfo.inbound?.length || 0;
|
|
1140
|
+
result.outboundCount = fedInfo.outbound?.length || 0;
|
|
1134
1141
|
|
|
1135
|
-
//
|
|
1142
|
+
// Reset federation record to empty.
|
|
1136
1143
|
const emptyFedInfo = {
|
|
1137
1144
|
id: spaceId,
|
|
1138
1145
|
name: spaceName || spaceId,
|
|
1139
|
-
|
|
1140
|
-
|
|
1146
|
+
federated: [],
|
|
1147
|
+
inbound: [],
|
|
1148
|
+
outbound: [],
|
|
1149
|
+
lensConfig: {},
|
|
1150
|
+
partnerNames: {},
|
|
1141
1151
|
timestamp: Date.now()
|
|
1142
1152
|
};
|
|
1143
|
-
|
|
1144
|
-
// Update federation record with empty lists
|
|
1145
1153
|
await holosphere.putGlobal('federation', emptyFedInfo, password);
|
|
1146
1154
|
|
|
1147
|
-
//
|
|
1148
|
-
if (notifyPartners &&
|
|
1149
|
-
const updatePromises =
|
|
1155
|
+
// Tell each partner to drop us from their lists.
|
|
1156
|
+
if (notifyPartners && allPartners.length > 0) {
|
|
1157
|
+
const updatePromises = allPartners.map(async (partnerSpace) => {
|
|
1150
1158
|
try {
|
|
1151
|
-
// Get partner's federation info
|
|
1152
1159
|
const partnerFedInfo = await getFederation(holosphere, partnerSpace);
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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;
|
|
1160
|
+
if (!partnerFedInfo) return false;
|
|
1161
|
+
|
|
1162
|
+
const sid = spaceId.toString();
|
|
1163
|
+
if (partnerFedInfo.federated) {
|
|
1164
|
+
partnerFedInfo.federated = partnerFedInfo.federated.filter(id => id !== sid);
|
|
1175
1165
|
}
|
|
1176
|
-
|
|
1166
|
+
if (partnerFedInfo.inbound) {
|
|
1167
|
+
partnerFedInfo.inbound = partnerFedInfo.inbound.filter(id => id !== sid);
|
|
1168
|
+
}
|
|
1169
|
+
if (partnerFedInfo.outbound) {
|
|
1170
|
+
partnerFedInfo.outbound = partnerFedInfo.outbound.filter(id => id !== sid);
|
|
1171
|
+
}
|
|
1172
|
+
if (partnerFedInfo.lensConfig) {
|
|
1173
|
+
delete partnerFedInfo.lensConfig[sid];
|
|
1174
|
+
}
|
|
1175
|
+
if (partnerFedInfo.partnerNames) {
|
|
1176
|
+
delete partnerFedInfo.partnerNames[sid];
|
|
1177
|
+
}
|
|
1178
|
+
partnerFedInfo.timestamp = Date.now();
|
|
1179
|
+
|
|
1180
|
+
await holosphere.putGlobal('federation', partnerFedInfo);
|
|
1181
|
+
result.partnersNotified++;
|
|
1182
|
+
return true;
|
|
1177
1183
|
} catch (error) {
|
|
1178
|
-
result.errors.push({
|
|
1179
|
-
partner: partnerSpace,
|
|
1180
|
-
error: error.message
|
|
1181
|
-
});
|
|
1184
|
+
result.errors.push({ partner: partnerSpace, error: error.message });
|
|
1182
1185
|
return false;
|
|
1183
1186
|
}
|
|
1184
1187
|
});
|
|
1185
|
-
|
|
1188
|
+
|
|
1186
1189
|
await Promise.all(updatePromises);
|
|
1187
1190
|
}
|
|
1188
|
-
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
await holosphere.putGlobal('federationMeta', meta);
|
|
1203
|
-
}
|
|
1204
|
-
} catch (error) {
|
|
1191
|
+
|
|
1192
|
+
// Mark any federationMeta records inactive.
|
|
1193
|
+
for (const partnerSpace of allPartners) {
|
|
1194
|
+
try {
|
|
1195
|
+
const metaId = `${spaceId}_${partnerSpace}`;
|
|
1196
|
+
const altMetaId = `${partnerSpace}_${spaceId}`;
|
|
1197
|
+
|
|
1198
|
+
const meta = await holosphere.getGlobal('federationMeta', metaId) ||
|
|
1199
|
+
await holosphere.getGlobal('federationMeta', altMetaId);
|
|
1200
|
+
|
|
1201
|
+
if (meta) {
|
|
1202
|
+
meta.status = 'inactive';
|
|
1203
|
+
meta.endedAt = Date.now();
|
|
1204
|
+
await holosphere.putGlobal('federationMeta', meta);
|
|
1205
1205
|
}
|
|
1206
|
-
}
|
|
1206
|
+
} catch {}
|
|
1207
1207
|
}
|
|
1208
1208
|
|
|
1209
1209
|
result.success = true;
|