holosphere 1.1.5 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FEDERATION.md +265 -0
- package/babel.config.js +5 -0
- package/federation.js +723 -0
- package/holosphere.js +855 -987
- package/package.json +3 -6
- package/test/ai.test.js +268 -76
- package/test/federation.test.js +115 -356
- package/test/holonauth.test.js +241 -0
- 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/services/{environmentalApi.test.js → environmentalApitest.js} +0 -0
package/federation.js
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation functionality for HoloSphere
|
|
3
|
+
* Provides methods for creating, managing, and using federated spaces
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a federation relationship between two spaces
|
|
8
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
9
|
+
* @param {string} spaceId1 - The first space ID
|
|
10
|
+
* @param {string} spaceId2 - The second space ID
|
|
11
|
+
* @param {string} [password1] - Optional password for the first space
|
|
12
|
+
* @param {string} [password2] - Optional password for the second space
|
|
13
|
+
* @returns {Promise<boolean>} - True if federation was created successfully
|
|
14
|
+
*/
|
|
15
|
+
export async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) {
|
|
16
|
+
if (!spaceId1 || !spaceId2) {
|
|
17
|
+
throw new Error('federate: Missing required space IDs');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Prevent self-federation
|
|
21
|
+
if (spaceId1 === spaceId2) {
|
|
22
|
+
throw new Error('Cannot federate a space with itself');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Verify access to both spaces before proceeding
|
|
26
|
+
let canAccessSpace1 = false;
|
|
27
|
+
let canAccessSpace2 = false;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Test authentication for first space
|
|
31
|
+
if (password1) {
|
|
32
|
+
try {
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
const user = holosphere.gun.user();
|
|
35
|
+
user.auth(holosphere.userName(spaceId1), password1, (ack) => {
|
|
36
|
+
if (ack.err) {
|
|
37
|
+
console.warn(`Authentication test for ${spaceId1} failed: ${ack.err}`);
|
|
38
|
+
reject(new Error(`Authentication failed for ${spaceId1}: ${ack.err}`));
|
|
39
|
+
} else {
|
|
40
|
+
canAccessSpace1 = true;
|
|
41
|
+
resolve();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Try to create the user if authentication fails
|
|
47
|
+
try {
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
const user = holosphere.gun.user();
|
|
50
|
+
user.create(holosphere.userName(spaceId1), password1, (ack) => {
|
|
51
|
+
if (ack.err && !ack.err.includes('already created')) {
|
|
52
|
+
reject(new Error(`User creation failed for ${spaceId1}: ${ack.err}`));
|
|
53
|
+
} else {
|
|
54
|
+
// Try to authenticate again
|
|
55
|
+
user.auth(holosphere.userName(spaceId1), password1, (authAck) => {
|
|
56
|
+
if (authAck.err) {
|
|
57
|
+
reject(new Error(`Authentication failed after creation for ${spaceId1}: ${authAck.err}`));
|
|
58
|
+
} else {
|
|
59
|
+
canAccessSpace1 = true;
|
|
60
|
+
resolve();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
} catch (createError) {
|
|
67
|
+
console.warn(`Could not create or authenticate user for ${spaceId1}: ${createError.message}`);
|
|
68
|
+
// Continue with limited functionality
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// No password required, assume we can access
|
|
73
|
+
canAccessSpace1 = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Test authentication for second space if password provided
|
|
77
|
+
if (password2) {
|
|
78
|
+
try {
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
const user = holosphere.gun.user();
|
|
81
|
+
user.auth(holosphere.userName(spaceId2), password2, (ack) => {
|
|
82
|
+
if (ack.err) {
|
|
83
|
+
console.warn(`Authentication test for ${spaceId2} failed: ${ack.err}`);
|
|
84
|
+
reject(new Error(`Authentication failed for ${spaceId2}: ${ack.err}`));
|
|
85
|
+
} else {
|
|
86
|
+
canAccessSpace2 = true;
|
|
87
|
+
resolve();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Try to create the user if authentication fails
|
|
93
|
+
try {
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
const user = holosphere.gun.user();
|
|
96
|
+
user.create(holosphere.userName(spaceId2), password2, (ack) => {
|
|
97
|
+
if (ack.err && !ack.err.includes('already created')) {
|
|
98
|
+
reject(new Error(`User creation failed for ${spaceId2}: ${ack.err}`));
|
|
99
|
+
} else {
|
|
100
|
+
// Try to authenticate again
|
|
101
|
+
user.auth(holosphere.userName(spaceId2), password2, (authAck) => {
|
|
102
|
+
if (authAck.err) {
|
|
103
|
+
reject(new Error(`Authentication failed after creation for ${spaceId2}: ${authAck.err}`));
|
|
104
|
+
} else {
|
|
105
|
+
canAccessSpace2 = true;
|
|
106
|
+
resolve();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
} catch (createError) {
|
|
113
|
+
console.warn(`Could not create or authenticate user for ${spaceId2}: ${createError.message}`);
|
|
114
|
+
// Continue with limited functionality
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// No password required, assume we can access
|
|
119
|
+
canAccessSpace2 = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Warn if we can't access one or both spaces
|
|
123
|
+
if (!canAccessSpace1) {
|
|
124
|
+
console.warn(`Limited access to space ${spaceId1} - federation may be incomplete`);
|
|
125
|
+
}
|
|
126
|
+
if (password2 && !canAccessSpace2) {
|
|
127
|
+
console.warn(`Limited access to space ${spaceId2} - federation may be incomplete`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get existing federation info for both spaces
|
|
131
|
+
let fedInfo1 = null;
|
|
132
|
+
let fedInfo2 = null;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
|
|
138
|
+
// Create new federation info if we couldn't get existing
|
|
139
|
+
fedInfo1 = {
|
|
140
|
+
id: spaceId1,
|
|
141
|
+
name: spaceId1,
|
|
142
|
+
federation: [],
|
|
143
|
+
notify: [],
|
|
144
|
+
timestamp: Date.now()
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (password2) {
|
|
149
|
+
try {
|
|
150
|
+
fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
|
|
153
|
+
// Create new federation info if we couldn't get existing
|
|
154
|
+
fedInfo2 = {
|
|
155
|
+
id: spaceId2,
|
|
156
|
+
name: spaceId2,
|
|
157
|
+
federation: [],
|
|
158
|
+
notify: [],
|
|
159
|
+
timestamp: Date.now()
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check if federation already exists
|
|
165
|
+
if (fedInfo1 && fedInfo1.federation && fedInfo1.federation.includes(spaceId2)) {
|
|
166
|
+
console.log(`Federation already exists between ${spaceId1} and ${spaceId2}`);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Create or update federation info for first space
|
|
171
|
+
if (!fedInfo1) {
|
|
172
|
+
fedInfo1 = {
|
|
173
|
+
id: spaceId1,
|
|
174
|
+
name: spaceId1,
|
|
175
|
+
federation: [],
|
|
176
|
+
notify: [],
|
|
177
|
+
timestamp: Date.now()
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (!fedInfo1.federation) fedInfo1.federation = [];
|
|
181
|
+
if (!fedInfo1.federation.includes(spaceId2)) {
|
|
182
|
+
fedInfo1.federation.push(spaceId2);
|
|
183
|
+
}
|
|
184
|
+
fedInfo1.timestamp = Date.now();
|
|
185
|
+
|
|
186
|
+
// Create or update federation info for second space
|
|
187
|
+
if (password2 && canAccessSpace2) {
|
|
188
|
+
if (!fedInfo2) {
|
|
189
|
+
fedInfo2 = {
|
|
190
|
+
id: spaceId2,
|
|
191
|
+
name: spaceId2,
|
|
192
|
+
federation: [],
|
|
193
|
+
notify: [],
|
|
194
|
+
timestamp: Date.now()
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (!fedInfo2.notify) fedInfo2.notify = [];
|
|
198
|
+
if (!fedInfo2.notify.includes(spaceId1)) {
|
|
199
|
+
fedInfo2.notify.push(spaceId1);
|
|
200
|
+
}
|
|
201
|
+
fedInfo2.timestamp = Date.now();
|
|
202
|
+
|
|
203
|
+
// Save second federation record if we have password and access
|
|
204
|
+
try {
|
|
205
|
+
await holosphere.putGlobal('federation', fedInfo2, password2);
|
|
206
|
+
console.log(`Updated federation info for ${spaceId2}`);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Save first federation record
|
|
213
|
+
try {
|
|
214
|
+
await holosphere.putGlobal('federation', fedInfo1, password1);
|
|
215
|
+
console.log(`Updated federation info for ${spaceId1}`);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
|
|
218
|
+
throw new Error(`Failed to create federation: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create federation metadata record
|
|
222
|
+
const federationMeta = {
|
|
223
|
+
id: `${spaceId1}_${spaceId2}`,
|
|
224
|
+
space1: spaceId1,
|
|
225
|
+
space2: spaceId2,
|
|
226
|
+
created: Date.now(),
|
|
227
|
+
status: 'active'
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
await holosphere.putGlobal('federationMeta', federationMeta);
|
|
232
|
+
console.log(`Created federation metadata for ${spaceId1} and ${spaceId2}`);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.warn(`Could not create federation metadata: ${error.message}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return true;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error(`Federation creation failed: ${error.message}`);
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Subscribes to federation notifications for a space
|
|
246
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
247
|
+
* @param {string} spaceId - The space ID to subscribe to
|
|
248
|
+
* @param {string} [password] - Optional password for the space
|
|
249
|
+
* @param {function} callback - The callback to execute on notifications
|
|
250
|
+
* @param {object} [options] - Subscription options
|
|
251
|
+
* @param {string[]} [options.lenses] - Specific lenses to subscribe to (default: all)
|
|
252
|
+
* @param {number} [options.throttle] - Throttle notifications in ms (default: 0)
|
|
253
|
+
* @returns {Promise<object>} - Subscription object with unsubscribe() method
|
|
254
|
+
*/
|
|
255
|
+
export async function subscribeFederation(holosphere, spaceId, password = null, callback, options = {}) {
|
|
256
|
+
if (!spaceId || !callback) {
|
|
257
|
+
throw new Error('subscribeFederation: Missing required parameters');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { lenses = ['*'], throttle = 0 } = options;
|
|
261
|
+
|
|
262
|
+
// Get federation info
|
|
263
|
+
const fedInfo = await holosphere.getGlobal('federation', spaceId, password);
|
|
264
|
+
if (!fedInfo) {
|
|
265
|
+
throw new Error('No federation info found for space');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Create subscription for each federated space
|
|
269
|
+
const subscriptions = [];
|
|
270
|
+
let lastNotificationTime = {};
|
|
271
|
+
|
|
272
|
+
if (fedInfo.federation && fedInfo.federation.length > 0) {
|
|
273
|
+
for (const federatedSpace of fedInfo.federation) {
|
|
274
|
+
// For each lens specified (or all if '*')
|
|
275
|
+
for (const lens of lenses) {
|
|
276
|
+
try {
|
|
277
|
+
const sub = await holosphere.subscribe(federatedSpace, lens, async (data) => {
|
|
278
|
+
try {
|
|
279
|
+
// Skip if data is missing or not from federated space
|
|
280
|
+
if (!data || !data.id) return;
|
|
281
|
+
|
|
282
|
+
// Apply throttling if configured
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
const key = `${federatedSpace}_${lens}_${data.id}`;
|
|
285
|
+
|
|
286
|
+
if (throttle > 0) {
|
|
287
|
+
if (lastNotificationTime[key] &&
|
|
288
|
+
(now - lastNotificationTime[key]) < throttle) {
|
|
289
|
+
return; // Skip this notification (throttled)
|
|
290
|
+
}
|
|
291
|
+
lastNotificationTime[key] = now;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Add federation metadata if not present
|
|
295
|
+
if (!data.federation) {
|
|
296
|
+
data.federation = {
|
|
297
|
+
origin: federatedSpace,
|
|
298
|
+
timestamp: now
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Execute callback with the data
|
|
303
|
+
await callback(data, federatedSpace, lens);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.warn('Federation notification error:', error);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (sub && typeof sub.unsubscribe === 'function') {
|
|
310
|
+
subscriptions.push(sub);
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.warn(`Error creating subscription for ${federatedSpace}/${lens}:`, error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Return combined subscription object
|
|
320
|
+
return {
|
|
321
|
+
unsubscribe: () => {
|
|
322
|
+
subscriptions.forEach(sub => {
|
|
323
|
+
try {
|
|
324
|
+
if (sub && typeof sub.unsubscribe === 'function') {
|
|
325
|
+
sub.unsubscribe();
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.warn('Error unsubscribing:', error);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
// Clear the subscriptions array
|
|
332
|
+
subscriptions.length = 0;
|
|
333
|
+
// Clear throttling data
|
|
334
|
+
lastNotificationTime = {};
|
|
335
|
+
},
|
|
336
|
+
getSubscriptionCount: () => subscriptions.length
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Gets federation info for a space
|
|
342
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
343
|
+
* @param {string} spaceId - The space ID
|
|
344
|
+
* @param {string} [password] - Optional password for the space
|
|
345
|
+
* @returns {Promise<object|null>} - Federation info or null if not found
|
|
346
|
+
*/
|
|
347
|
+
export async function getFederation(holosphere, spaceId, password = null) {
|
|
348
|
+
if (!spaceId) {
|
|
349
|
+
throw new Error('getFederation: Missing space ID');
|
|
350
|
+
}
|
|
351
|
+
return await holosphere.getGlobal('federation', spaceId, password);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Removes a federation relationship between spaces
|
|
356
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
357
|
+
* @param {string} spaceId1 - The first space ID
|
|
358
|
+
* @param {string} spaceId2 - The second space ID
|
|
359
|
+
* @param {string} [password1] - Optional password for the first space
|
|
360
|
+
* @param {string} [password2] - Optional password for the second space
|
|
361
|
+
* @returns {Promise<boolean>} - True if federation was removed successfully
|
|
362
|
+
*/
|
|
363
|
+
export async function unfederate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null) {
|
|
364
|
+
if (!spaceId1 || !spaceId2) {
|
|
365
|
+
throw new Error('unfederate: Missing required space IDs');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
// Get federation info for first space
|
|
370
|
+
let fedInfo1 = null;
|
|
371
|
+
try {
|
|
372
|
+
fedInfo1 = await holosphere.getGlobal('federation', spaceId1, password1);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.warn(`Could not get federation info for ${spaceId1}: ${error.message}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!fedInfo1 || !fedInfo1.federation) {
|
|
378
|
+
console.warn(`Federation not found for space ${spaceId1}`);
|
|
379
|
+
// Continue anyway to clean up any potential metadata
|
|
380
|
+
} else {
|
|
381
|
+
// Update first space federation info
|
|
382
|
+
fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
|
|
383
|
+
fedInfo1.timestamp = Date.now();
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await holosphere.putGlobal('federation', fedInfo1, password1);
|
|
387
|
+
console.log(`Updated federation info for ${spaceId1}`);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn(`Could not update federation info for ${spaceId1}: ${error.message}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Update second space federation info if password provided
|
|
394
|
+
if (password2) {
|
|
395
|
+
let fedInfo2 = null;
|
|
396
|
+
try {
|
|
397
|
+
fedInfo2 = await holosphere.getGlobal('federation', spaceId2, password2);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.warn(`Could not get federation info for ${spaceId2}: ${error.message}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (fedInfo2 && fedInfo2.notify) {
|
|
403
|
+
fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
|
|
404
|
+
fedInfo2.timestamp = Date.now();
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
await holosphere.putGlobal('federation', fedInfo2, password2);
|
|
408
|
+
console.log(`Updated federation info for ${spaceId2}`);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.warn(`Could not update federation info for ${spaceId2}: ${error.message}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Update federation metadata
|
|
416
|
+
const metaId = `${spaceId1}_${spaceId2}`;
|
|
417
|
+
const altMetaId = `${spaceId2}_${spaceId1}`;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const meta = await holosphere.getGlobal('federationMeta', metaId) ||
|
|
421
|
+
await holosphere.getGlobal('federationMeta', altMetaId);
|
|
422
|
+
|
|
423
|
+
if (meta) {
|
|
424
|
+
meta.status = 'inactive';
|
|
425
|
+
meta.endedAt = Date.now();
|
|
426
|
+
await holosphere.putGlobal('federationMeta', meta);
|
|
427
|
+
console.log(`Updated federation metadata for ${spaceId1} and ${spaceId2}`);
|
|
428
|
+
}
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.warn(`Could not update federation metadata: ${error.message}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return true;
|
|
434
|
+
} catch (error) {
|
|
435
|
+
console.error(`Federation removal failed: ${error.message}`);
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Gets data from a holon and lens, including data from federated spaces with optional aggregation
|
|
442
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
443
|
+
* @param {string} holon - The holon identifier
|
|
444
|
+
* @param {string} lens - The lens identifier
|
|
445
|
+
* @param {object} options - Options for data retrieval and aggregation
|
|
446
|
+
* @param {boolean} [options.aggregate=false] - Whether to aggregate data
|
|
447
|
+
* @param {string} [options.idField='id'] - Field to use as unique identifier
|
|
448
|
+
* @param {string[]} [options.sumFields=[]] - Fields to sum when aggregating
|
|
449
|
+
* @param {string[]} [options.concatArrays=[]] - Array fields to concatenate
|
|
450
|
+
* @param {boolean} [options.removeDuplicates=true] - Whether to remove duplicates
|
|
451
|
+
* @param {function} [options.mergeStrategy=null] - Custom merge function
|
|
452
|
+
* @param {boolean} [options.includeLocal=true] - Whether to include local data
|
|
453
|
+
* @param {boolean} [options.includeFederated=true] - Whether to include federated data
|
|
454
|
+
* @param {string} [password] - Optional password for accessing private data
|
|
455
|
+
* @returns {Promise<Array>} - Combined array of local and federated data
|
|
456
|
+
*/
|
|
457
|
+
export async function getFederated(holosphere, holon, lens, options = {}, password = null) {
|
|
458
|
+
// Validate required parameters
|
|
459
|
+
if (!holon || !lens) {
|
|
460
|
+
throw new Error('getFederated: Missing required parameters');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const {
|
|
464
|
+
aggregate = false,
|
|
465
|
+
idField = 'id',
|
|
466
|
+
sumFields = [],
|
|
467
|
+
concatArrays = [],
|
|
468
|
+
removeDuplicates = true,
|
|
469
|
+
mergeStrategy = null,
|
|
470
|
+
includeLocal = true,
|
|
471
|
+
includeFederated = true,
|
|
472
|
+
maxFederatedSpaces = 10,
|
|
473
|
+
timeout = 5000
|
|
474
|
+
} = options;
|
|
475
|
+
|
|
476
|
+
// Get federation info for current space
|
|
477
|
+
// Use holon as the space ID
|
|
478
|
+
const spaceId = holon;
|
|
479
|
+
const fedInfo = await getFederation(holosphere, spaceId, password);
|
|
480
|
+
|
|
481
|
+
// Initialize result array
|
|
482
|
+
let allData = [];
|
|
483
|
+
|
|
484
|
+
// Get local data if requested
|
|
485
|
+
if (includeLocal) {
|
|
486
|
+
const localData = await holosphere.getAll(holon, lens, password);
|
|
487
|
+
allData = [...localData];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// If federation is disabled or no federation exists, return local data only
|
|
491
|
+
if (!includeFederated || !fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
492
|
+
return allData;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Limit number of federated spaces to query
|
|
496
|
+
const federatedSpaces = fedInfo.federation.slice(0, maxFederatedSpaces);
|
|
497
|
+
|
|
498
|
+
// Get data from each federated space with timeout
|
|
499
|
+
const federatedDataPromises = federatedSpaces.map(async (federatedSpace) => {
|
|
500
|
+
try {
|
|
501
|
+
// Create a promise that rejects after timeout
|
|
502
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
503
|
+
setTimeout(() => reject(new Error('Federation request timed out')), timeout);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Create the actual data fetch promise
|
|
507
|
+
const dataPromise = holosphere.getAll(federatedSpace, lens, password);
|
|
508
|
+
|
|
509
|
+
// Race the promises
|
|
510
|
+
const data = await Promise.race([dataPromise, timeoutPromise]);
|
|
511
|
+
|
|
512
|
+
// Add federation metadata to each item
|
|
513
|
+
return (data || []).map(item => ({
|
|
514
|
+
...item,
|
|
515
|
+
federation: {
|
|
516
|
+
...item.federation,
|
|
517
|
+
origin: federatedSpace,
|
|
518
|
+
timestamp: item.federation?.timestamp || Date.now()
|
|
519
|
+
}
|
|
520
|
+
}));
|
|
521
|
+
} catch (error) {
|
|
522
|
+
console.warn(`Error getting data from federated space ${federatedSpace}:`, error);
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Wait for all federated data requests to complete
|
|
528
|
+
const federatedResults = await Promise.allSettled(federatedDataPromises);
|
|
529
|
+
|
|
530
|
+
// Add successful results to allData
|
|
531
|
+
federatedResults.forEach(result => {
|
|
532
|
+
if (result.status === 'fulfilled') {
|
|
533
|
+
allData = [...allData, ...result.value];
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// If aggregating, use enhanced aggregation logic
|
|
538
|
+
if (aggregate) {
|
|
539
|
+
const aggregated = new Map();
|
|
540
|
+
|
|
541
|
+
for (const item of allData) {
|
|
542
|
+
const itemId = item[idField];
|
|
543
|
+
if (!itemId) continue;
|
|
544
|
+
|
|
545
|
+
const existing = aggregated.get(itemId);
|
|
546
|
+
if (!existing) {
|
|
547
|
+
aggregated.set(itemId, { ...item });
|
|
548
|
+
} else {
|
|
549
|
+
// If custom merge strategy is provided, use it
|
|
550
|
+
if (mergeStrategy && typeof mergeStrategy === 'function') {
|
|
551
|
+
aggregated.set(itemId, mergeStrategy(existing, item));
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Enhanced default merge strategy
|
|
556
|
+
const merged = { ...existing };
|
|
557
|
+
|
|
558
|
+
// Sum numeric fields
|
|
559
|
+
for (const field of sumFields) {
|
|
560
|
+
if (typeof item[field] === 'number') {
|
|
561
|
+
merged[field] = (merged[field] || 0) + (item[field] || 0);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Concatenate and deduplicate array fields
|
|
566
|
+
for (const field of concatArrays) {
|
|
567
|
+
if (Array.isArray(item[field])) {
|
|
568
|
+
const combinedArray = [
|
|
569
|
+
...(merged[field] || []),
|
|
570
|
+
...(item[field] || [])
|
|
571
|
+
];
|
|
572
|
+
// Remove duplicates if elements are primitive
|
|
573
|
+
merged[field] = Array.from(new Set(combinedArray));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Update federation metadata
|
|
578
|
+
merged.federation = {
|
|
579
|
+
...merged.federation,
|
|
580
|
+
timestamp: Math.max(
|
|
581
|
+
merged.federation?.timestamp || 0,
|
|
582
|
+
item.federation?.timestamp || 0
|
|
583
|
+
),
|
|
584
|
+
origins: Array.from(new Set([
|
|
585
|
+
...(Array.isArray(merged.federation?.origins) ? merged.federation.origins :
|
|
586
|
+
(merged.federation?.origin ? [merged.federation.origin] : [])),
|
|
587
|
+
...(Array.isArray(item.federation?.origins) ? item.federation.origins :
|
|
588
|
+
(item.federation?.origin ? [item.federation.origin] : []))
|
|
589
|
+
]))
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// Update the aggregated item
|
|
593
|
+
aggregated.set(itemId, merged);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return Array.from(aggregated.values());
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// If not aggregating, optionally remove duplicates based on idField
|
|
601
|
+
if (!removeDuplicates) {
|
|
602
|
+
return allData;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Remove duplicates keeping the most recent version
|
|
606
|
+
const uniqueMap = new Map();
|
|
607
|
+
allData.forEach(item => {
|
|
608
|
+
const id = item[idField];
|
|
609
|
+
if (!id) return;
|
|
610
|
+
|
|
611
|
+
const existing = uniqueMap.get(id);
|
|
612
|
+
if (!existing ||
|
|
613
|
+
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
614
|
+
uniqueMap.set(id, item);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
return Array.from(uniqueMap.values());
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Propagates data to federated spaces
|
|
622
|
+
* @param {object} holosphere - The HoloSphere instance
|
|
623
|
+
* @param {string} holon - The holon identifier
|
|
624
|
+
* @param {string} lens - The lens identifier
|
|
625
|
+
* @param {object} data - The data to propagate
|
|
626
|
+
* @param {object} [options] - Propagation options
|
|
627
|
+
* @param {string[]} [options.targetSpaces] - Specific spaces to propagate to (default: all federated)
|
|
628
|
+
* @param {boolean} [options.addFederationMetadata=true] - Whether to add federation metadata
|
|
629
|
+
* @returns {Promise<object>} - Result with success count and errors
|
|
630
|
+
*/
|
|
631
|
+
export async function propagateToFederation(holosphere, holon, lens, data, options = {}) {
|
|
632
|
+
if (!holon || !lens || !data) {
|
|
633
|
+
throw new Error('propagateToFederation: Missing required parameters');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (!data.id) {
|
|
637
|
+
data.id = holosphere.generateId();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const {
|
|
641
|
+
targetSpaces = null,
|
|
642
|
+
addFederationMetadata = true
|
|
643
|
+
} = options;
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
// Get federation info for current space
|
|
647
|
+
// Use holon as the space ID
|
|
648
|
+
const spaceId = holon;
|
|
649
|
+
const fedInfo = await getFederation(holosphere, spaceId);
|
|
650
|
+
if (!fedInfo || !fedInfo.notify || fedInfo.notify.length === 0) {
|
|
651
|
+
return { success: 0, errors: 0, message: 'No federation to propagate to' };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Determine which spaces to propagate to
|
|
655
|
+
const spacesToNotify = targetSpaces ?
|
|
656
|
+
fedInfo.notify.filter(spaceId => targetSpaces.includes(spaceId)) :
|
|
657
|
+
fedInfo.notify;
|
|
658
|
+
|
|
659
|
+
if (spacesToNotify.length === 0) {
|
|
660
|
+
return { success: 0, errors: 0, message: 'No matching spaces to propagate to' };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Add federation metadata if requested
|
|
664
|
+
const dataToPropagate = { ...data };
|
|
665
|
+
if (addFederationMetadata) {
|
|
666
|
+
dataToPropagate.federation = {
|
|
667
|
+
...dataToPropagate.federation,
|
|
668
|
+
origin: spaceId,
|
|
669
|
+
timestamp: Date.now()
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Track results
|
|
674
|
+
const results = {
|
|
675
|
+
success: 0,
|
|
676
|
+
errors: 0,
|
|
677
|
+
errorDetails: []
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// Propagate to each federated space
|
|
681
|
+
const propagationPromises = spacesToNotify.map(spaceId =>
|
|
682
|
+
new Promise((resolve) => {
|
|
683
|
+
try {
|
|
684
|
+
// Store data in the federated space's lens
|
|
685
|
+
holosphere.gun.get(holosphere.appname)
|
|
686
|
+
.get(spaceId)
|
|
687
|
+
.get(lens)
|
|
688
|
+
.get(dataToPropagate.id)
|
|
689
|
+
.put(JSON.stringify(dataToPropagate), ack => {
|
|
690
|
+
if (ack.err) {
|
|
691
|
+
results.errors++;
|
|
692
|
+
results.errorDetails.push({
|
|
693
|
+
space: spaceId,
|
|
694
|
+
error: ack.err
|
|
695
|
+
});
|
|
696
|
+
} else {
|
|
697
|
+
results.success++;
|
|
698
|
+
}
|
|
699
|
+
resolve();
|
|
700
|
+
});
|
|
701
|
+
} catch (error) {
|
|
702
|
+
results.errors++;
|
|
703
|
+
results.errorDetails.push({
|
|
704
|
+
space: spaceId,
|
|
705
|
+
error: error.message
|
|
706
|
+
});
|
|
707
|
+
resolve();
|
|
708
|
+
}
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
await Promise.all(propagationPromises);
|
|
713
|
+
return results;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
console.warn('Federation propagation error:', error);
|
|
716
|
+
return {
|
|
717
|
+
success: 0,
|
|
718
|
+
errors: 1,
|
|
719
|
+
message: error.message,
|
|
720
|
+
errorDetails: [{ error: error.message }]
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|