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 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
- 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,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
- this.subscriptions = new Map(); // Track active subscriptions
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(schemaString, ack => {
118
+ .put(schemaData, ack => {
101
119
  if (ack.err) {
102
120
  reject(new Error(ack.err));
103
121
  } else {
104
- resolve(true);
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 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));
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
- resolve(null);
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
- if (!holon || !lens || !data) {
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 (!data.id) {
169
- data.id = this.generateId();
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
- // Clone data to avoid modifying original
175
- const dataToValidate = JSON.parse(JSON.stringify(data));
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
- throw new Error(`Schema validation failed: ${JSON.stringify(this.validator.errors)}`);
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
- return new Promise((resolve, reject) => {
237
+ // Store data in current space
238
+ const putResult = await new Promise((resolve, reject) => {
187
239
  try {
188
- const payload = JSON.stringify(data);
240
+ const payload = JSON.stringify(dataWithMeta);
189
241
  this.gun.get(this.appname)
190
242
  .get(holon)
191
243
  .get(lens)
192
- .get(data.id)
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
- return new Promise((resolve) => {
223
- const output = new Set();
224
- const promises = new Set();
225
- let timeout;
345
+ // Get local data
346
+ const localData = await this._getAllLocal(holon, lens, schema);
226
347
 
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);
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
- } 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);
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
- const listener = this.gun.get(this.appname)
472
+ this.gun.get(this.appname)
251
473
  .get(holon)
252
474
  .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));
475
+ .once(async (data) => {
476
+ if (!data) {
477
+ cleanup();
478
+ resolve([]);
479
+ return;
480
+ }
258
481
 
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
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
- throw new Error(`Parse error: ${error.message}`);
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
- return new Promise((resolve) => {
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); // 5 second timeout
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,key) => {
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} node - The node to store.
804
+ * @param {object} data - The node to store.
463
805
  */
464
- async putNode(holon, lens, node) {
465
- if (!holon || !lens || !node) {
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
- .put(node, ack => {
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
- * @returns {Promise<object|null>} - The retrieved node or null if not found.
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
- console.error('getNode: Missing required parameters');
497
- return null;
839
+ throw new Error('getNode: Missing required parameters');
498
840
  }
499
841
 
500
- return this.gun.get(this.appname)
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
- // 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 [];
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 counter = 0;
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(lens).once((data, key) => {
988
+ this.gun.get(this.appname).get(tableName).once(async (data) => {
662
989
  if (!data) {
663
- resolve(output);
990
+ clearTimeout(timeout);
991
+ isResolved = true;
992
+ resolve([]);
664
993
  return;
665
994
  }
666
995
 
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);
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
- if (counter === mapLength) {
697
- resolve(output);
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
- await this.gun.get(this.appname).get(tableName).get(key).put(null)
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
- // 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
- // });
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
- * Computes summaries based on the content within a holon and lens.
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 {string} operation - The operation to perform.
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, operation, depth = 0, maxDepth = 15) {
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
- if (depth >= maxDepth) return;
744
-
745
- const res = h3.getResolution(holon);
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
- const parent = h3.cellToParent(holon, res - 1);
751
- const siblings = h3.cellToChildren(parent, res);
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
- this.gun.get(this.appname)
762
- .get(sibling)
763
- .get(lens)
764
- .map()
765
- .once((data) => {
766
- clearTimeout(timeout);
767
- if (data?.content) {
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
- await Promise.all(promises);
1195
+ if (res < 1 || res > 15) {
1196
+ throw new Error('compute: Invalid holon resolution (must be between 1 and 15)');
1197
+ }
776
1198
 
777
- 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
- });
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
- if (res > 1) { // Only recurse if not at top level
787
- await this.compute(parent, lens, operation, depth + 1, maxDepth);
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
- * Clears all entities under a specific holon and lens.
794
- * @param {string} holon - The holon identifier.
795
- * @param {string} lens - The lens to clear.
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
- return new Promise((resolve, reject) => {
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
- const deletions = new Set();
805
- const timeout = setTimeout(() => {
806
- if (deletions.size === 0) {
807
- resolve(); // No data to delete
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
- this.gun.get(this.appname)
812
- .get(holon)
813
- .get(lens)
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
- reject(error);
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
- 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);
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 holonagon with a new report.
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 = `${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
- });
978
-
979
- this.subscriptions.set(subscriptionId, listener);
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
- listener.off();
984
- this.subscriptions.delete(subscriptionId);
985
- },
986
- id: subscriptionId
1436
+ if (this.subscriptions[subscriptionId]) {
1437
+ delete this.subscriptions[subscriptionId];
1438
+ }
1439
+ }
987
1440
  };
988
1441
  }
989
1442
 
990
- // Add cleanup method
991
- cleanup() {
992
- this.subscriptions.forEach(listener => listener.off());
993
- this.subscriptions.clear();
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(36) + Math.random().toString(36).substr(2);
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