holosphere 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FEDERATION.md +213 -0
- package/README.md +140 -0
- package/babel.config.js +5 -0
- package/examples/README-environmental.md +158 -0
- package/examples/environmentalData.js +380 -0
- package/examples/federation.js +154 -0
- package/examples/queryEnvironmentalData.js +147 -0
- package/examples/references.js +177 -0
- package/federation.js +988 -0
- package/holosphere.d.ts +445 -20
- package/holosphere.js +1083 -998
- package/package.json +3 -6
- package/services/environmentalApi.js +162 -0
- package/services/environmentalApi.test.js +0 -6
- package/test/ai.test.js +268 -76
- package/test/auth.test.js +241 -0
- package/test/delete.test.js +225 -0
- package/test/federation.test.js +163 -356
- package/test/holosphere.test.js +109 -955
- package/test/sea.html +33 -0
- package/test/jest.setup.js +0 -5
- package/test/spacesauth.test.js +0 -335
package/federation.js
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation functionality for HoloSphere
|
|
3
|
+
* Provides methods for creating, managing, and using federated spaces
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a federation relationship between two spaces
|
|
9
|
+
* Federation is bidirectional by default, and data propagation uses soul references by default.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
12
|
+
* @param {string} spaceId1 - The first space ID
|
|
13
|
+
* @param {string} spaceId2 - The second space ID
|
|
14
|
+
* @param {string} [password1] - Optional password for the first space
|
|
15
|
+
* @param {string} [password2] - Optional password for the second space
|
|
16
|
+
* @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications (default: true)
|
|
17
|
+
* @returns {Promise<boolean>} - True if federation was created successfully
|
|
18
|
+
*/
|
|
19
|
+
export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true) {
|
|
20
|
+
console.log('FEDERATING', spaceId1, spaceId2, password1, password2, bidirectional)
|
|
21
|
+
if (!spaceId1 || !spaceId2) {
|
|
22
|
+
throw new Error('federate: Missing required space IDs');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Prevent self-federation
|
|
26
|
+
if (spaceId1 === spaceId2) {
|
|
27
|
+
throw new Error('Cannot federate a space with itself');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Get or create federation info for first space (A)
|
|
32
|
+
let fedInfo1 = null;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
|
|
38
|
+
// Create new federation info if it doesn't exist
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
if (fedInfo1 == null) {
|
|
42
|
+
fedInfo1 = {
|
|
43
|
+
id: spaceId1,
|
|
44
|
+
name: spaceId1,
|
|
45
|
+
federation: [],
|
|
46
|
+
notify: [],
|
|
47
|
+
timestamp: Date.now()
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// Ensure arrays exist
|
|
53
|
+
if (!fedInfo1.federation) fedInfo1.federation = [];
|
|
54
|
+
if (!fedInfo1.notify) fedInfo1.notify = [];
|
|
55
|
+
|
|
56
|
+
// Add space2 to space1's federation and notify lists if not already present
|
|
57
|
+
if (!fedInfo1.federation.includes(spaceId2)) {
|
|
58
|
+
fedInfo1.federation.push(spaceId2);
|
|
59
|
+
}
|
|
60
|
+
// // Always add to notify list for the first space (primary direction)
|
|
61
|
+
// if (!fedInfo1.notify.includes(spaceId2)) {
|
|
62
|
+
// fedInfo1.notify.push(spaceId2);
|
|
63
|
+
// }
|
|
64
|
+
|
|
65
|
+
// Update timestamp
|
|
66
|
+
fedInfo1.timestamp = Date.now();
|
|
67
|
+
|
|
68
|
+
// Save updated federation info for space1
|
|
69
|
+
try {
|
|
70
|
+
await holosphere.putGlobal('federation', fedInfo1, password1);
|
|
71
|
+
console.log(`Updated federation info for ${spaceId1}`);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
|
|
74
|
+
throw new Error(`Failed to create federation: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If bidirectional is true, handle space2 (B) as well
|
|
78
|
+
//if (bidirectional && password2) {
|
|
79
|
+
{
|
|
80
|
+
let fedInfo2 = null;
|
|
81
|
+
try {
|
|
82
|
+
fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
|
|
85
|
+
// Create new federation info if it doesn't exist
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
if (fedInfo2 == null) {
|
|
89
|
+
fedInfo2 = {
|
|
90
|
+
id: spaceId2,
|
|
91
|
+
name: spaceId2,
|
|
92
|
+
federation: [],
|
|
93
|
+
notify: [],
|
|
94
|
+
timestamp: Date.now()
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add nEnsure arrays exist
|
|
99
|
+
|
|
100
|
+
if (!fedInfo2.notify) fedInfo2.notify = [];
|
|
101
|
+
|
|
102
|
+
// Add space1 to space2's federation list if not already present
|
|
103
|
+
if (!fedInfo2.notify.includes(spaceId1)) {
|
|
104
|
+
fedInfo2.notify.push(spaceId1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
// Update timestamp
|
|
109
|
+
fedInfo2.timestamp = Date.now();
|
|
110
|
+
|
|
111
|
+
// Save updated federation info for space2
|
|
112
|
+
try {
|
|
113
|
+
await holosphere.putGlobal('federation', fedInfo2, password2);
|
|
114
|
+
console.log(`Updated federation info for ${spaceId2}`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
|
|
117
|
+
// Don't throw here as the main federation was successful
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create federation metadata record
|
|
122
|
+
const federationMeta = {
|
|
123
|
+
id: `${spaceId1}_${spaceId2}`,
|
|
124
|
+
space1: spaceId1,
|
|
125
|
+
space2: spaceId2,
|
|
126
|
+
created: Date.now(),
|
|
127
|
+
status: 'active',
|
|
128
|
+
bidirectional: bidirectional
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await holosphere.putGlobal('federationMeta', federationMeta);
|
|
133
|
+
console.log(`Created federation metadata for ${spaceId1} and ${spaceId2}`);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.warn(`Could not create federation metadata: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error(`Federation creation failed: ${error.message}`);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Subscribes to federation notifications for a space
|
|
147
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
148
|
+
* @param {string} spaceId - The space ID to subscribe to
|
|
149
|
+
* @param {string} [password] - Optional password for the space
|
|
150
|
+
* @param {function} callback - The callback to execute on notifications
|
|
151
|
+
* @param {object} [options] - Subscription options
|
|
152
|
+
* @param {string[]} [options.lenses] - Specific lenses to subscribe to (default: all)
|
|
153
|
+
* @param {number} [options.throttle] - Throttle notifications in ms (default: 0)
|
|
154
|
+
* @returns {Promise<object>} - Subscription object with unsubscribe() method
|
|
155
|
+
*/
|
|
156
|
+
export async function subscribeFederation(holosphere, spaceId, password = null, callback, options = {}) {
|
|
157
|
+
if (!spaceId || !callback) {
|
|
158
|
+
throw new Error('subscribeFederation: Missing required parameters');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { lenses = ['*'], throttle = 0 } = options;
|
|
162
|
+
|
|
163
|
+
// Get federation info
|
|
164
|
+
const fedInfo = await holosphere.getGlobal('federation', spaceId, password);
|
|
165
|
+
if (!fedInfo) {
|
|
166
|
+
throw new Error('No federation info found for space');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Create subscription for each federated space
|
|
170
|
+
const subscriptions = [];
|
|
171
|
+
let lastNotificationTime = {};
|
|
172
|
+
|
|
173
|
+
if (fedInfo.federation && fedInfo.federation.length > 0) {
|
|
174
|
+
for (const federatedSpace of fedInfo.federation) {
|
|
175
|
+
// For each lens specified (or all if '*')
|
|
176
|
+
for (const lens of lenses) {
|
|
177
|
+
try {
|
|
178
|
+
const sub = await holosphere.subscribe(federatedSpace, lens, async (data) => {
|
|
179
|
+
try {
|
|
180
|
+
// Skip if data is missing or not from federated space
|
|
181
|
+
if (!data || !data.id) return;
|
|
182
|
+
|
|
183
|
+
// Apply throttling if configured
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const key = `${federatedSpace}_${lens}_${data.id}`;
|
|
186
|
+
|
|
187
|
+
if (throttle > 0) {
|
|
188
|
+
if (lastNotificationTime[key] &&
|
|
189
|
+
(now - lastNotificationTime[key]) < throttle) {
|
|
190
|
+
return; // Skip this notification (throttled)
|
|
191
|
+
}
|
|
192
|
+
lastNotificationTime[key] = now;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Add federation metadata if not present
|
|
196
|
+
if (!data.federation) {
|
|
197
|
+
data.federation = {
|
|
198
|
+
origin: federatedSpace,
|
|
199
|
+
timestamp: now
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Execute callback with the data
|
|
204
|
+
await callback(data, federatedSpace, lens);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.warn('Federation notification error:', error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (sub && typeof sub.unsubscribe === 'function') {
|
|
211
|
+
subscriptions.push(sub);
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.warn(`Error creating subscription for ${federatedSpace}/${lens}:`, error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Return combined subscription object
|
|
221
|
+
return {
|
|
222
|
+
unsubscribe: () => {
|
|
223
|
+
subscriptions.forEach(sub => {
|
|
224
|
+
try {
|
|
225
|
+
if (sub && typeof sub.unsubscribe === 'function') {
|
|
226
|
+
sub.unsubscribe();
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.warn('Error unsubscribing:', error);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
// Clear the subscriptions array
|
|
233
|
+
subscriptions.length = 0;
|
|
234
|
+
// Clear throttling data
|
|
235
|
+
lastNotificationTime = {};
|
|
236
|
+
},
|
|
237
|
+
getSubscriptionCount: () => subscriptions.length
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Gets federation info for a space
|
|
243
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
244
|
+
* @param {string} spaceId - The space ID
|
|
245
|
+
* @param {string} [password] - Optional password for the space
|
|
246
|
+
* @returns {Promise<object|null>} - Federation info or null if not found
|
|
247
|
+
*/
|
|
248
|
+
export async function getFederation(holosphere, spaceId, password = null) {
|
|
249
|
+
if (!spaceId) {
|
|
250
|
+
throw new Error('getFederation: Missing space ID');
|
|
251
|
+
}
|
|
252
|
+
return await holosphere.getGlobal('federation', spaceId, password);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Removes a federation relationship between spaces
|
|
257
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
258
|
+
* @param {string} spaceId1 - The first space ID
|
|
259
|
+
* @param {string} spaceId2 - The second space ID
|
|
260
|
+
* @param {string} [password1] - Optional password for the first space
|
|
261
|
+
* @param {string} [password2] - Optional password for the second space
|
|
262
|
+
* @returns {Promise<boolean>} - True if federation was removed successfully
|
|
263
|
+
*/
|
|
264
|
+
export async function unfederate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) {
|
|
265
|
+
if (!spaceId1 || !spaceId2) {
|
|
266
|
+
throw new Error('unfederate: Missing required space IDs');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
// Get federation info for first space
|
|
271
|
+
let fedInfo1 = null;
|
|
272
|
+
try {
|
|
273
|
+
fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!fedInfo1 || !fedInfo1.federation) {
|
|
279
|
+
console.warn(`Federation not found for space ${spaceId1}`);
|
|
280
|
+
// Continue anyway to clean up any potential metadata
|
|
281
|
+
} else {
|
|
282
|
+
// Update first space federation info
|
|
283
|
+
fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
|
|
284
|
+
fedInfo1.timestamp = Date.now();
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await holosphere.putGlobal('federation', fedInfo1, password1);
|
|
288
|
+
console.log(`Updated federation info for ${spaceId1}`);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Update second space federation info if password provided
|
|
295
|
+
if (password2) {
|
|
296
|
+
let fedInfo2 = null;
|
|
297
|
+
try {
|
|
298
|
+
fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (fedInfo2 && fedInfo2.notify) {
|
|
304
|
+
fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
|
|
305
|
+
fedInfo2.timestamp = Date.now();
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await holosphere.putGlobal('federation', fedInfo2, password2);
|
|
309
|
+
console.log(`Updated federation info for ${spaceId2}`);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Update federation metadata
|
|
317
|
+
const metaId = `${spaceId1}_${spaceId2}`;
|
|
318
|
+
const altMetaId = `${spaceId2}_${spaceId1}`;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const meta = await holosphere.getGlobal('federationMeta', metaId) ||
|
|
322
|
+
await holosphere.getGlobal('federationMeta', altMetaId);
|
|
323
|
+
|
|
324
|
+
if (meta) {
|
|
325
|
+
meta.status = 'inactive';
|
|
326
|
+
meta.endedAt = Date.now();
|
|
327
|
+
await holosphere.putGlobal('federationMeta', meta);
|
|
328
|
+
console.log(`Updated federation metadata for ${spaceId1} and ${spaceId2}`);
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.warn(`Could not update federation metadata: ${error.message}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return true;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error(`Federation removal failed: ${error.message}`);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Removes a notification relationship between two spaces
|
|
343
|
+
* This removes spaceId2 from the notify list of spaceId1
|
|
344
|
+
*
|
|
345
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
346
|
+
* @param {string} spaceId1 - The space to modify (remove from its notify list)
|
|
347
|
+
* @param {string} spaceId2 - The space to be removed from notifications
|
|
348
|
+
* @param {string} [password1] - Optional password for the first space
|
|
349
|
+
* @returns {Promise<boolean>} - True if notification was removed successfully
|
|
350
|
+
*/
|
|
351
|
+
export async function removeNotify(holosphere, spaceId1, spaceId2, password1 = null) {
|
|
352
|
+
if (!spaceId1 || !spaceId2) {
|
|
353
|
+
throw new Error('removeNotify: Missing required space IDs');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
// Get federation info for space
|
|
358
|
+
let fedInfo = await holosphere.getGlobal('federation', spaceId1, password1);
|
|
359
|
+
|
|
360
|
+
if (!fedInfo) {
|
|
361
|
+
throw new Error(`No federation info found for ${spaceId1}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Ensure notify array exists
|
|
365
|
+
if (!fedInfo.notify) fedInfo.notify = [];
|
|
366
|
+
|
|
367
|
+
// Remove space2 from space1's notify list if present
|
|
368
|
+
if (fedInfo.notify.includes(spaceId2)) {
|
|
369
|
+
fedInfo.notify = fedInfo.notify.filter(id => id !== spaceId2);
|
|
370
|
+
|
|
371
|
+
// Update timestamp
|
|
372
|
+
fedInfo.timestamp = Date.now();
|
|
373
|
+
|
|
374
|
+
// Save updated federation info
|
|
375
|
+
await holosphere.putGlobal('federation', fedInfo, password1);
|
|
376
|
+
console.log(`Removed ${spaceId2} from ${spaceId1}'s notify list`);
|
|
377
|
+
return true;
|
|
378
|
+
} else {
|
|
379
|
+
console.log(`${spaceId2} not found in ${spaceId1}'s notify list`);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error(`Remove notification failed: ${error.message}`);
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get and combine data from local and federated sources
|
|
390
|
+
* @param {HoloSphere} holosphere The HoloSphere instance
|
|
391
|
+
* @param {string} holon The local holon name
|
|
392
|
+
* @param {string} lens The lens to query
|
|
393
|
+
* @param {Object} options Options for data retrieval and aggregation
|
|
394
|
+
* @param {boolean} options.aggregate Whether to aggregate results by ID (default: false)
|
|
395
|
+
* @param {string} options.idField The field to use as ID (default: '_id')
|
|
396
|
+
* @param {string[]} options.sumFields Fields to sum during aggregation (default: [])
|
|
397
|
+
* @param {string[]} options.concatArrays Array fields to concatenate during aggregation (default: [])
|
|
398
|
+
* @param {boolean} options.removeDuplicates Whether to remove duplicates in concatenated arrays (default: true)
|
|
399
|
+
* @param {Function} options.mergeStrategy Custom merge function for aggregation (default: null)
|
|
400
|
+
* @param {boolean} options.includeLocal Whether to include local data (default: true)
|
|
401
|
+
* @param {boolean} options.includeFederated Whether to include federated data (default: true)
|
|
402
|
+
* @param {boolean} options.resolveReferences Whether to resolve federation references (default: true)
|
|
403
|
+
* @param {number} options.maxFederatedSpaces Maximum number of federated spaces to query (default: -1 for all)
|
|
404
|
+
* @param {number} options.timeout Timeout in milliseconds for federated queries (default: 10000)
|
|
405
|
+
* @returns {Promise<Array>} Combined array of local and federated data
|
|
406
|
+
*/
|
|
407
|
+
export async function getFederated(holosphere, holon, lens, options = {}) {
|
|
408
|
+
console.log(`getFederated called with options:`, JSON.stringify(options));
|
|
409
|
+
|
|
410
|
+
// Set default options
|
|
411
|
+
const {
|
|
412
|
+
aggregate = false,
|
|
413
|
+
idField = 'id',
|
|
414
|
+
sumFields = [],
|
|
415
|
+
concatArrays = [],
|
|
416
|
+
removeDuplicates = true,
|
|
417
|
+
mergeStrategy = null,
|
|
418
|
+
includeLocal = true,
|
|
419
|
+
includeFederated = true,
|
|
420
|
+
resolveReferences = true, // Default to true
|
|
421
|
+
maxFederatedSpaces = -1,
|
|
422
|
+
timeout = 10000
|
|
423
|
+
} = options;
|
|
424
|
+
|
|
425
|
+
console.log(`resolveReferences option: ${resolveReferences}`);
|
|
426
|
+
|
|
427
|
+
// Validate required parameters
|
|
428
|
+
if (!holosphere || !holon || !lens) {
|
|
429
|
+
throw new Error('Missing required parameters: holosphere, holon, and lens are required');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Get federation info for current space
|
|
433
|
+
// Use holon as the space ID
|
|
434
|
+
const spaceId = holon;
|
|
435
|
+
const fedInfo = await getFederation(holosphere, spaceId);
|
|
436
|
+
|
|
437
|
+
console.log(`Federation info retrieved:`, JSON.stringify(fedInfo));
|
|
438
|
+
|
|
439
|
+
// Initialize result array and track processed IDs to avoid duplicates
|
|
440
|
+
const result = [];
|
|
441
|
+
const processedIds = new Set();
|
|
442
|
+
const references = new Map(); // To keep track of references for resolution
|
|
443
|
+
|
|
444
|
+
// Process each federated space first to prioritize federation data
|
|
445
|
+
if (includeFederated && fedInfo && fedInfo.federation && fedInfo.federation.length > 0) {
|
|
446
|
+
console.log(`Found ${fedInfo.federation.length} federated spaces`);
|
|
447
|
+
|
|
448
|
+
// Limit number of federated spaces to query
|
|
449
|
+
const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.federation : fedInfo.federation.slice(0, maxFederatedSpaces);
|
|
450
|
+
console.log(`Will process ${federatedSpaces.length} federated spaces: ${JSON.stringify(federatedSpaces)}`);
|
|
451
|
+
|
|
452
|
+
// Process federated spaces
|
|
453
|
+
for (const federatedSpace of federatedSpaces) {
|
|
454
|
+
try {
|
|
455
|
+
console.log(`=== PROCESSING FEDERATED SPACE: ${federatedSpace} ===`);
|
|
456
|
+
|
|
457
|
+
// Get all data for this lens from the federated space
|
|
458
|
+
const federatedItems = await holosphere.getAll(federatedSpace, lens);
|
|
459
|
+
console.log(`Got ${federatedItems.length} items from federated space ${federatedSpace}`);
|
|
460
|
+
console.log(`Federated items:`, JSON.stringify(federatedItems));
|
|
461
|
+
|
|
462
|
+
// Process each item
|
|
463
|
+
for (const item of federatedItems) {
|
|
464
|
+
if (!item) {
|
|
465
|
+
console.log('Item is null or undefined, skipping');
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
console.log(`Checking item for ID field '${idField}':`, item);
|
|
470
|
+
|
|
471
|
+
if (!item[idField]) {
|
|
472
|
+
console.log(`Item missing ID field '${idField}', available fields:`, Object.keys(item));
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// For now, just add this item to results, we'll resolve references later
|
|
477
|
+
result.push(item);
|
|
478
|
+
processedIds.add(item[idField]);
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.warn(`Error processing federated space ${federatedSpace}: ${error.message}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Now get local data if requested
|
|
487
|
+
if (includeLocal) {
|
|
488
|
+
const localData = await holosphere.getAll(holon, lens);
|
|
489
|
+
console.log(`Got ${localData.length} local items from holon ${holon}`);
|
|
490
|
+
|
|
491
|
+
// Add each local item to results, but only if not already processed
|
|
492
|
+
for (const item of localData) {
|
|
493
|
+
if (item && item[idField] && !processedIds.has(item[idField])) {
|
|
494
|
+
result.push(item);
|
|
495
|
+
processedIds.add(item[idField]);
|
|
496
|
+
} else if (item && item[idField]) {
|
|
497
|
+
console.log(`Local item ${item[idField]} already in result from federation, skipping`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Now resolve references if needed
|
|
503
|
+
if (resolveReferences) {
|
|
504
|
+
console.log(`Resolving references for ${result.length} items`);
|
|
505
|
+
|
|
506
|
+
for (let i = 0; i < result.length; i++) {
|
|
507
|
+
const item = result[i];
|
|
508
|
+
|
|
509
|
+
// Check for simplified reference (item with id and soul)
|
|
510
|
+
if (item.soul && item.id) {
|
|
511
|
+
console.log(`Found simple reference with soul: ${item.soul}`);
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
// Parse the soul to get the components
|
|
515
|
+
const soulParts = item.soul.split('/');
|
|
516
|
+
if (soulParts.length >= 4) {
|
|
517
|
+
const originHolon = soulParts[1];
|
|
518
|
+
const originLens = soulParts[2];
|
|
519
|
+
const originKey = soulParts[3];
|
|
520
|
+
|
|
521
|
+
console.log(`Extracting from soul - holon: ${originHolon}, lens: ${originLens}, key: ${originKey}`);
|
|
522
|
+
|
|
523
|
+
// Get original data using the extracted path
|
|
524
|
+
const originalData = await holosphere.get(
|
|
525
|
+
originHolon,
|
|
526
|
+
originLens,
|
|
527
|
+
originKey,
|
|
528
|
+
null,
|
|
529
|
+
{ resolveReferences: false } // Prevent infinite recursion
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
console.log(`Original data found via soul path:`, JSON.stringify(originalData));
|
|
533
|
+
|
|
534
|
+
if (originalData) {
|
|
535
|
+
// Replace the reference with the original data
|
|
536
|
+
result[i] = {
|
|
537
|
+
...originalData,
|
|
538
|
+
_federation: {
|
|
539
|
+
isReference: true,
|
|
540
|
+
resolved: true,
|
|
541
|
+
soul: item.soul,
|
|
542
|
+
timestamp: Date.now()
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
console.log(`Reference resolved successfully via soul path, processed item:`, JSON.stringify(result[i]));
|
|
546
|
+
} else {
|
|
547
|
+
console.warn(`Could not resolve reference: original data not found at extracted path`);
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
console.warn(`Soul doesn't match expected format: ${item.soul}`);
|
|
551
|
+
}
|
|
552
|
+
} catch (refError) {
|
|
553
|
+
console.warn(`Error resolving reference by soul in getFederated: ${refError.message}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// For backward compatibility, check for old-style references
|
|
557
|
+
else if (item._federation && item._federation.isReference) {
|
|
558
|
+
console.log(`Found legacy reference: ${item._federation.origin}/${item._federation.lens}/${item[idField]}`);
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const reference = item._federation;
|
|
562
|
+
console.log(`Getting original data from ${reference.origin} / ${reference.lens} / ${item[idField]}`);
|
|
563
|
+
|
|
564
|
+
// Get original data
|
|
565
|
+
const originalData = await holosphere.get(
|
|
566
|
+
reference.origin,
|
|
567
|
+
reference.lens,
|
|
568
|
+
item[idField],
|
|
569
|
+
null,
|
|
570
|
+
{ resolveReferences: false } // Prevent infinite recursion
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
console.log(`Original data found:`, JSON.stringify(originalData));
|
|
574
|
+
|
|
575
|
+
if (originalData) {
|
|
576
|
+
// Add federation information to the resolved data
|
|
577
|
+
result[i] = {
|
|
578
|
+
...originalData,
|
|
579
|
+
_federation: {
|
|
580
|
+
...reference,
|
|
581
|
+
resolved: true,
|
|
582
|
+
timestamp: Date.now()
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
console.log(`Legacy reference resolved successfully, processed item:`, JSON.stringify(result[i]));
|
|
586
|
+
} else {
|
|
587
|
+
console.warn(`Could not resolve legacy reference: original data not found`);
|
|
588
|
+
}
|
|
589
|
+
} catch (refError) {
|
|
590
|
+
console.warn(`Error resolving legacy reference in getFederated: ${refError.message}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Apply aggregation if requested
|
|
597
|
+
if (aggregate && result.length > 0) {
|
|
598
|
+
// Group items by ID
|
|
599
|
+
const groupedById = result.reduce((acc, item) => {
|
|
600
|
+
const id = item[idField];
|
|
601
|
+
if (!acc[id]) {
|
|
602
|
+
acc[id] = [];
|
|
603
|
+
}
|
|
604
|
+
acc[id].push(item);
|
|
605
|
+
return acc;
|
|
606
|
+
}, {});
|
|
607
|
+
|
|
608
|
+
// Aggregate each group
|
|
609
|
+
const aggregatedData = Object.values(groupedById).map(group => {
|
|
610
|
+
// If only one item in group, no aggregation needed
|
|
611
|
+
if (group.length === 1) return group[0];
|
|
612
|
+
|
|
613
|
+
// Use custom merge strategy if provided
|
|
614
|
+
if (mergeStrategy && typeof mergeStrategy === 'function') {
|
|
615
|
+
return mergeStrategy(group);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Default aggregation strategy
|
|
619
|
+
const base = { ...group[0] };
|
|
620
|
+
|
|
621
|
+
// Sum numeric fields
|
|
622
|
+
for (const field of sumFields) {
|
|
623
|
+
if (typeof base[field] === 'number') {
|
|
624
|
+
base[field] = group.reduce((sum, item) => sum + (Number(item[field]) || 0), 0);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Concatenate array fields
|
|
629
|
+
for (const field of concatArrays) {
|
|
630
|
+
if (Array.isArray(base[field])) {
|
|
631
|
+
const allValues = group.reduce((all, item) => {
|
|
632
|
+
return Array.isArray(item[field]) ? [...all, ...item[field]] : all;
|
|
633
|
+
}, []);
|
|
634
|
+
|
|
635
|
+
// Remove duplicates if requested
|
|
636
|
+
base[field] = removeDuplicates ? Array.from(new Set(allValues)) : allValues;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Add aggregation metadata
|
|
641
|
+
base._aggregated = {
|
|
642
|
+
count: group.length,
|
|
643
|
+
timestamp: Date.now()
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
return base;
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
return aggregatedData;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Propagates data to federated spaces
|
|
657
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
658
|
+
* @param {string} holon - The holon identifier
|
|
659
|
+
* @param {string} lens - The lens identifier
|
|
660
|
+
* @param {object} data - The data to propagate
|
|
661
|
+
* @param {object} [options] - Propagation options
|
|
662
|
+
* @param {boolean} [options.useReferences=true] - Whether to use references instead of duplicating data
|
|
663
|
+
* @param {string[]} [options.targetSpaces] - Specific target spaces to propagate to (defaults to all federated spaces)
|
|
664
|
+
* @param {string} [options.password] - Password for accessing the source holon (if needed)
|
|
665
|
+
* @returns {Promise<object>} - Result with success count and errors
|
|
666
|
+
*/
|
|
667
|
+
export async function propagate(holosphere, holon, lens, data, options = {}) {
|
|
668
|
+
if (!holosphere || !holon || !lens || !data) {
|
|
669
|
+
throw new Error('propagate: Missing required parameters');
|
|
670
|
+
}
|
|
671
|
+
// Default propagation options
|
|
672
|
+
const {
|
|
673
|
+
useReferences = true,
|
|
674
|
+
targetSpaces = null,
|
|
675
|
+
password = null
|
|
676
|
+
} = options;
|
|
677
|
+
|
|
678
|
+
const result = {
|
|
679
|
+
success: 0,
|
|
680
|
+
errors: 0,
|
|
681
|
+
errorDetails: [],
|
|
682
|
+
propagated: false,
|
|
683
|
+
referencesUsed: useReferences
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
// Get federation info for this holon using getFederation
|
|
688
|
+
const fedInfo = await getFederation(holosphere, holon, password);
|
|
689
|
+
|
|
690
|
+
// If no federation info or no federation list, return with message
|
|
691
|
+
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
692
|
+
return {
|
|
693
|
+
...result,
|
|
694
|
+
message: `No federation found for ${holon}`
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// If no notification list or it's empty, return with message
|
|
699
|
+
if (!fedInfo.notify || fedInfo.notify.length === 0) {
|
|
700
|
+
return {
|
|
701
|
+
...result,
|
|
702
|
+
message: `No notification targets found for ${holon}`
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Filter federation spaces to those in notify list
|
|
707
|
+
let spaces = fedInfo.notify;
|
|
708
|
+
|
|
709
|
+
// Further filter by targetSpaces if provided
|
|
710
|
+
if (targetSpaces && Array.isArray(targetSpaces) && targetSpaces.length > 0) {
|
|
711
|
+
spaces = spaces.filter(space => targetSpaces.includes(space));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (spaces.length === 0) {
|
|
715
|
+
return {
|
|
716
|
+
...result,
|
|
717
|
+
message: 'No valid target spaces found after filtering'
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// For each target space, propagate the data
|
|
722
|
+
const propagatePromises = spaces.map(async (targetSpace) => {
|
|
723
|
+
try {
|
|
724
|
+
// Get federation info for target space using getFederation
|
|
725
|
+
const targetFedInfo = await getFederation(holosphere, targetSpace);
|
|
726
|
+
|
|
727
|
+
// If using references, create a soul reference instead of duplicating the data
|
|
728
|
+
if (useReferences) {
|
|
729
|
+
// Create a soul path that points to the original data
|
|
730
|
+
const soul = `${holosphere.appname}/${holon}/${lens}/${data.id}`;
|
|
731
|
+
|
|
732
|
+
// Create a minimal reference object with just id and soul
|
|
733
|
+
const reference = {
|
|
734
|
+
id: data.id,
|
|
735
|
+
soul: soul,
|
|
736
|
+
_federation: {
|
|
737
|
+
origin: holon,
|
|
738
|
+
lens: lens,
|
|
739
|
+
timestamp: Date.now()
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
console.log(`Using soul reference: ${soul} for data: ${data.id}`);
|
|
744
|
+
|
|
745
|
+
// Store the reference in the target space without propagation
|
|
746
|
+
await holosphere.put(targetSpace, lens, reference, null, { autoPropagate: false });
|
|
747
|
+
|
|
748
|
+
result.success++;
|
|
749
|
+
return true;
|
|
750
|
+
} else {
|
|
751
|
+
// If not using references, store a full copy without propagation
|
|
752
|
+
const dataToStore = {
|
|
753
|
+
...data,
|
|
754
|
+
_federation: {
|
|
755
|
+
origin: holon,
|
|
756
|
+
lens: lens,
|
|
757
|
+
timestamp: Date.now()
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
await holosphere.put(targetSpace, lens, dataToStore, null, { autoPropagate: false });
|
|
761
|
+
result.success++;
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
} catch (error) {
|
|
765
|
+
result.errors++;
|
|
766
|
+
result.errorDetails.push({
|
|
767
|
+
space: targetSpace,
|
|
768
|
+
error: error.message
|
|
769
|
+
});
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await Promise.all(propagatePromises);
|
|
775
|
+
|
|
776
|
+
result.propagated = result.success > 0;
|
|
777
|
+
return result;
|
|
778
|
+
} catch (error) {
|
|
779
|
+
console.error('Error in propagate:', error);
|
|
780
|
+
return {
|
|
781
|
+
...result,
|
|
782
|
+
error: error.message
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Tracks a federated message across different chats
|
|
789
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
790
|
+
* @param {string} originalChatId - The ID of the original chat
|
|
791
|
+
* @param {string} messageId - The ID of the original message
|
|
792
|
+
* @param {string} federatedChatId - The ID of the federated chat
|
|
793
|
+
* @param {string} federatedMessageId - The ID of the message in the federated chat
|
|
794
|
+
* @param {string} type - The type of message (e.g., 'quest', 'announcement')
|
|
795
|
+
* @returns {Promise<void>}
|
|
796
|
+
*/
|
|
797
|
+
export async function federateMessage(holosphere, originalChatId, messageId, federatedChatId, federatedMessageId, type = 'generic') {
|
|
798
|
+
const trackingKey = `${originalChatId}_${messageId}_fedmsgs`;
|
|
799
|
+
const tracking = await holosphere.getGlobal('federation_messages', trackingKey) || {
|
|
800
|
+
id: trackingKey,
|
|
801
|
+
originalChatId,
|
|
802
|
+
originalMessageId: messageId,
|
|
803
|
+
type,
|
|
804
|
+
messages: []
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// Update or add the federated message info
|
|
808
|
+
const existingMsg = tracking.messages.find(m => m.chatId === federatedChatId);
|
|
809
|
+
if (existingMsg) {
|
|
810
|
+
existingMsg.messageId = federatedMessageId;
|
|
811
|
+
existingMsg.timestamp = Date.now();
|
|
812
|
+
} else {
|
|
813
|
+
tracking.messages.push({
|
|
814
|
+
chatId: federatedChatId,
|
|
815
|
+
messageId: federatedMessageId,
|
|
816
|
+
timestamp: Date.now()
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
await holosphere.putGlobal('federation_messages', tracking);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Gets all federated messages for a given original message
|
|
825
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
826
|
+
* @param {string} originalChatId - The ID of the original chat
|
|
827
|
+
* @param {string} messageId - The ID of the original message
|
|
828
|
+
* @returns {Promise<Object|null>} The tracking information for the message
|
|
829
|
+
*/
|
|
830
|
+
export async function getFederatedMessages(holosphere, originalChatId, messageId) {
|
|
831
|
+
const trackingKey = `${originalChatId}_${messageId}_fedmsgs`;
|
|
832
|
+
return await holosphere.getGlobal('federation_messages', trackingKey);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Updates a federated message across all federated chats
|
|
837
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
838
|
+
* @param {string} originalChatId - The ID of the original chat
|
|
839
|
+
* @param {string} messageId - The ID of the original message
|
|
840
|
+
* @param {Function} updateCallback - Function to update the message in each chat
|
|
841
|
+
* @returns {Promise<void>}
|
|
842
|
+
*/
|
|
843
|
+
export async function updateFederatedMessages(holosphere, originalChatId, messageId, updateCallback) {
|
|
844
|
+
const tracking = await getFederatedMessages(holosphere, originalChatId, messageId);
|
|
845
|
+
if (!tracking?.messages) return;
|
|
846
|
+
|
|
847
|
+
for (const msg of tracking.messages) {
|
|
848
|
+
try {
|
|
849
|
+
await updateCallback(msg.chatId, msg.messageId);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
console.warn(`Failed to update federated message in chat ${msg.chatId}:`, error);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Resets all federation relationships for a space
|
|
858
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
859
|
+
* @param {string} spaceId - The ID of the space to reset federation for
|
|
860
|
+
* @param {string} [password] - Optional password for the space
|
|
861
|
+
* @param {object} [options] - Reset options
|
|
862
|
+
* @param {boolean} [options.notifyPartners=true] - Whether to notify federation partners about the reset
|
|
863
|
+
* @param {string} [options.spaceName] - Optional name for the space (defaults to spaceId if not provided)
|
|
864
|
+
* @returns {Promise<object>} - Result object with success/error info
|
|
865
|
+
*/
|
|
866
|
+
export async function resetFederation(holosphere, spaceId, password = null, options = {}) {
|
|
867
|
+
if (!spaceId) {
|
|
868
|
+
throw new Error('resetFederation: Missing required space ID');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const {
|
|
872
|
+
notifyPartners = true,
|
|
873
|
+
spaceName = null
|
|
874
|
+
} = options;
|
|
875
|
+
|
|
876
|
+
const result = {
|
|
877
|
+
success: false,
|
|
878
|
+
federatedCount: 0,
|
|
879
|
+
notifyCount: 0,
|
|
880
|
+
partnersNotified: 0,
|
|
881
|
+
errors: []
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
// Get current federation info to know what we're clearing
|
|
886
|
+
const fedInfo = await getFederation(holosphere, spaceId, password);
|
|
887
|
+
|
|
888
|
+
if (!fedInfo) {
|
|
889
|
+
return {
|
|
890
|
+
...result,
|
|
891
|
+
success: true,
|
|
892
|
+
message: 'No federation configuration found for this space'
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Store counts for reporting
|
|
897
|
+
result.federatedCount = fedInfo.federation?.length || 0;
|
|
898
|
+
result.notifyCount = fedInfo.notify?.length || 0;
|
|
899
|
+
|
|
900
|
+
// Create empty federation record
|
|
901
|
+
const emptyFedInfo = {
|
|
902
|
+
id: spaceId,
|
|
903
|
+
name: spaceName || spaceId,
|
|
904
|
+
federation: [],
|
|
905
|
+
notify: [],
|
|
906
|
+
timestamp: Date.now()
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
// Update federation record with empty lists
|
|
910
|
+
await holosphere.putGlobal('federation', emptyFedInfo, password);
|
|
911
|
+
|
|
912
|
+
// Notify federation partners if requested
|
|
913
|
+
if (notifyPartners && fedInfo.federation && fedInfo.federation.length > 0) {
|
|
914
|
+
const updatePromises = fedInfo.federation.map(async (partnerSpace) => {
|
|
915
|
+
try {
|
|
916
|
+
// Get partner's federation info
|
|
917
|
+
const partnerFedInfo = await getFederation(holosphere, partnerSpace);
|
|
918
|
+
|
|
919
|
+
if (partnerFedInfo) {
|
|
920
|
+
// Remove this space from partner's federation list
|
|
921
|
+
if (partnerFedInfo.federation) {
|
|
922
|
+
partnerFedInfo.federation = partnerFedInfo.federation.filter(
|
|
923
|
+
id => id !== spaceId.toString()
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Remove this space from partner's notify list
|
|
928
|
+
if (partnerFedInfo.notify) {
|
|
929
|
+
partnerFedInfo.notify = partnerFedInfo.notify.filter(
|
|
930
|
+
id => id !== spaceId.toString()
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
partnerFedInfo.timestamp = Date.now();
|
|
935
|
+
|
|
936
|
+
// Save partner's updated federation info
|
|
937
|
+
await holosphere.putGlobal('federation', partnerFedInfo);
|
|
938
|
+
console.log(`Updated federation info for partner ${partnerSpace}`);
|
|
939
|
+
result.partnersNotified++;
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
return false;
|
|
943
|
+
} catch (error) {
|
|
944
|
+
console.warn(`Could not update federation info for partner ${partnerSpace}: ${error.message}`);
|
|
945
|
+
result.errors.push({
|
|
946
|
+
partner: partnerSpace,
|
|
947
|
+
error: error.message
|
|
948
|
+
});
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
await Promise.all(updatePromises);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Update federation metadata records if they exist
|
|
957
|
+
if (fedInfo.federation && fedInfo.federation.length > 0) {
|
|
958
|
+
for (const partnerSpace of fedInfo.federation) {
|
|
959
|
+
try {
|
|
960
|
+
const metaId = `${spaceId}_${partnerSpace}`;
|
|
961
|
+
const altMetaId = `${partnerSpace}_${spaceId}`;
|
|
962
|
+
|
|
963
|
+
const meta = await holosphere.getGlobal('federationMeta', metaId) ||
|
|
964
|
+
await holosphere.getGlobal('federationMeta', altMetaId);
|
|
965
|
+
|
|
966
|
+
if (meta) {
|
|
967
|
+
meta.status = 'inactive';
|
|
968
|
+
meta.endedAt = Date.now();
|
|
969
|
+
await holosphere.putGlobal('federationMeta', meta);
|
|
970
|
+
console.log(`Updated federation metadata for ${spaceId} and ${partnerSpace}`);
|
|
971
|
+
}
|
|
972
|
+
} catch (error) {
|
|
973
|
+
console.warn(`Could not update federation metadata for ${partnerSpace}: ${error.message}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
result.success = true;
|
|
979
|
+
return result;
|
|
980
|
+
} catch (error) {
|
|
981
|
+
console.error(`Federation reset failed: ${error.message}`);
|
|
982
|
+
return {
|
|
983
|
+
...result,
|
|
984
|
+
success: false,
|
|
985
|
+
error: error.message
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
}
|