holosphere 1.1.2 → 1.1.3

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