holosphere 1.1.2 → 1.1.4
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/holosphere.js +1143 -281
- package/package.json +3 -2
- package/test/ai.test.js +233 -0
- package/test/federation.test.js +363 -0
- package/test/holosphere.test.js +681 -133
- package/test/spacesauth.test.js +335 -0
package/holosphere.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as h3 from 'h3-js';
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
3
|
import Gun from 'gun'
|
|
4
|
+
import SEA from 'gun/sea.js'
|
|
4
5
|
import Ajv2019 from 'ajv/dist/2019.js'
|
|
5
6
|
|
|
6
7
|
|
|
@@ -10,8 +11,9 @@ class HoloSphere {
|
|
|
10
11
|
* @param {string} appname - The name of the application.
|
|
11
12
|
* @param {boolean} strict - Whether to enforce strict schema validation.
|
|
12
13
|
* @param {string|null} openaikey - The OpenAI API key.
|
|
14
|
+
* @param {Gun|null} gunInstance - The Gun instance to use.
|
|
13
15
|
*/
|
|
14
|
-
constructor(appname, strict = false, openaikey = null) {
|
|
16
|
+
constructor(appname, strict = false, openaikey = null, gunInstance = null) {
|
|
15
17
|
this.appname = appname
|
|
16
18
|
this.strict = strict;
|
|
17
19
|
this.validator = new Ajv2019({
|
|
@@ -19,7 +21,9 @@ class HoloSphere {
|
|
|
19
21
|
strict: false, // Keep this false to avoid Ajv strict mode issues
|
|
20
22
|
validateSchema: true // Always validate schemas
|
|
21
23
|
});
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
// Use provided Gun instance or create new one
|
|
26
|
+
this.gun = gunInstance || Gun({
|
|
23
27
|
peers: ['https://gun.holons.io/gun', 'https://59.src.eco/gun'],
|
|
24
28
|
axe: false,
|
|
25
29
|
// uuid: (content) => { // generate a unique id for each node
|
|
@@ -27,13 +31,20 @@ class HoloSphere {
|
|
|
27
31
|
// return content;}
|
|
28
32
|
});
|
|
29
33
|
|
|
34
|
+
// Initialize SEA
|
|
35
|
+
this.sea = SEA;
|
|
36
|
+
|
|
30
37
|
if (openaikey != null) {
|
|
31
38
|
this.openai = new OpenAI({
|
|
32
39
|
apiKey: openaikey,
|
|
33
40
|
});
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
// Add currentSpace property to track logged in space
|
|
44
|
+
this.currentSpace = null;
|
|
45
|
+
|
|
46
|
+
// Initialize subscriptions
|
|
47
|
+
this.subscriptions = {};
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
// ================================ SCHEMA FUNCTIONS ================================
|
|
@@ -94,14 +105,22 @@ class HoloSphere {
|
|
|
94
105
|
return new Promise((resolve, reject) => {
|
|
95
106
|
try {
|
|
96
107
|
const schemaString = JSON.stringify(schema);
|
|
108
|
+
const schemaData = {
|
|
109
|
+
schema: schemaString,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
// Only set owner if there's an authenticated space
|
|
112
|
+
...(this.currentSpace && { owner: this.currentSpace.alias })
|
|
113
|
+
};
|
|
114
|
+
|
|
97
115
|
this.gun.get(this.appname)
|
|
98
116
|
.get(lens)
|
|
99
117
|
.get('schema')
|
|
100
|
-
.put(
|
|
118
|
+
.put(schemaData, ack => {
|
|
101
119
|
if (ack.err) {
|
|
102
120
|
reject(new Error(ack.err));
|
|
103
121
|
} else {
|
|
104
|
-
|
|
122
|
+
// Add small delay to ensure data is written
|
|
123
|
+
setTimeout(() => resolve(true), 50);
|
|
105
124
|
}
|
|
106
125
|
});
|
|
107
126
|
} catch (error) {
|
|
@@ -121,27 +140,31 @@ class HoloSphere {
|
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
return new Promise((resolve) => {
|
|
143
|
+
let timeout = setTimeout(() => {
|
|
144
|
+
console.warn('getSchema: Operation timed out');
|
|
145
|
+
resolve(null);
|
|
146
|
+
}, 5000);
|
|
147
|
+
|
|
124
148
|
this.gun.get(this.appname)
|
|
125
149
|
.get(lens)
|
|
126
150
|
.get('schema')
|
|
127
151
|
.once(data => {
|
|
152
|
+
clearTimeout(timeout);
|
|
128
153
|
if (!data) {
|
|
129
154
|
resolve(null);
|
|
130
155
|
return;
|
|
131
156
|
}
|
|
132
157
|
|
|
133
158
|
try {
|
|
134
|
-
// Handle both
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
typeof v === 'string' && v.includes('"type":'));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (schemaStr) {
|
|
142
|
-
resolve(JSON.parse(schemaStr));
|
|
159
|
+
// Handle both new format and legacy format
|
|
160
|
+
if (data.schema) {
|
|
161
|
+
// New format with timestamp
|
|
162
|
+
resolve(JSON.parse(data.schema));
|
|
143
163
|
} else {
|
|
144
|
-
|
|
164
|
+
// Legacy format or direct string
|
|
165
|
+
const schemaStr = typeof data === 'string' ? data :
|
|
166
|
+
Object.values(data).find(v => typeof v === 'string' && v.includes('"type":'));
|
|
167
|
+
resolve(schemaStr ? JSON.parse(schemaStr) : null);
|
|
145
168
|
}
|
|
146
169
|
} catch (error) {
|
|
147
170
|
console.error('getSchema: Error parsing schema:', error);
|
|
@@ -161,39 +184,74 @@ class HoloSphere {
|
|
|
161
184
|
* @returns {Promise<boolean>} - Returns true if successful, false if there was an error
|
|
162
185
|
*/
|
|
163
186
|
async put(holon, lens, data) {
|
|
164
|
-
|
|
187
|
+
// Check authentication for data operations
|
|
188
|
+
if (!this.currentSpace) {
|
|
189
|
+
throw new Error('Unauthorized to modify this data');
|
|
190
|
+
}
|
|
191
|
+
this._checkSession();
|
|
192
|
+
|
|
193
|
+
// If updating existing data, check ownership
|
|
194
|
+
if (data.id) {
|
|
195
|
+
const existing = await this.get(holon, lens, data.id);
|
|
196
|
+
if (existing && existing.owner &&
|
|
197
|
+
existing.owner !== this.currentSpace.alias &&
|
|
198
|
+
!existing.federation) { // Skip ownership check for federated data
|
|
199
|
+
throw new Error('Unauthorized to modify this data');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Add owner and federation information to data
|
|
204
|
+
const dataWithMeta = {
|
|
205
|
+
...data,
|
|
206
|
+
owner: this.currentSpace.alias,
|
|
207
|
+
federation: {
|
|
208
|
+
origin: this.currentSpace.alias,
|
|
209
|
+
timestamp: Date.now()
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (!holon || !lens || !dataWithMeta) {
|
|
165
214
|
throw new Error('put: Missing required parameters');
|
|
166
215
|
}
|
|
167
216
|
|
|
168
|
-
if (!
|
|
169
|
-
|
|
217
|
+
if (!dataWithMeta.id) {
|
|
218
|
+
dataWithMeta.id = this.generateId();
|
|
170
219
|
}
|
|
171
220
|
|
|
221
|
+
// Get and validate schema first
|
|
172
222
|
const schema = await this.getSchema(lens);
|
|
173
223
|
if (schema) {
|
|
174
|
-
//
|
|
175
|
-
const dataToValidate = JSON.parse(JSON.stringify(
|
|
176
|
-
|
|
177
|
-
// Validate against schema
|
|
224
|
+
// Deep clone data to avoid modifying the original
|
|
225
|
+
const dataToValidate = JSON.parse(JSON.stringify(dataWithMeta));
|
|
178
226
|
const valid = this.validator.validate(schema, dataToValidate);
|
|
227
|
+
|
|
179
228
|
if (!valid) {
|
|
180
|
-
|
|
229
|
+
const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
|
|
230
|
+
// Always throw on schema validation failure, regardless of strict mode
|
|
231
|
+
throw new Error(errorMsg);
|
|
181
232
|
}
|
|
182
233
|
} else if (this.strict) {
|
|
183
234
|
throw new Error('Schema required in strict mode');
|
|
184
235
|
}
|
|
185
236
|
|
|
186
|
-
|
|
237
|
+
// Store data in current space
|
|
238
|
+
const putResult = await new Promise((resolve, reject) => {
|
|
187
239
|
try {
|
|
188
|
-
const payload = JSON.stringify(
|
|
240
|
+
const payload = JSON.stringify(dataWithMeta);
|
|
189
241
|
this.gun.get(this.appname)
|
|
190
242
|
.get(holon)
|
|
191
243
|
.get(lens)
|
|
192
|
-
.get(
|
|
244
|
+
.get(dataWithMeta.id)
|
|
193
245
|
.put(payload, ack => {
|
|
194
246
|
if (ack.err) {
|
|
195
247
|
reject(new Error(ack.err));
|
|
196
248
|
} else {
|
|
249
|
+
// Notify subscribers after successful put
|
|
250
|
+
this.notifySubscribers({
|
|
251
|
+
holon,
|
|
252
|
+
lens,
|
|
253
|
+
...dataWithMeta
|
|
254
|
+
});
|
|
197
255
|
resolve(true);
|
|
198
256
|
}
|
|
199
257
|
});
|
|
@@ -201,6 +259,71 @@ class HoloSphere {
|
|
|
201
259
|
reject(error);
|
|
202
260
|
}
|
|
203
261
|
});
|
|
262
|
+
|
|
263
|
+
// If successful, propagate to federated spaces
|
|
264
|
+
if (putResult) {
|
|
265
|
+
await this._propagateToFederation(holon, lens, dataWithMeta);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return putResult;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Propagates data to federated spaces
|
|
273
|
+
* @private
|
|
274
|
+
* @param {string} holon - The holon identifier
|
|
275
|
+
* @param {string} lens - The lens identifier
|
|
276
|
+
* @param {object} data - The data to propagate
|
|
277
|
+
*/
|
|
278
|
+
async _propagateToFederation(holon, lens, data) {
|
|
279
|
+
try {
|
|
280
|
+
// Get federation info for current space
|
|
281
|
+
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
282
|
+
if (!fedInfo || !fedInfo.notify || fedInfo.notify.length === 0) {
|
|
283
|
+
return; // No federation to propagate to
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Propagate to each federated space
|
|
287
|
+
const propagationPromises = fedInfo.notify.map(spaceId =>
|
|
288
|
+
new Promise((resolve) => {
|
|
289
|
+
// Store data in the federated space's lens
|
|
290
|
+
this.gun.get(this.appname)
|
|
291
|
+
.get(spaceId)
|
|
292
|
+
.get(lens)
|
|
293
|
+
.get(data.id)
|
|
294
|
+
.put(JSON.stringify({
|
|
295
|
+
...data,
|
|
296
|
+
federation: {
|
|
297
|
+
...data.federation,
|
|
298
|
+
notified: Date.now()
|
|
299
|
+
}
|
|
300
|
+
}), ack => {
|
|
301
|
+
if (ack.err) {
|
|
302
|
+
console.warn(`Failed to propagate to space ${spaceId}:`, ack.err);
|
|
303
|
+
}
|
|
304
|
+
resolve();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Also store in federation lens for notifications
|
|
308
|
+
this.gun.get(this.appname)
|
|
309
|
+
.get(spaceId)
|
|
310
|
+
.get('federation')
|
|
311
|
+
.get(data.id)
|
|
312
|
+
.put(JSON.stringify({
|
|
313
|
+
...data,
|
|
314
|
+
federation: {
|
|
315
|
+
...data.federation,
|
|
316
|
+
notified: Date.now()
|
|
317
|
+
}
|
|
318
|
+
}));
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
await Promise.all(propagationPromises);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.warn('Federation propagation error:', error);
|
|
325
|
+
// Don't throw here to avoid failing the original put
|
|
326
|
+
}
|
|
204
327
|
}
|
|
205
328
|
|
|
206
329
|
/**
|
|
@@ -219,53 +342,225 @@ class HoloSphere {
|
|
|
219
342
|
throw new Error('getAll: Schema required in strict mode');
|
|
220
343
|
}
|
|
221
344
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const promises = new Set();
|
|
225
|
-
let timeout;
|
|
345
|
+
// Get local data
|
|
346
|
+
const localData = await this._getAllLocal(holon, lens, schema);
|
|
226
347
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
348
|
+
// If authenticated, get federated data
|
|
349
|
+
let federatedData = [];
|
|
350
|
+
if (this.currentSpace) {
|
|
351
|
+
federatedData = await this._getAllFederated(holon, lens, schema);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Combine and deduplicate data based on ID
|
|
355
|
+
const combined = new Map();
|
|
356
|
+
|
|
357
|
+
// Add local data first
|
|
358
|
+
localData.forEach(item => {
|
|
359
|
+
if (item.id) {
|
|
360
|
+
combined.set(item.id, item);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Add federated data, potentially overwriting local data if newer
|
|
365
|
+
federatedData.forEach(item => {
|
|
366
|
+
if (item.id) {
|
|
367
|
+
const existing = combined.get(item.id);
|
|
368
|
+
if (!existing ||
|
|
369
|
+
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
370
|
+
combined.set(item.id, item);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return Array.from(combined.values());
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Gets data from federated spaces
|
|
380
|
+
* @private
|
|
381
|
+
* @param {string} holon - The holon identifier
|
|
382
|
+
* @param {string} lens - The lens identifier
|
|
383
|
+
* @param {string} key - The key to get
|
|
384
|
+
* @returns {Promise<object|null>} - The federated data or null if not found
|
|
385
|
+
*/
|
|
386
|
+
async _getFederatedData(holon, lens, key) {
|
|
387
|
+
try {
|
|
388
|
+
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
389
|
+
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Try each federated space
|
|
394
|
+
for (const spaceId of fedInfo.federation) {
|
|
395
|
+
const result = await new Promise((resolve) => {
|
|
396
|
+
this.gun.get(this.appname)
|
|
397
|
+
.get(spaceId)
|
|
398
|
+
.get(lens)
|
|
399
|
+
.get(key)
|
|
400
|
+
.once(async (data) => {
|
|
401
|
+
if (!data) {
|
|
402
|
+
resolve(null);
|
|
403
|
+
return;
|
|
237
404
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
405
|
+
try {
|
|
406
|
+
const parsed = await this.parse(data);
|
|
407
|
+
resolve(parsed);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.warn(`Error parsing federated data from ${spaceId}:`, error);
|
|
410
|
+
resolve(null);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (result) {
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.warn('Federation get error:', error);
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Gets all data from local space
|
|
427
|
+
* @private
|
|
428
|
+
* @param {string} holon - The holon identifier
|
|
429
|
+
* @param {string} lens - The lens identifier
|
|
430
|
+
* @param {object} schema - The schema to validate against
|
|
431
|
+
* @returns {Promise<Array>} - Array of local data
|
|
432
|
+
*/
|
|
433
|
+
async _getAllLocal(holon, lens, schema) {
|
|
434
|
+
return new Promise((resolve) => {
|
|
435
|
+
const output = new Map();
|
|
436
|
+
let isResolved = false;
|
|
437
|
+
let listener = null;
|
|
438
|
+
|
|
439
|
+
const hardTimeout = setTimeout(() => {
|
|
440
|
+
cleanup();
|
|
441
|
+
resolve(Array.from(output.values()));
|
|
442
|
+
}, 5000);
|
|
443
|
+
|
|
444
|
+
const cleanup = () => {
|
|
445
|
+
if (listener) {
|
|
446
|
+
listener.off();
|
|
447
|
+
}
|
|
448
|
+
clearTimeout(hardTimeout);
|
|
449
|
+
isResolved = true;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const processData = async (data, key) => {
|
|
453
|
+
if (!data || key === '_') return;
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const parsed = await this.parse(data);
|
|
457
|
+
if (!parsed || !parsed.id) return;
|
|
458
|
+
|
|
459
|
+
if (schema) {
|
|
460
|
+
const valid = this.validator.validate(schema, parsed);
|
|
461
|
+
if (valid || !this.strict) {
|
|
462
|
+
output.set(parsed.id, parsed);
|
|
245
463
|
}
|
|
464
|
+
} else {
|
|
465
|
+
output.set(parsed.id, parsed);
|
|
246
466
|
}
|
|
467
|
+
} catch (error) {
|
|
468
|
+
console.error('Error processing data:', error);
|
|
247
469
|
}
|
|
248
470
|
};
|
|
249
471
|
|
|
250
|
-
|
|
472
|
+
this.gun.get(this.appname)
|
|
251
473
|
.get(holon)
|
|
252
474
|
.get(lens)
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
475
|
+
.once(async (data) => {
|
|
476
|
+
if (!data) {
|
|
477
|
+
cleanup();
|
|
478
|
+
resolve([]);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
258
481
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
482
|
+
const initialPromises = [];
|
|
483
|
+
Object.keys(data)
|
|
484
|
+
.filter(key => key !== '_')
|
|
485
|
+
.forEach(key => {
|
|
486
|
+
initialPromises.push(processData(data[key], key));
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
await Promise.all(initialPromises);
|
|
491
|
+
cleanup();
|
|
492
|
+
resolve(Array.from(output.values()));
|
|
493
|
+
} catch (error) {
|
|
494
|
+
cleanup();
|
|
495
|
+
resolve([]);
|
|
496
|
+
}
|
|
265
497
|
});
|
|
266
498
|
});
|
|
267
499
|
}
|
|
268
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Gets all data from federated spaces
|
|
503
|
+
* @private
|
|
504
|
+
* @param {string} holon - The holon identifier
|
|
505
|
+
* @param {string} lens - The lens identifier
|
|
506
|
+
* @param {object} schema - The schema to validate against
|
|
507
|
+
* @returns {Promise<Array>} - Array of federated data
|
|
508
|
+
*/
|
|
509
|
+
async _getAllFederated(holon, lens, schema) {
|
|
510
|
+
try {
|
|
511
|
+
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
512
|
+
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const federatedData = new Map();
|
|
517
|
+
|
|
518
|
+
// Get data from each federated space
|
|
519
|
+
const fedPromises = fedInfo.federation.map(spaceId =>
|
|
520
|
+
new Promise((resolve) => {
|
|
521
|
+
this.gun.get(this.appname)
|
|
522
|
+
.get(spaceId)
|
|
523
|
+
.get(lens)
|
|
524
|
+
.once(async (data) => {
|
|
525
|
+
if (!data) {
|
|
526
|
+
resolve();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const processPromises = Object.keys(data)
|
|
531
|
+
.filter(key => key !== '_')
|
|
532
|
+
.map(async key => {
|
|
533
|
+
try {
|
|
534
|
+
const parsed = await this.parse(data[key]);
|
|
535
|
+
if (parsed && parsed.id) {
|
|
536
|
+
if (schema) {
|
|
537
|
+
const valid = this.validator.validate(schema, parsed);
|
|
538
|
+
if (valid || !this.strict) {
|
|
539
|
+
federatedData.set(parsed.id, parsed);
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
federatedData.set(parsed.id, parsed);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.warn(`Error processing federated data from ${spaceId}:`, error);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await Promise.all(processPromises);
|
|
551
|
+
resolve();
|
|
552
|
+
});
|
|
553
|
+
})
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
await Promise.all(fedPromises);
|
|
557
|
+
return Array.from(federatedData.values());
|
|
558
|
+
} catch (error) {
|
|
559
|
+
console.warn('Federation getAll error:', error);
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
269
564
|
/**
|
|
270
565
|
* Parses data from GunDB, handling various data formats and references.
|
|
271
566
|
* @param {*} data - The data to parse, could be a string, object, or GunDB reference.
|
|
@@ -311,7 +606,9 @@ class HoloSphere {
|
|
|
311
606
|
|
|
312
607
|
return parsedData;
|
|
313
608
|
} catch (error) {
|
|
314
|
-
|
|
609
|
+
console.log("Parsing not a JSON, returning raw data", rawData);
|
|
610
|
+
return rawData;
|
|
611
|
+
//throw new Error(`Parse error: ${error.message}`);
|
|
315
612
|
}
|
|
316
613
|
}
|
|
317
614
|
|
|
@@ -331,26 +628,27 @@ class HoloSphere {
|
|
|
331
628
|
// Get schema for validation
|
|
332
629
|
const schema = await this.getSchema(lens);
|
|
333
630
|
|
|
334
|
-
|
|
631
|
+
// First try to get from current space
|
|
632
|
+
const localResult = await new Promise((resolve) => {
|
|
335
633
|
let timeout = setTimeout(() => {
|
|
336
634
|
console.warn('get: Operation timed out');
|
|
337
635
|
resolve(null);
|
|
338
|
-
}, 5000);
|
|
636
|
+
}, 5000);
|
|
339
637
|
|
|
340
638
|
this.gun.get(this.appname)
|
|
341
639
|
.get(holon)
|
|
342
640
|
.get(lens)
|
|
343
641
|
.get(key)
|
|
344
|
-
.once((data
|
|
642
|
+
.once(async (data) => {
|
|
345
643
|
clearTimeout(timeout);
|
|
346
|
-
|
|
644
|
+
|
|
347
645
|
if (!data) {
|
|
348
646
|
resolve(null);
|
|
349
647
|
return;
|
|
350
648
|
}
|
|
351
649
|
|
|
352
650
|
try {
|
|
353
|
-
const parsed = this.parse(data);
|
|
651
|
+
const parsed = await this.parse(data);
|
|
354
652
|
|
|
355
653
|
// Validate against schema if one exists
|
|
356
654
|
if (schema) {
|
|
@@ -363,6 +661,20 @@ class HoloSphere {
|
|
|
363
661
|
}
|
|
364
662
|
}
|
|
365
663
|
}
|
|
664
|
+
|
|
665
|
+
// Check if user has access - only allow if:
|
|
666
|
+
// 1. No owner (public data)
|
|
667
|
+
// 2. User is the owner
|
|
668
|
+
// 3. User is in shared list
|
|
669
|
+
// 4. Data is from federation
|
|
670
|
+
if (parsed.owner &&
|
|
671
|
+
this.currentSpace?.alias !== parsed.owner &&
|
|
672
|
+
(!parsed.shared || !parsed.shared.includes(this.currentSpace?.alias)) &&
|
|
673
|
+
(!parsed.federation || !parsed.federation.origin)) {
|
|
674
|
+
resolve(null);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
366
678
|
resolve(parsed);
|
|
367
679
|
} catch (error) {
|
|
368
680
|
console.error('Error parsing data:', error);
|
|
@@ -370,6 +682,21 @@ class HoloSphere {
|
|
|
370
682
|
}
|
|
371
683
|
});
|
|
372
684
|
});
|
|
685
|
+
|
|
686
|
+
// If found locally, return it
|
|
687
|
+
if (localResult) {
|
|
688
|
+
return localResult;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// If not found locally and we're authenticated, try federated spaces
|
|
692
|
+
if (this.currentSpace) {
|
|
693
|
+
const fedResult = await this._getFederatedData(holon, lens, key);
|
|
694
|
+
if (fedResult) {
|
|
695
|
+
return fedResult;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return null;
|
|
373
700
|
}
|
|
374
701
|
|
|
375
702
|
/**
|
|
@@ -383,6 +710,21 @@ class HoloSphere {
|
|
|
383
710
|
throw new Error('delete: Missing required parameters');
|
|
384
711
|
}
|
|
385
712
|
|
|
713
|
+
if (!this.currentSpace) {
|
|
714
|
+
throw new Error('Unauthorized to delete this data');
|
|
715
|
+
}
|
|
716
|
+
this._checkSession();
|
|
717
|
+
|
|
718
|
+
// Check ownership before delete
|
|
719
|
+
const data = await this.get(holon, lens, key);
|
|
720
|
+
if (!data) {
|
|
721
|
+
return true; // Nothing to delete
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (data.owner && data.owner !== this.currentSpace.alias) {
|
|
725
|
+
throw new Error('Unauthorized to delete this data');
|
|
726
|
+
}
|
|
727
|
+
|
|
386
728
|
return new Promise((resolve, reject) => {
|
|
387
729
|
try {
|
|
388
730
|
this.gun.get(this.appname)
|
|
@@ -447,7 +789,7 @@ class HoloSphere {
|
|
|
447
789
|
.catch(error => {
|
|
448
790
|
console.error('Error in deleteAll:', error);
|
|
449
791
|
resolve(false);
|
|
450
|
-
|
|
792
|
+
});
|
|
451
793
|
});
|
|
452
794
|
});
|
|
453
795
|
}
|
|
@@ -459,10 +801,10 @@ class HoloSphere {
|
|
|
459
801
|
* Stores a specific gun node in a given holon and lens.
|
|
460
802
|
* @param {string} holon - The holon identifier.
|
|
461
803
|
* @param {string} lens - The lens under which to store the node.
|
|
462
|
-
* @param {object}
|
|
804
|
+
* @param {object} data - The node to store.
|
|
463
805
|
*/
|
|
464
|
-
async putNode(holon, lens,
|
|
465
|
-
if (!holon || !lens || !
|
|
806
|
+
async putNode(holon, lens, data) {
|
|
807
|
+
if (!holon || !lens || !data) {
|
|
466
808
|
throw new Error('putNode: Missing required parameters');
|
|
467
809
|
}
|
|
468
810
|
|
|
@@ -471,7 +813,8 @@ class HoloSphere {
|
|
|
471
813
|
this.gun.get(this.appname)
|
|
472
814
|
.get(holon)
|
|
473
815
|
.get(lens)
|
|
474
|
-
.
|
|
816
|
+
.get('value') // Store at 'value' key
|
|
817
|
+
.put(data.value, ack => { // Store the value directly
|
|
475
818
|
if (ack.err) {
|
|
476
819
|
reject(new Error(ack.err));
|
|
477
820
|
} else {
|
|
@@ -489,19 +832,26 @@ class HoloSphere {
|
|
|
489
832
|
* @param {string} holon - The holon identifier.
|
|
490
833
|
* @param {string} lens - The lens identifier.
|
|
491
834
|
* @param {string} key - The specific key to retrieve.
|
|
492
|
-
|
|
835
|
+
* @returns {Promise<any>} - The retrieved node or null if not found.
|
|
493
836
|
*/
|
|
494
|
-
getNode(holon, lens, key) {
|
|
837
|
+
async getNode(holon, lens, key) {
|
|
495
838
|
if (!holon || !lens || !key) {
|
|
496
|
-
|
|
497
|
-
return null;
|
|
839
|
+
throw new Error('getNode: Missing required parameters');
|
|
498
840
|
}
|
|
499
841
|
|
|
500
|
-
return
|
|
842
|
+
return new Promise((resolve) => {
|
|
843
|
+
this.gun.get(this.appname)
|
|
501
844
|
.get(holon)
|
|
502
845
|
.get(lens)
|
|
503
846
|
.get(key)
|
|
504
|
-
|
|
847
|
+
.once((data) => {
|
|
848
|
+
if (!data) {
|
|
849
|
+
resolve(null);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
resolve(data); // Return the data directly
|
|
853
|
+
});
|
|
854
|
+
});
|
|
505
855
|
}
|
|
506
856
|
|
|
507
857
|
getNodeRef(soul) {
|
|
@@ -620,83 +970,54 @@ class HoloSphere {
|
|
|
620
970
|
* @param {string} tableName - The table name to retrieve data from.
|
|
621
971
|
* @returns {Promise<object|null>} - The parsed data from the table or null if not found.
|
|
622
972
|
*/
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
// let counter = 0
|
|
627
|
-
// this.gun.get(this.appname).get(tableName.toString()).once((data, key) => {
|
|
628
|
-
|
|
629
|
-
// counter += 1
|
|
630
|
-
// if (itemdata) {
|
|
631
|
-
// let parsed = await this.parse(itemdata)
|
|
632
|
-
// output.push(parsed);
|
|
633
|
-
// console.log('getAllGlobal: parsed: ', parsed)
|
|
634
|
-
// }
|
|
635
|
-
|
|
636
|
-
// if (counter == maplenght) {
|
|
637
|
-
// resolve(output);
|
|
638
|
-
|
|
639
|
-
// }
|
|
640
|
-
// }
|
|
641
|
-
// );
|
|
642
|
-
// }
|
|
643
|
-
// )
|
|
644
|
-
// }
|
|
645
|
-
async getAllGlobal(lens) {
|
|
646
|
-
if ( !lens) {
|
|
647
|
-
console.error('getAll: Missing required parameters:', { lens });
|
|
648
|
-
return [];
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const schema = await this.getSchema(lens);
|
|
652
|
-
if (!schema && this.strict) {
|
|
653
|
-
console.error('getAll: Schema required in strict mode for lens:', lens);
|
|
654
|
-
return [];
|
|
973
|
+
async getAllGlobal(tableName) {
|
|
974
|
+
if (!tableName) {
|
|
975
|
+
throw new Error('getAllGlobal: Missing table name parameter');
|
|
655
976
|
}
|
|
656
977
|
|
|
657
978
|
return new Promise((resolve) => {
|
|
658
979
|
let output = [];
|
|
659
|
-
let
|
|
980
|
+
let isResolved = false;
|
|
981
|
+
let timeout = setTimeout(() => {
|
|
982
|
+
if (!isResolved) {
|
|
983
|
+
isResolved = true;
|
|
984
|
+
resolve(output);
|
|
985
|
+
}
|
|
986
|
+
}, 5000);
|
|
660
987
|
|
|
661
|
-
this.gun.get(this.appname).get(
|
|
988
|
+
this.gun.get(this.appname).get(tableName).once(async (data) => {
|
|
662
989
|
if (!data) {
|
|
663
|
-
|
|
990
|
+
clearTimeout(timeout);
|
|
991
|
+
isResolved = true;
|
|
992
|
+
resolve([]);
|
|
664
993
|
return;
|
|
665
994
|
}
|
|
666
995
|
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
await this.delete(holon, lens, key);
|
|
681
|
-
} else {
|
|
682
|
-
console.warn('Invalid data found:', key, this.validator.errors);
|
|
683
|
-
output.push(parsed);
|
|
684
|
-
}
|
|
685
|
-
} else {
|
|
686
|
-
output.push(parsed);
|
|
687
|
-
}
|
|
688
|
-
} catch (error) {
|
|
689
|
-
console.error('Error parsing data:', error);
|
|
690
|
-
if (this.strict) {
|
|
691
|
-
await this.delete(holon, lens, key);
|
|
996
|
+
const keys = Object.keys(data).filter(key => key !== '_');
|
|
997
|
+
const promises = keys.map(key =>
|
|
998
|
+
new Promise(async (resolveItem) => {
|
|
999
|
+
const itemData = await new Promise(resolveData => {
|
|
1000
|
+
this.gun.get(this.appname).get(tableName).get(key).once(resolveData);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
if (itemData) {
|
|
1004
|
+
try {
|
|
1005
|
+
const parsed = await this.parse(itemData);
|
|
1006
|
+
if (parsed) output.push(parsed);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
console.error('Error parsing data:', error);
|
|
692
1009
|
}
|
|
693
1010
|
}
|
|
694
|
-
|
|
1011
|
+
resolveItem();
|
|
1012
|
+
})
|
|
1013
|
+
);
|
|
695
1014
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1015
|
+
await Promise.all(promises);
|
|
1016
|
+
clearTimeout(timeout);
|
|
1017
|
+
if (!isResolved) {
|
|
1018
|
+
isResolved = true;
|
|
1019
|
+
resolve(output);
|
|
1020
|
+
}
|
|
700
1021
|
});
|
|
701
1022
|
});
|
|
702
1023
|
}
|
|
@@ -707,7 +1028,36 @@ class HoloSphere {
|
|
|
707
1028
|
* @returns {Promise<void>}
|
|
708
1029
|
*/
|
|
709
1030
|
async deleteGlobal(tableName, key) {
|
|
710
|
-
|
|
1031
|
+
if (!tableName || !key) {
|
|
1032
|
+
throw new Error('deleteGlobal: Missing required parameters');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Only check authentication for non-spaces tables
|
|
1036
|
+
if (tableName !== 'spaces' && !this.currentSpace) {
|
|
1037
|
+
throw new Error('Unauthorized to delete this data');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Skip session check for spaces table
|
|
1041
|
+
if (tableName !== 'spaces') {
|
|
1042
|
+
this._checkSession();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return new Promise((resolve, reject) => {
|
|
1046
|
+
try {
|
|
1047
|
+
this.gun.get(this.appname)
|
|
1048
|
+
.get(tableName)
|
|
1049
|
+
.get(key)
|
|
1050
|
+
.put(null, ack => {
|
|
1051
|
+
if (ack.err) {
|
|
1052
|
+
reject(new Error(ack.err));
|
|
1053
|
+
} else {
|
|
1054
|
+
resolve(true);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
reject(error);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
711
1061
|
}
|
|
712
1062
|
|
|
713
1063
|
/**
|
|
@@ -716,132 +1066,237 @@ class HoloSphere {
|
|
|
716
1066
|
* @returns {Promise<void>}
|
|
717
1067
|
*/
|
|
718
1068
|
async deleteAllGlobal(tableName) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
//
|
|
1069
|
+
if (!tableName) {
|
|
1070
|
+
throw new Error('deleteAllGlobal: Missing table name parameter');
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Only check authentication for non-spaces and non-federation tables
|
|
1074
|
+
if (!['spaces', 'federation'].includes(tableName) && !this.currentSpace) {
|
|
1075
|
+
throw new Error('Unauthorized to delete this data');
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Skip session check for spaces and federation tables
|
|
1079
|
+
if (!['spaces', 'federation'].includes(tableName)) {
|
|
1080
|
+
this._checkSession();
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return new Promise((resolve, reject) => {
|
|
1084
|
+
try {
|
|
1085
|
+
const deletions = new Set();
|
|
1086
|
+
let timeout = setTimeout(() => {
|
|
1087
|
+
if (deletions.size === 0) {
|
|
1088
|
+
resolve(true); // No data to delete
|
|
1089
|
+
}
|
|
1090
|
+
}, 5000);
|
|
1091
|
+
|
|
1092
|
+
this.gun.get(this.appname).get(tableName).once(async (data) => {
|
|
1093
|
+
if (!data) {
|
|
1094
|
+
clearTimeout(timeout);
|
|
1095
|
+
resolve(true);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const keys = Object.keys(data).filter(key => key !== '_');
|
|
1100
|
+
const promises = keys.map(key =>
|
|
1101
|
+
new Promise((resolveDelete) => {
|
|
1102
|
+
this.gun.get(this.appname)
|
|
1103
|
+
.get(tableName)
|
|
1104
|
+
.get(key)
|
|
1105
|
+
.put(null, ack => {
|
|
1106
|
+
if (ack.err) {
|
|
1107
|
+
console.error(`Failed to delete ${key}:`, ack.err);
|
|
1108
|
+
}
|
|
1109
|
+
resolveDelete();
|
|
1110
|
+
});
|
|
1111
|
+
})
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
try {
|
|
1115
|
+
await Promise.all(promises);
|
|
1116
|
+
// Finally delete the table itself
|
|
1117
|
+
this.gun.get(this.appname).get(tableName).put(null);
|
|
1118
|
+
clearTimeout(timeout);
|
|
1119
|
+
resolve(true);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
reject(error);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
reject(error);
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
729
1128
|
}
|
|
730
1129
|
|
|
731
1130
|
// ================================ COMPUTE FUNCTIONS ================================
|
|
732
1131
|
/**
|
|
733
|
-
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Computes operations across multiple layers up the hierarchy
|
|
1135
|
+
* @param {string} holon - Starting holon identifier
|
|
1136
|
+
* @param {string} lens - The lens to compute
|
|
1137
|
+
* @param {object} options - Computation options
|
|
1138
|
+
* @param {number} [maxLevels=15] - Maximum levels to compute up
|
|
1139
|
+
*/
|
|
1140
|
+
async computeHierarchy(holon, lens, options, maxLevels = 15) {
|
|
1141
|
+
let currentHolon = holon;
|
|
1142
|
+
let currentRes = h3.getResolution(currentHolon);
|
|
1143
|
+
const results = [];
|
|
1144
|
+
|
|
1145
|
+
while (currentRes > 0 && maxLevels > 0) {
|
|
1146
|
+
try {
|
|
1147
|
+
const result = await this.compute(currentHolon, lens, options);
|
|
1148
|
+
if (result) {
|
|
1149
|
+
results.push(result);
|
|
1150
|
+
}
|
|
1151
|
+
currentHolon = h3.cellToParent(currentHolon, currentRes - 1);
|
|
1152
|
+
currentRes--;
|
|
1153
|
+
maxLevels--;
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
console.error('Error in compute hierarchy:', error);
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return results;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/* Computes operations on content within a holon and lens for one layer up.
|
|
734
1164
|
* @param {string} holon - The holon identifier.
|
|
735
1165
|
* @param {string} lens - The lens to compute.
|
|
736
|
-
* @param {
|
|
1166
|
+
* @param {object} options - Computation options
|
|
1167
|
+
* @param {string} options.operation - The operation to perform ('summarize', 'aggregate', 'concatenate')
|
|
1168
|
+
* @param {string[]} [options.fields] - Fields to perform operation on
|
|
1169
|
+
* @param {string} [options.targetField] - Field to store the result in
|
|
1170
|
+
* @throws {Error} If parameters are invalid or missing
|
|
737
1171
|
*/
|
|
738
|
-
async compute(holon, lens,
|
|
1172
|
+
async compute(holon, lens, options) {
|
|
1173
|
+
// Validate required parameters
|
|
739
1174
|
if (!holon || !lens) {
|
|
740
1175
|
throw new Error('compute: Missing required parameters');
|
|
741
1176
|
}
|
|
742
1177
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (res < 1 || res > 15) {
|
|
747
|
-
throw new Error('compute: Invalid holon resolution');
|
|
1178
|
+
// Convert string operation to options object
|
|
1179
|
+
if (typeof options === 'string') {
|
|
1180
|
+
options = { operation: options };
|
|
748
1181
|
}
|
|
749
1182
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const content = [];
|
|
754
|
-
const promises = siblings.map(sibling =>
|
|
755
|
-
new Promise((resolve) => {
|
|
756
|
-
const timeout = setTimeout(() => {
|
|
757
|
-
console.warn(`Timeout for sibling ${sibling}`);
|
|
758
|
-
resolve();
|
|
759
|
-
}, 1000);
|
|
1183
|
+
if (!options?.operation) {
|
|
1184
|
+
throw new Error('compute: Missing required parameters');
|
|
1185
|
+
}
|
|
760
1186
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
content.push(data.content);
|
|
769
|
-
}
|
|
770
|
-
resolve();
|
|
771
|
-
});
|
|
772
|
-
})
|
|
773
|
-
);
|
|
1187
|
+
// Validate holon format and resolution first
|
|
1188
|
+
let res;
|
|
1189
|
+
try {
|
|
1190
|
+
res = h3.getResolution(holon);
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
throw new Error('compute: Invalid holon format');
|
|
1193
|
+
}
|
|
774
1194
|
|
|
775
|
-
|
|
1195
|
+
if (res < 1 || res > 15) {
|
|
1196
|
+
throw new Error('compute: Invalid holon resolution (must be between 1 and 15)');
|
|
1197
|
+
}
|
|
776
1198
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1199
|
+
const {
|
|
1200
|
+
operation,
|
|
1201
|
+
fields = [],
|
|
1202
|
+
targetField,
|
|
1203
|
+
depth,
|
|
1204
|
+
maxDepth
|
|
1205
|
+
} = options;
|
|
1206
|
+
|
|
1207
|
+
// Validate depth parameters if provided
|
|
1208
|
+
if (depth !== undefined && depth < 0) {
|
|
1209
|
+
throw new Error('compute: Invalid depth parameter');
|
|
1210
|
+
}
|
|
785
1211
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
1212
|
+
if (maxDepth !== undefined && (maxDepth < 1 || maxDepth > 15)) {
|
|
1213
|
+
throw new Error('compute: Invalid maxDepth parameter (must be between 1 and 15)');
|
|
789
1214
|
}
|
|
790
|
-
}
|
|
791
1215
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
*/
|
|
797
|
-
async clearlens(holon, lens) {
|
|
798
|
-
if (!holon || !lens) {
|
|
799
|
-
throw new Error('clearlens: Missing required parameters');
|
|
1216
|
+
// Validate operation
|
|
1217
|
+
const validOperations = ['summarize', 'aggregate', 'concatenate'];
|
|
1218
|
+
if (!validOperations.includes(operation)) {
|
|
1219
|
+
throw new Error(`compute: Invalid operation (must be one of ${validOperations.join(', ')})`);
|
|
800
1220
|
}
|
|
801
1221
|
|
|
802
|
-
|
|
1222
|
+
const parent = h3.cellToParent(holon, res - 1);
|
|
1223
|
+
const siblings = h3.cellToChildren(parent, res);
|
|
1224
|
+
|
|
1225
|
+
// Collect all content from siblings
|
|
1226
|
+
const contents = await Promise.all(
|
|
1227
|
+
siblings.map(sibling => this.getAll(sibling, lens))
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
const flatContents = contents.flat().filter(Boolean);
|
|
1231
|
+
|
|
1232
|
+
if (flatContents.length > 0) {
|
|
803
1233
|
try {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1234
|
+
let computed;
|
|
1235
|
+
switch (operation) {
|
|
1236
|
+
case 'summarize':
|
|
1237
|
+
// For summarize, concatenate specified fields or use entire content
|
|
1238
|
+
const textToSummarize = fields.length > 0
|
|
1239
|
+
? flatContents.map(item => fields.map(field => item[field]).filter(Boolean).join('\n')).join('\n')
|
|
1240
|
+
: JSON.stringify(flatContents);
|
|
1241
|
+
computed = await this.summarize(textToSummarize);
|
|
1242
|
+
break;
|
|
1243
|
+
|
|
1244
|
+
case 'aggregate':
|
|
1245
|
+
// For aggregate, sum numeric fields
|
|
1246
|
+
computed = fields.reduce((acc, field) => {
|
|
1247
|
+
acc[field] = flatContents.reduce((sum, item) => {
|
|
1248
|
+
return sum + (Number(item[field]) || 0);
|
|
1249
|
+
}, 0);
|
|
1250
|
+
return acc;
|
|
1251
|
+
}, {});
|
|
1252
|
+
break;
|
|
1253
|
+
|
|
1254
|
+
case 'concatenate':
|
|
1255
|
+
// For concatenate, combine arrays or strings
|
|
1256
|
+
computed = fields.reduce((acc, field) => {
|
|
1257
|
+
acc[field] = flatContents.reduce((combined, item) => {
|
|
1258
|
+
const value = item[field];
|
|
1259
|
+
if (Array.isArray(value)) {
|
|
1260
|
+
return [...combined, ...value];
|
|
1261
|
+
} else if (value) {
|
|
1262
|
+
return [...combined, value];
|
|
1263
|
+
}
|
|
1264
|
+
return combined;
|
|
1265
|
+
}, []);
|
|
1266
|
+
// Remove duplicates if array
|
|
1267
|
+
acc[field] = Array.from(new Set(acc[field]));
|
|
1268
|
+
return acc;
|
|
1269
|
+
}, {});
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (computed) {
|
|
1274
|
+
const resultId = `${parent}_${operation}`;
|
|
1275
|
+
const result = {
|
|
1276
|
+
id: resultId,
|
|
1277
|
+
timestamp: Date.now()
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
// Store result in targetField if specified, otherwise at root level
|
|
1281
|
+
if (targetField) {
|
|
1282
|
+
result[targetField] = computed;
|
|
1283
|
+
} else if (typeof computed === 'object') {
|
|
1284
|
+
Object.assign(result, computed);
|
|
1285
|
+
} else {
|
|
1286
|
+
result.value = computed;
|
|
808
1287
|
}
|
|
809
|
-
}, 1000);
|
|
810
1288
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
.map()
|
|
815
|
-
.once((data, key) => {
|
|
816
|
-
if (data) {
|
|
817
|
-
const deletion = new Promise((resolveDelete) => {
|
|
818
|
-
this.gun.get(this.appname)
|
|
819
|
-
.get(holon)
|
|
820
|
-
.get(lens)
|
|
821
|
-
.get(key)
|
|
822
|
-
.put(null, ack => {
|
|
823
|
-
if (ack.err) {
|
|
824
|
-
console.error(`Failed to delete ${key}:`, ack.err);
|
|
825
|
-
}
|
|
826
|
-
resolveDelete();
|
|
827
|
-
});
|
|
828
|
-
});
|
|
829
|
-
deletions.add(deletion);
|
|
830
|
-
deletion.finally(() => {
|
|
831
|
-
deletions.delete(deletion);
|
|
832
|
-
if (deletions.size === 0) {
|
|
833
|
-
clearTimeout(timeout);
|
|
834
|
-
resolve();
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
});
|
|
1289
|
+
await this.put(parent, lens, result);
|
|
1290
|
+
return result;
|
|
1291
|
+
}
|
|
839
1292
|
} catch (error) {
|
|
840
|
-
|
|
1293
|
+
console.warn('Error in compute operation:', error);
|
|
1294
|
+
throw error;
|
|
841
1295
|
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
1296
|
+
}
|
|
844
1297
|
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
845
1300
|
|
|
846
1301
|
/**
|
|
847
1302
|
* Summarizes provided history text using OpenAI.
|
|
@@ -852,32 +1307,29 @@ class HoloSphere {
|
|
|
852
1307
|
if (!this.openai) {
|
|
853
1308
|
return 'OpenAI not initialized, please specify the API key in the constructor.'
|
|
854
1309
|
}
|
|
855
|
-
//const run = await this.openai.beta.threads.runs.retrieve(thread.id,run.id)
|
|
856
|
-
const assistant = await this.openai.beta.assistants.retrieve("asst_qhk79F8wV9BDNuwfOI80TqzC")
|
|
857
|
-
const thread = await this.openai.beta.threads.create()
|
|
858
|
-
const message = await this.openai.beta.threads.messages.create(thread.id, {
|
|
859
|
-
role: "user",
|
|
860
|
-
content: history
|
|
861
|
-
})
|
|
862
|
-
const run = await this.openai.beta.threads.runs.create(thread.id, {
|
|
863
|
-
assistant_id: assistant.id //,
|
|
864
|
-
//instructions: "What is the meaning of life?",
|
|
865
|
-
});
|
|
866
1310
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1311
|
+
try {
|
|
1312
|
+
const response = await this.openai.chat.completions.create({
|
|
1313
|
+
model: "gpt-4",
|
|
1314
|
+
messages: [
|
|
1315
|
+
{
|
|
1316
|
+
role: "system",
|
|
1317
|
+
content: "You are a helpful assistant that summarizes text concisely while preserving key information. Keep summaries clear and focused."
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
role: "user",
|
|
1321
|
+
content: history
|
|
1322
|
+
}
|
|
1323
|
+
],
|
|
1324
|
+
temperature: 0.7,
|
|
1325
|
+
max_tokens: 500
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
return response.choices[0].message.content.trim();
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
console.error('Error in summarize:', error);
|
|
1331
|
+
throw new Error('Failed to generate summary');
|
|
876
1332
|
}
|
|
877
|
-
// Get the latest messages from the thread
|
|
878
|
-
const messages = await this.openai.beta.threads.messages.list(thread.id)
|
|
879
|
-
const summary = messages.data[0].content[0].text.value.replace(/\`\`\`json\n/, '').replace(/\`\`\`/, '').trim()
|
|
880
|
-
return summary
|
|
881
1333
|
}
|
|
882
1334
|
|
|
883
1335
|
/**
|
|
@@ -903,7 +1355,7 @@ class HoloSphere {
|
|
|
903
1355
|
|
|
904
1356
|
|
|
905
1357
|
/**
|
|
906
|
-
* Updates the parent
|
|
1358
|
+
* Updates the parent holon with a new report.
|
|
907
1359
|
* @param {string} id - The child holon identifier.
|
|
908
1360
|
* @param {string} report - The report to update.
|
|
909
1361
|
* @returns {Promise<object>} - The updated parent information.
|
|
@@ -971,31 +1423,441 @@ class HoloSphere {
|
|
|
971
1423
|
* @param {function} callback - The callback to execute on changes.
|
|
972
1424
|
*/
|
|
973
1425
|
async subscribe(holon, lens, callback) {
|
|
974
|
-
const subscriptionId =
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1426
|
+
const subscriptionId = this.generateSubscriptionId();
|
|
1427
|
+
this.subscriptions[subscriptionId] = {
|
|
1428
|
+
query: { holon, lens },
|
|
1429
|
+
callback,
|
|
1430
|
+
active: true
|
|
1431
|
+
};
|
|
980
1432
|
|
|
1433
|
+
// Add cleanup to ensure callback isn't called after unsubscribe
|
|
981
1434
|
return {
|
|
982
1435
|
unsubscribe: () => {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1436
|
+
if (this.subscriptions[subscriptionId]) {
|
|
1437
|
+
delete this.subscriptions[subscriptionId];
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
987
1440
|
};
|
|
988
1441
|
}
|
|
989
1442
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1443
|
+
notifySubscribers(data) {
|
|
1444
|
+
Object.values(this.subscriptions).forEach(subscription => {
|
|
1445
|
+
if (subscription.active && this.matchesQuery(data, subscription.query)) {
|
|
1446
|
+
subscription.callback(data);
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
994
1449
|
}
|
|
995
1450
|
|
|
996
1451
|
// Add ID generation method
|
|
997
1452
|
generateId() {
|
|
998
|
-
return Date.now().toString(
|
|
1453
|
+
return Date.now().toString(10) + Math.random().toString(2);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
generateSubscriptionId() {
|
|
1457
|
+
return Date.now().toString(10) + Math.random().toString(2);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
matchesQuery(data, query) {
|
|
1461
|
+
return data && query &&
|
|
1462
|
+
data.holon === query.holon &&
|
|
1463
|
+
data.lens === query.lens;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* Creates a new space with the given credentials
|
|
1468
|
+
* @param {string} spacename - The space identifier/username
|
|
1469
|
+
* @param {string} password - The space password
|
|
1470
|
+
* @returns {Promise<boolean>} - True if space was created successfully
|
|
1471
|
+
*/
|
|
1472
|
+
async createSpace(spacename, password) {
|
|
1473
|
+
if (!spacename || !password) {
|
|
1474
|
+
throw new Error('Invalid credentials format');
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Check if space already exists
|
|
1478
|
+
const existingSpace = await this.getGlobal('spaces', spacename);
|
|
1479
|
+
if (existingSpace) {
|
|
1480
|
+
throw new Error('Space already exists');
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
try {
|
|
1484
|
+
// Generate key pair
|
|
1485
|
+
const pair = await Gun.SEA.pair();
|
|
1486
|
+
|
|
1487
|
+
// Create auth record with SEA
|
|
1488
|
+
const salt = await Gun.SEA.random(64).toString('base64');
|
|
1489
|
+
const hash = await Gun.SEA.work(password, salt);
|
|
1490
|
+
const auth = {
|
|
1491
|
+
salt: salt,
|
|
1492
|
+
hash: hash,
|
|
1493
|
+
pub: pair.pub
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
// Create space record with encrypted data
|
|
1497
|
+
const space = {
|
|
1498
|
+
alias: spacename,
|
|
1499
|
+
auth: auth,
|
|
1500
|
+
epub: pair.epub,
|
|
1501
|
+
pub: pair.pub,
|
|
1502
|
+
created: Date.now()
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
await this.putGlobal('spaces', {
|
|
1506
|
+
...space,
|
|
1507
|
+
id: spacename
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
return true;
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
throw new Error(`Space creation failed: ${error.message}`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Logs in to a space with the given credentials
|
|
1518
|
+
* @param {string} spacename - The space identifier/username
|
|
1519
|
+
* @param {string} password - The space password
|
|
1520
|
+
* @returns {Promise<boolean>} - True if login was successful
|
|
1521
|
+
*/
|
|
1522
|
+
async login(spacename, password) {
|
|
1523
|
+
// Validate input
|
|
1524
|
+
if (!spacename || !password ||
|
|
1525
|
+
typeof spacename !== 'string' ||
|
|
1526
|
+
typeof password !== 'string') {
|
|
1527
|
+
throw new Error('Invalid credentials format');
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
try {
|
|
1531
|
+
// Get space record
|
|
1532
|
+
const space = await this.getGlobal('spaces', spacename);
|
|
1533
|
+
if (!space || !space.auth) {
|
|
1534
|
+
throw new Error('Invalid spacename or password');
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Verify password using SEA
|
|
1538
|
+
const hash = await Gun.SEA.work(password, space.auth.salt);
|
|
1539
|
+
if (hash !== space.auth.hash) {
|
|
1540
|
+
throw new Error('Invalid spacename or password');
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Set current space with expiration
|
|
1544
|
+
this.currentSpace = {
|
|
1545
|
+
...space,
|
|
1546
|
+
exp: Date.now() + (24 * 60 * 60 * 1000) // 24 hour expiration
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
return true;
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
throw new Error('Authentication failed');
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Logs out the current space
|
|
1557
|
+
* @returns {Promise<void>}
|
|
1558
|
+
*/
|
|
1559
|
+
async logout() {
|
|
1560
|
+
this.currentSpace = null;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Checks if the current session is valid
|
|
1565
|
+
* @private
|
|
1566
|
+
*/
|
|
1567
|
+
_checkSession() {
|
|
1568
|
+
if (!this.currentSpace) {
|
|
1569
|
+
throw new Error('No active session');
|
|
1570
|
+
}
|
|
1571
|
+
if (this.currentSpace.exp < Date.now()) {
|
|
1572
|
+
this.currentSpace = null;
|
|
1573
|
+
throw new Error('Session expired');
|
|
1574
|
+
}
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Creates a federation relationship between two spaces
|
|
1580
|
+
* @param {string} spaceId1 - The first space ID
|
|
1581
|
+
* @param {string} spaceId2 - The second space ID
|
|
1582
|
+
* @returns {Promise<boolean>} - True if federation was created successfully
|
|
1583
|
+
*/
|
|
1584
|
+
async federate(spaceId1, spaceId2) {
|
|
1585
|
+
if (!spaceId1 || !spaceId2) {
|
|
1586
|
+
throw new Error('federate: Missing required parameters');
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// Get existing federation info for both spaces
|
|
1590
|
+
let fedInfo1 = await this.getGlobal('federation', spaceId1);
|
|
1591
|
+
let fedInfo2 = await this.getGlobal('federation', spaceId2);
|
|
1592
|
+
|
|
1593
|
+
// Check if federation already exists
|
|
1594
|
+
if (fedInfo1 && fedInfo1.federation && fedInfo1.federation.includes(spaceId2)) {
|
|
1595
|
+
throw new Error('Federation already exists');
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Create or update federation info for first space
|
|
1599
|
+
if (!fedInfo1) {
|
|
1600
|
+
fedInfo1 = {
|
|
1601
|
+
id: spaceId1,
|
|
1602
|
+
name: spaceId1,
|
|
1603
|
+
federation: [],
|
|
1604
|
+
notify: []
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
if (!fedInfo1.federation) fedInfo1.federation = [];
|
|
1608
|
+
fedInfo1.federation.push(spaceId2);
|
|
1609
|
+
|
|
1610
|
+
// Create or update federation info for second space
|
|
1611
|
+
if (!fedInfo2) {
|
|
1612
|
+
fedInfo2 = {
|
|
1613
|
+
id: spaceId2,
|
|
1614
|
+
name: spaceId2,
|
|
1615
|
+
federation: [],
|
|
1616
|
+
notify: []
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
if (!fedInfo2.notify) fedInfo2.notify = [];
|
|
1620
|
+
fedInfo2.notify.push(spaceId1);
|
|
1621
|
+
|
|
1622
|
+
// Save both federation records
|
|
1623
|
+
await this.putGlobal('federation', fedInfo1);
|
|
1624
|
+
await this.putGlobal('federation', fedInfo2);
|
|
1625
|
+
|
|
1626
|
+
return true;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Subscribes to federation notifications for a space
|
|
1631
|
+
* @param {string} spaceId - The space ID to subscribe to
|
|
1632
|
+
* @param {function} callback - The callback to execute on notifications
|
|
1633
|
+
* @returns {Promise<object>} - Subscription object with off() method
|
|
1634
|
+
*/
|
|
1635
|
+
async subscribeFederation(spaceId, callback) {
|
|
1636
|
+
if (!spaceId || !callback) {
|
|
1637
|
+
throw new Error('subscribeFederation: Missing required parameters');
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Get federation info
|
|
1641
|
+
const fedInfo = await this.getGlobal('federation', spaceId);
|
|
1642
|
+
if (!fedInfo) {
|
|
1643
|
+
throw new Error('No federation info found for space');
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Create subscription for each federated space
|
|
1647
|
+
const subscriptions = [];
|
|
1648
|
+
if (fedInfo.federation && fedInfo.federation.length > 0) {
|
|
1649
|
+
for (const federatedSpace of fedInfo.federation) {
|
|
1650
|
+
// Subscribe to all lenses in the federated space
|
|
1651
|
+
const sub = await this.subscribe(federatedSpace, '*', async (data) => {
|
|
1652
|
+
try {
|
|
1653
|
+
// Only notify if the data has federation info and is from the federated space
|
|
1654
|
+
if (data && data.federation && data.federation.origin === federatedSpace) {
|
|
1655
|
+
await callback(data);
|
|
1656
|
+
}
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
console.warn('Federation notification error:', error);
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
subscriptions.push(sub);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// Return combined subscription object
|
|
1666
|
+
return {
|
|
1667
|
+
off: () => {
|
|
1668
|
+
subscriptions.forEach(sub => {
|
|
1669
|
+
if (sub && typeof sub.off === 'function') {
|
|
1670
|
+
sub.off();
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Gets federation info for a space
|
|
1679
|
+
* @param {string} spaceId - The space ID
|
|
1680
|
+
* @returns {Promise<object|null>} - Federation info or null if not found
|
|
1681
|
+
*/
|
|
1682
|
+
async getFederation(spaceId) {
|
|
1683
|
+
if (!spaceId) {
|
|
1684
|
+
throw new Error('getFederationInfo: Missing space ID');
|
|
1685
|
+
}
|
|
1686
|
+
return await this.getGlobal('federation', spaceId);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Removes a federation relationship between spaces
|
|
1691
|
+
* @param {string} spaceId1 - The first space ID
|
|
1692
|
+
* @param {string} spaceId2 - The second space ID
|
|
1693
|
+
* @returns {Promise<boolean>} - True if federation was removed successfully
|
|
1694
|
+
*/
|
|
1695
|
+
async unfederate(spaceId1, spaceId2) {
|
|
1696
|
+
if (!spaceId1 || !spaceId2) {
|
|
1697
|
+
throw new Error('unfederate: Missing required parameters');
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// Get federation info for both spaces
|
|
1701
|
+
const fedInfo1 = await this.getGlobal('federation', spaceId1);
|
|
1702
|
+
const fedInfo2 = await this.getGlobal('federation', spaceId2);
|
|
1703
|
+
|
|
1704
|
+
if (fedInfo1) {
|
|
1705
|
+
fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
|
|
1706
|
+
await this.putGlobal('federation', fedInfo1);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (fedInfo2) {
|
|
1710
|
+
fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
|
|
1711
|
+
await this.putGlobal('federation', fedInfo2);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Gets the name of a chat/space
|
|
1719
|
+
* @param {string} spaceId - The space ID
|
|
1720
|
+
* @returns {Promise<string>} - The space name or the ID if not found
|
|
1721
|
+
*/
|
|
1722
|
+
async getChatName(spaceId) {
|
|
1723
|
+
const spaceInfo = await this.getGlobal('spaces', spaceId);
|
|
1724
|
+
return spaceInfo?.name || spaceId;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Gets data from a holon and lens, including data from federated spaces with optional aggregation
|
|
1729
|
+
* @param {string} holon - The holon identifier
|
|
1730
|
+
* @param {string} lens - The lens identifier
|
|
1731
|
+
* @param {object} options - Options for data retrieval and aggregation
|
|
1732
|
+
* @param {boolean} options.aggregate - Whether to aggregate items with matching IDs (default: false)
|
|
1733
|
+
* @param {string} options.idField - Field to use as identifier for aggregation (default: 'id')
|
|
1734
|
+
* @param {string[]} options.sumFields - Numeric fields to sum during aggregation (e.g., ['received', 'sent'])
|
|
1735
|
+
* @param {string[]} options.concatArrays - Array fields to concatenate during aggregation (e.g., ['wants', 'offers'])
|
|
1736
|
+
* @param {boolean} options.removeDuplicates - Whether to remove duplicates when not aggregating (default: true)
|
|
1737
|
+
* @param {function} options.mergeStrategy - Custom function to merge items during aggregation
|
|
1738
|
+
* @returns {Promise<Array>} - Combined array of local and federated data
|
|
1739
|
+
*/
|
|
1740
|
+
async getFederated(holon, lens, options = {}) {
|
|
1741
|
+
// Validate required parameters
|
|
1742
|
+
if (!holon || !lens) {
|
|
1743
|
+
throw new Error('getFederated: Missing required parameters');
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
const {
|
|
1747
|
+
aggregate = false,
|
|
1748
|
+
idField = 'id',
|
|
1749
|
+
sumFields = [],
|
|
1750
|
+
concatArrays = [],
|
|
1751
|
+
removeDuplicates = true,
|
|
1752
|
+
mergeStrategy = null
|
|
1753
|
+
} = options;
|
|
1754
|
+
|
|
1755
|
+
// Get federation info for current space
|
|
1756
|
+
const fedInfo = await this.getFederation(this.currentSpace?.alias);
|
|
1757
|
+
|
|
1758
|
+
// Get local data
|
|
1759
|
+
const localData = await this.getAll(holon, lens);
|
|
1760
|
+
|
|
1761
|
+
// If no federation or not authenticated, return local data only
|
|
1762
|
+
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
1763
|
+
return localData;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// Get data from each federated space
|
|
1767
|
+
const federatedData = await Promise.all(
|
|
1768
|
+
fedInfo.federation.map(async (federatedSpace) => {
|
|
1769
|
+
try {
|
|
1770
|
+
const data = await this.getAll(federatedSpace, lens);
|
|
1771
|
+
return data || [];
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
console.warn(`Error getting data from federated space ${federatedSpace}:`, error);
|
|
1774
|
+
return [];
|
|
1775
|
+
}
|
|
1776
|
+
})
|
|
1777
|
+
);
|
|
1778
|
+
|
|
1779
|
+
// Combine all data
|
|
1780
|
+
const allData = [...localData, ...federatedData.flat()];
|
|
1781
|
+
|
|
1782
|
+
// If aggregating, use enhanced aggregation logic
|
|
1783
|
+
if (aggregate) {
|
|
1784
|
+
const aggregated = new Map();
|
|
1785
|
+
|
|
1786
|
+
for (const item of allData) {
|
|
1787
|
+
const itemId = item[idField];
|
|
1788
|
+
if (!itemId) continue;
|
|
1789
|
+
|
|
1790
|
+
const existing = aggregated.get(itemId);
|
|
1791
|
+
if (!existing) {
|
|
1792
|
+
aggregated.set(itemId, { ...item });
|
|
1793
|
+
} else {
|
|
1794
|
+
// If custom merge strategy is provided, use it
|
|
1795
|
+
if (mergeStrategy && typeof mergeStrategy === 'function') {
|
|
1796
|
+
aggregated.set(itemId, mergeStrategy(existing, item));
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// Enhanced default merge strategy
|
|
1801
|
+
const merged = { ...existing };
|
|
1802
|
+
|
|
1803
|
+
// Sum numeric fields
|
|
1804
|
+
for (const field of sumFields) {
|
|
1805
|
+
if (typeof item[field] === 'number') {
|
|
1806
|
+
merged[field] = (merged[field] || 0) + (item[field] || 0);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Concatenate and deduplicate array fields
|
|
1811
|
+
for (const field of concatArrays) {
|
|
1812
|
+
if (Array.isArray(item[field])) {
|
|
1813
|
+
const combinedArray = [
|
|
1814
|
+
...(merged[field] || []),
|
|
1815
|
+
...(item[field] || [])
|
|
1816
|
+
];
|
|
1817
|
+
// Remove duplicates if elements are primitive
|
|
1818
|
+
merged[field] = Array.from(new Set(combinedArray));
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Update federation metadata
|
|
1823
|
+
merged.federation = {
|
|
1824
|
+
...merged.federation,
|
|
1825
|
+
timestamp: Math.max(
|
|
1826
|
+
merged.federation?.timestamp || 0,
|
|
1827
|
+
item.federation?.timestamp || 0
|
|
1828
|
+
),
|
|
1829
|
+
origins: Array.from(new Set([
|
|
1830
|
+
...(merged.federation?.origins || [merged.federation?.origin]),
|
|
1831
|
+
...(item.federation?.origins || [item.federation?.origin])
|
|
1832
|
+
]).filter(Boolean))
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
// Update the aggregated item
|
|
1836
|
+
aggregated.set(itemId, merged);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
return Array.from(aggregated.values());
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// If not aggregating, optionally remove duplicates based on idField
|
|
1844
|
+
if (!removeDuplicates) {
|
|
1845
|
+
return allData;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Remove duplicates keeping the most recent version
|
|
1849
|
+
const uniqueMap = new Map();
|
|
1850
|
+
allData.forEach(item => {
|
|
1851
|
+
const id = item[idField];
|
|
1852
|
+
if (!id) return;
|
|
1853
|
+
|
|
1854
|
+
const existing = uniqueMap.get(id);
|
|
1855
|
+
if (!existing ||
|
|
1856
|
+
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
1857
|
+
uniqueMap.set(id, item);
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
return Array.from(uniqueMap.values());
|
|
999
1861
|
}
|
|
1000
1862
|
}
|
|
1001
1863
|
|