holosphere 1.1.1 → 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,19 +21,27 @@ 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({
23
- peers: ['https://gun.holons.io', 'https://59.src.eco/gun'],
24
+
25
+ // Use provided Gun instance or create new one
26
+ this.gun = gunInstance || Gun({
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
26
30
  // console.log('uuid', content);
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
  }
42
+
43
+ // Add currentSpace property to track logged in space
44
+ this.currentSpace = null;
35
45
  }
36
46
 
37
47
  // ================================ SCHEMA FUNCTIONS ================================
@@ -44,81 +54,74 @@ class HoloSphere {
44
54
  */
45
55
  async setSchema(lens, schema) {
46
56
  if (!lens || !schema) {
47
- console.error('setSchema: Missing required parameters');
48
- return false;
57
+ throw new Error('setSchema: Missing required parameters');
49
58
  }
50
59
 
51
- // Basic schema validation - check for required fields
60
+ // Basic schema validation
52
61
  if (!schema.type || typeof schema.type !== 'string') {
53
- console.error('setSchema: Schema must have a type field');
54
- return false;
62
+ throw new Error('setSchema: Schema must have a type field');
55
63
  }
56
64
 
57
65
  if (this.strict) {
58
- try {
59
- // Validate schema against JSON Schema meta-schema
60
- const metaSchema = {
61
- type: 'object',
62
- required: ['type', 'properties'],
66
+ const metaSchema = {
67
+ type: 'object',
68
+ required: ['type', 'properties'],
69
+ properties: {
70
+ type: { type: 'string' },
63
71
  properties: {
64
- type: { type: 'string' },
65
- properties: {
72
+ type: 'object',
73
+ additionalProperties: {
66
74
  type: 'object',
67
- additionalProperties: {
68
- type: 'object',
69
- required: ['type'],
70
- properties: {
71
- type: { type: 'string' }
72
- }
75
+ required: ['type'],
76
+ properties: {
77
+ type: { type: 'string' }
73
78
  }
74
- },
75
- required: {
76
- type: 'array',
77
- items: { type: 'string' }
78
79
  }
80
+ },
81
+ required: {
82
+ type: 'array',
83
+ items: { type: 'string' }
79
84
  }
80
- };
81
-
82
- const valid = this.validator.validate(metaSchema, schema);
83
- if (!valid) {
84
- console.error('setSchema: Invalid schema structure:', this.validator.errors);
85
- return false;
86
85
  }
86
+ };
87
87
 
88
- // Additional strict mode checks
89
- if (!schema.properties || typeof schema.properties !== 'object') {
90
- console.error('setSchema: Schema must have properties in strict mode');
91
- return false;
92
- }
88
+ const valid = this.validator.validate(metaSchema, schema);
89
+ if (!valid) {
90
+ throw new Error(`Invalid schema structure: ${JSON.stringify(this.validator.errors)}`);
91
+ }
93
92
 
94
- if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
95
- console.error('setSchema: Schema must have required fields in strict mode');
96
- return false;
97
- }
98
- } catch (error) {
99
- console.error('setSchema: Schema validation error:', error);
100
- return false;
93
+ if (!schema.properties || typeof schema.properties !== 'object') {
94
+ throw new Error('Schema must have properties in strict mode');
95
+ }
96
+
97
+ if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
98
+ throw new Error('Schema must have required fields in strict mode');
101
99
  }
102
100
  }
103
101
 
104
- return new Promise((resolve) => {
102
+ return new Promise((resolve, reject) => {
105
103
  try {
106
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
+
107
112
  this.gun.get(this.appname)
108
113
  .get(lens)
109
114
  .get('schema')
110
- .put(schemaString, ack => {
115
+ .put(schemaData, ack => {
111
116
  if (ack.err) {
112
- console.error('Failed to add schema:', ack.err);
113
- resolve(false);
117
+ reject(new Error(ack.err));
114
118
  } else {
115
- console.log('Schema added successfully for lens:', lens);
116
- resolve(true);
119
+ // Add small delay to ensure data is written
120
+ setTimeout(() => resolve(true), 50);
117
121
  }
118
122
  });
119
123
  } catch (error) {
120
- console.error('setSchema: Error stringifying schema:', error);
121
- resolve(false);
124
+ reject(error);
122
125
  }
123
126
  });
124
127
  }
@@ -130,36 +133,35 @@ class HoloSphere {
130
133
  */
131
134
  async getSchema(lens) {
132
135
  if (!lens) {
133
- console.error('getSchema: Missing lens parameter');
134
- return null;
136
+ throw new Error('getSchema: Missing lens parameter');
135
137
  }
136
138
 
137
139
  return new Promise((resolve) => {
140
+ let timeout = setTimeout(() => {
141
+ console.warn('getSchema: Operation timed out');
142
+ resolve(null);
143
+ }, 5000);
144
+
138
145
  this.gun.get(this.appname)
139
146
  .get(lens)
140
147
  .get('schema')
141
148
  .once(data => {
149
+ clearTimeout(timeout);
142
150
  if (!data) {
143
151
  resolve(null);
144
152
  return;
145
153
  }
146
154
 
147
155
  try {
148
- // If data is already a string, parse it
149
- if (typeof data === 'string') {
150
- resolve(JSON.parse(data));
151
- }
152
- // If data is an object with a string value (GunDB format)
153
- else if (typeof data === 'object' && data !== null) {
154
- const schemaStr = Object.values(data).find(v =>
155
- typeof v === 'string' && v.includes('"type":'));
156
- if (schemaStr) {
157
- resolve(JSON.parse(schemaStr));
158
- } else {
159
- resolve(null);
160
- }
156
+ // Handle both new format and legacy format
157
+ if (data.schema) {
158
+ // New format with timestamp
159
+ resolve(JSON.parse(data.schema));
161
160
  } else {
162
- 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);
163
165
  }
164
166
  } catch (error) {
165
167
  console.error('getSchema: Error parsing schema:', error);
@@ -179,56 +181,140 @@ class HoloSphere {
179
181
  * @returns {Promise<boolean>} - Returns true if successful, false if there was an error
180
182
  */
181
183
  async put(holon, lens, data) {
182
- if (!holon || !lens || !data) {
183
- console.error('put: Missing required parameters:', { holon, lens, data });
184
- return false;
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
+ }
185
198
  }
186
199
 
187
- if (!data.id) {
188
- console.error('put: Data must have an id field');
189
- return false;
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) {
211
+ throw new Error('put: Missing required parameters');
212
+ }
213
+
214
+ if (!dataWithMeta.id) {
215
+ dataWithMeta.id = this.generateId();
190
216
  }
191
217
 
192
- // Strict validation of schema and data
218
+ // Get and validate schema first
193
219
  const schema = await this.getSchema(lens);
194
220
  if (schema) {
195
- try {
196
- const valid = this.validator.validate(schema, data);
197
- if (!valid) {
198
- const errors = this.validator.errors;
199
- console.error('put: Schema validation failed:', errors);
200
- return false;
201
- }
202
- } catch (error) {
203
- console.error('put: Schema validation error:', error);
204
- return false;
221
+ // Deep clone data to avoid modifying the original
222
+ const dataToValidate = JSON.parse(JSON.stringify(dataWithMeta));
223
+ const valid = this.validator.validate(schema, dataToValidate);
224
+
225
+ if (!valid) {
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);
205
229
  }
206
230
  } else if (this.strict) {
207
- console.error('put: Schema required in strict mode for lens:', lens);
208
- return false;
231
+ throw new Error('Schema required in strict mode');
209
232
  }
210
233
 
211
- return new Promise((resolve) => {
234
+ // Store data in current space
235
+ const putResult = await new Promise((resolve, reject) => {
212
236
  try {
213
- const payload = JSON.stringify(data);
214
-
237
+ const payload = JSON.stringify(dataWithMeta);
215
238
  this.gun.get(this.appname)
216
239
  .get(holon)
217
240
  .get(lens)
218
- .get(data.id)
241
+ .get(dataWithMeta.id)
219
242
  .put(payload, ack => {
220
243
  if (ack.err) {
221
- console.error("Error adding data to GunDB:", ack.err);
222
- resolve(false);
244
+ reject(new Error(ack.err));
223
245
  } else {
224
246
  resolve(true);
225
247
  }
226
248
  });
227
249
  } catch (error) {
228
- console.error('Error in put operation:', error);
229
- resolve(false);
250
+ reject(error);
230
251
  }
231
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
+ }
232
318
  }
233
319
 
234
320
  /**
@@ -239,120 +325,282 @@ class HoloSphere {
239
325
  */
240
326
  async getAll(holon, lens) {
241
327
  if (!holon || !lens) {
242
- console.error('getAll: Missing required parameters:', { holon, lens });
243
- return [];
328
+ throw new Error('getAll: Missing required parameters');
244
329
  }
245
330
 
246
331
  const schema = await this.getSchema(lens);
247
332
  if (!schema && this.strict) {
248
- console.error('getAll: Schema required in strict mode for lens:', lens);
249
- return [];
333
+ throw new Error('getAll: Schema required in strict mode');
250
334
  }
251
335
 
252
- return new Promise((resolve) => {
253
- let output = [];
254
- let counter = 0;
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
+ }
255
344
 
256
- this.gun.get(this.appname).get(holon).get(lens).once((data, key) => {
257
- if (!data) {
258
- resolve(output);
259
- return;
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);
260
362
  }
363
+ }
364
+ });
261
365
 
262
- const mapLength = Object.keys(data).length - 1;
366
+ return Array.from(combined.values());
367
+ }
263
368
 
264
- this.gun.get(this.appname).get(holon).get(lens).map().once(async (itemdata, key) => {
265
- counter += 1;
266
- if (itemdata) {
267
- try {
268
- const parsed = await this.parse(itemdata);
269
- if (schema) {
270
- const valid = this.validator.validate(schema, parsed);
271
- if (valid) {
272
- output.push(parsed);
273
- } else if (this.strict) {
274
- console.warn('Invalid data removed:', key, this.validator.errors);
275
- await this.delete(holon, lens, key);
276
- } else {
277
- console.warn('Invalid data found:', key, this.validator.errors);
278
- output.push(parsed);
279
- }
280
- } else {
281
- output.push(parsed);
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;
282
395
  }
283
- } catch (error) {
284
- console.error('Error parsing data:', error);
285
- if (this.strict) {
286
- 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);
287
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);
288
454
  }
455
+ } else {
456
+ output.set(parsed.id, parsed);
457
+ }
458
+ } catch (error) {
459
+ console.error('Error processing data:', error);
460
+ }
461
+ };
462
+
463
+ this.gun.get(this.appname)
464
+ .get(holon)
465
+ .get(lens)
466
+ .once(async (data) => {
467
+ if (!data) {
468
+ cleanup();
469
+ resolve([]);
470
+ return;
289
471
  }
290
472
 
291
- if (counter === mapLength) {
292
- resolve(output);
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([]);
293
487
  }
294
488
  });
295
- });
296
489
  });
297
490
  }
298
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
+
299
555
  /**
300
556
  * Parses data from GunDB, handling various data formats and references.
301
557
  * @param {*} data - The data to parse, could be a string, object, or GunDB reference.
302
558
  * @returns {Promise<object>} - The parsed data.
303
559
  */
304
560
  async parse(rawData) {
305
- let parsedData = {};
306
- if (rawData.soul) {
307
- console.log('Parsing link:', rawData.soul);
308
- this.getNodeRef(rawData.soul).once (data => {return JSON.parse(data)});
309
- }
310
-
311
- if (typeof rawData === 'object' && rawData !== null) {
312
- if (rawData._ && rawData._["#"]) {
313
- console.log('Parsing object reference:', rawData._['#']);
314
- // If the data is a reference, fetch the actual content
315
- let pathParts = rawData._['#'].split('/');
316
- let hexId = pathParts[1];
317
- let lensId = pathParts[2];
318
- let dataKey = pathParts[3];
319
- parsedData = await this.get(hexId, lensId, dataKey);
320
- } else if (rawData._ && rawData._['>']) {
321
- console.log('Parsing objectnode:', rawData._['>']);
322
- // This might be a GunDB node, try to get its value
323
- const nodeValue = Object.values(rawData).find(v => typeof v !== 'object' && v !== '_');
324
- if (nodeValue) {
325
- try {
326
- parsedData = JSON.parse(nodeValue);
327
- } catch (e) {
328
- console.log('Invalid JSON in node value:', nodeValue);
329
- parsedData = nodeValue; // return the raw data
561
+ if (!rawData) {
562
+ throw new Error('parse: No data provided');
563
+ }
564
+
565
+ try {
566
+ if (rawData.soul) {
567
+ const data = await this.getNodeRef(rawData.soul).once();
568
+ if (!data) {
569
+ throw new Error('Referenced data not found');
570
+ }
571
+ return JSON.parse(data);
572
+ }
573
+
574
+ let parsedData = {};
575
+ if (typeof rawData === 'object' && rawData !== null) {
576
+ if (rawData._ && rawData._["#"]) {
577
+ const pathParts = rawData._['#'].split('/');
578
+ if (pathParts.length < 4) {
579
+ throw new Error('Invalid reference format');
580
+ }
581
+ parsedData = await this.get(pathParts[1], pathParts[2], pathParts[3]);
582
+ if (!parsedData) {
583
+ throw new Error('Referenced data not found');
330
584
  }
585
+ } else if (rawData._ && rawData._['>']) {
586
+ const nodeValue = Object.values(rawData).find(v => typeof v !== 'object' && v !== '_');
587
+ if (!nodeValue) {
588
+ throw new Error('Invalid node data');
589
+ }
590
+ parsedData = JSON.parse(nodeValue);
331
591
  } else {
332
- console.log('Unable to parse GunDB node:', rawData);
333
- parsedData = rawData; // return the original data
592
+ parsedData = rawData;
334
593
  }
335
594
  } else {
336
- // Treat it as object data
337
- console.log('Parsing object data:', rawData);
338
- parsedData = rawData;
339
- }
340
- } else {
341
- // If it's not an object, try parsing it as JSON
342
- try {
343
595
  parsedData = JSON.parse(rawData);
344
- //if the data has a soul, return the soul node
345
- if (parsedData.soul) {
346
- console.log('Parsing link:', parsedData.soul);
347
- parsedData = await this.get(parsedData.soul.split('/')[1], parsedData.soul.split('/')[2], parsedData.soul.split('/')[3]);
348
- }
349
- } catch (e) {
350
- console.log('Failed to parse, returning raw data', e);
351
- parsedData = rawData; // return the raw data
352
596
  }
353
- }
354
597
 
355
- return parsedData;
598
+ return parsedData;
599
+ } catch (error) {
600
+ console.log("Parsing not a JSON, returning raw data", rawData);
601
+ return rawData;
602
+ //throw new Error(`Parse error: ${error.message}`);
603
+ }
356
604
  }
357
605
 
358
606
  /**
@@ -371,17 +619,18 @@ class HoloSphere {
371
619
  // Get schema for validation
372
620
  const schema = await this.getSchema(lens);
373
621
 
374
- return new Promise((resolve) => {
622
+ // First try to get from current space
623
+ const localResult = await new Promise((resolve) => {
375
624
  let timeout = setTimeout(() => {
376
625
  console.warn('get: Operation timed out');
377
626
  resolve(null);
378
- }, 5000); // 5 second timeout
627
+ }, 5000);
379
628
 
380
629
  this.gun.get(this.appname)
381
630
  .get(holon)
382
631
  .get(lens)
383
632
  .get(key)
384
- .once((data,key) => {
633
+ .once(async (data) => {
385
634
  clearTimeout(timeout);
386
635
 
387
636
  if (!data) {
@@ -390,7 +639,7 @@ class HoloSphere {
390
639
  }
391
640
 
392
641
  try {
393
- const parsed = this.parse(data);
642
+ const parsed = await this.parse(data);
394
643
 
395
644
  // Validate against schema if one exists
396
645
  if (schema) {
@@ -403,6 +652,20 @@ class HoloSphere {
403
652
  }
404
653
  }
405
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
+
406
669
  resolve(parsed);
407
670
  } catch (error) {
408
671
  console.error('Error parsing data:', error);
@@ -410,6 +673,21 @@ class HoloSphere {
410
673
  }
411
674
  });
412
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;
413
691
  }
414
692
 
415
693
  /**
@@ -419,14 +697,41 @@ class HoloSphere {
419
697
  * @param {string} key - The specific key to delete.
420
698
  */
421
699
  async delete(holon, lens, key) {
700
+ if (!holon || !lens || !key) {
701
+ throw new Error('delete: Missing required parameters');
702
+ }
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
+
422
719
  return new Promise((resolve, reject) => {
423
- this.gun.get(this.appname).get(holon).get(lens).get(key).put(null, ack => {
424
- if (ack.err) {
425
- resolve(ack.err);
426
- } else {
427
- resolve(ack.ok);
428
- }
429
- });
720
+ try {
721
+ this.gun.get(this.appname)
722
+ .get(holon)
723
+ .get(lens)
724
+ .get(key)
725
+ .put(null, ack => {
726
+ if (ack.err) {
727
+ reject(new Error(ack.err));
728
+ } else {
729
+ resolve(true);
730
+ }
731
+ });
732
+ } catch (error) {
733
+ reject(error);
734
+ }
430
735
  });
431
736
  }
432
737
 
@@ -475,7 +780,7 @@ class HoloSphere {
475
780
  .catch(error => {
476
781
  console.error('Error in deleteAll:', error);
477
782
  resolve(false);
478
- });
783
+ });
479
784
  });
480
785
  });
481
786
  }
@@ -487,43 +792,75 @@ class HoloSphere {
487
792
  * Stores a specific gun node in a given holon and lens.
488
793
  * @param {string} holon - The holon identifier.
489
794
  * @param {string} lens - The lens under which to store the node.
490
- * @param {object} node - The node to store.
795
+ * @param {object} data - The node to store.
491
796
  */
492
- async putNode(holon, lens, node) {
493
- return new Promise((resolve) => {
494
- this.gun.get(this.appname).get(holon).get(lens).put(node, ack => {
495
- if (ack.err) {
496
- console.error("Error adding data to GunDB:", ack.err);
497
- resolve(false);
498
- } else {
499
- resolve(true);
500
- }
501
- });
797
+ async putNode(holon, lens, data) {
798
+ if (!holon || !lens || !data) {
799
+ throw new Error('putNode: Missing required parameters');
800
+ }
801
+
802
+ return new Promise((resolve, reject) => {
803
+ try {
804
+ this.gun.get(this.appname)
805
+ .get(holon)
806
+ .get(lens)
807
+ .get('value') // Store at 'value' key
808
+ .put(data.value, ack => { // Store the value directly
809
+ if (ack.err) {
810
+ reject(new Error(ack.err));
811
+ } else {
812
+ resolve(true);
813
+ }
814
+ });
815
+ } catch (error) {
816
+ reject(error);
817
+ }
502
818
  });
503
819
  }
504
820
 
505
821
  /**
506
- * Retrieves a specific gun node from the specified holon and lens.
507
- * @param {string} holon - The holon identifier.
508
- * @param {string} lens - The lens identifier.
509
- * @param {string} key - The specific key to retrieve.
510
- * @returns {Promise<object|null>} - The retrieved node or null if not found.
511
- */
512
- getNode(holon, lens, key) {
822
+ * Retrieves a specific gun node from the specified holon and lens.
823
+ * @param {string} holon - The holon identifier.
824
+ * @param {string} lens - The lens identifier.
825
+ * @param {string} key - The specific key to retrieve.
826
+ * @returns {Promise<any>} - The retrieved node or null if not found.
827
+ */
828
+ async getNode(holon, lens, key) {
513
829
  if (!holon || !lens || !key) {
514
- console.error('getNode: Missing required parameters');
515
- return null;
830
+ throw new Error('getNode: Missing required parameters');
516
831
  }
517
832
 
518
- return this.gun.get(this.appname)
519
- .get(holon)
520
- .get(lens)
521
- .get(key)
522
-
833
+ return new Promise((resolve) => {
834
+ this.gun.get(this.appname)
835
+ .get(holon)
836
+ .get(lens)
837
+ .get(key)
838
+ .once((data) => {
839
+ if (!data) {
840
+ resolve(null);
841
+ return;
842
+ }
843
+ resolve(data); // Return the data directly
844
+ });
845
+ });
523
846
  }
524
847
 
525
848
  getNodeRef(soul) {
526
- const parts = soul.split('/');
849
+ if (typeof soul !== 'string' || !soul) {
850
+ throw new Error('getNodeRef: Invalid soul parameter');
851
+ }
852
+
853
+ const parts = soul.split('/').filter(part => {
854
+ if (!part.trim() || /[<>:"/\\|?*]/.test(part)) {
855
+ throw new Error('getNodeRef: Invalid path segment');
856
+ }
857
+ return part.trim();
858
+ });
859
+
860
+ if (parts.length === 0) {
861
+ throw new Error('getNodeRef: Invalid soul format');
862
+ }
863
+
527
864
  let ref = this.gun.get(this.appname);
528
865
  parts.forEach(part => {
529
866
  ref = ref.get(part);
@@ -540,19 +877,16 @@ class HoloSphere {
540
877
  */
541
878
  async deleteNode(holon, lens, key) {
542
879
  if (!holon || !lens || !key) {
543
- console.error('deleteNode: Missing required parameters');
544
- return false;
880
+ throw new Error('deleteNode: Missing required parameters');
545
881
  }
546
-
547
- return new Promise((resolve) => {
882
+ return new Promise((resolve, reject) => {
548
883
  this.gun.get(this.appname)
549
884
  .get(holon)
550
885
  .get(lens)
551
886
  .get(key)
552
887
  .put(null, ack => {
553
888
  if (ack.err) {
554
- console.error('deleteNode: Error deleting node:', ack.err);
555
- resolve(false);
889
+ reject(new Error(ack.err));
556
890
  } else {
557
891
  resolve(true);
558
892
  }
@@ -568,30 +902,31 @@ class HoloSphere {
568
902
  * @returns {Promise<void>}
569
903
  */
570
904
  async putGlobal(tableName, data) {
571
-
572
905
  return new Promise((resolve, reject) => {
573
- if (!tableName || !data) {
574
- reject(new Error('Table name and data are required'));
575
- return;
576
- }
577
-
906
+ try {
907
+ if (!tableName || !data) {
908
+ throw new Error('Table name and data are required');
909
+ }
578
910
 
579
- if (data.id) {
580
- this.gun.get(this.appname).get(tableName).get(data.id).put(JSON.stringify(data), ack => {
581
- if (ack.err) {
582
- reject(new Error(ack.err));
583
- } else {
584
- resolve();
585
- }
586
- });
587
- } else {
588
- this.gun.get(this.appname).get(tableName).put(JSON.stringify(data), ack => {
589
- if (ack.err) {
590
- reject(new Error(ack.err));
591
- } else {
592
- resolve();
593
- }
594
- });
911
+ if (data.id) {
912
+ this.gun.get(this.appname).get(tableName).get(data.id).put(JSON.stringify(data), ack => {
913
+ if (ack.err) {
914
+ reject(new Error(ack.err));
915
+ } else {
916
+ resolve();
917
+ }
918
+ });
919
+ } else {
920
+ this.gun.get(this.appname).get(tableName).put(JSON.stringify(data), ack => {
921
+ if (ack.err) {
922
+ reject(new Error(ack.err));
923
+ } else {
924
+ resolve();
925
+ }
926
+ });
927
+ }
928
+ } catch (error) {
929
+ reject(error);
595
930
  }
596
931
  });
597
932
  }
@@ -626,88 +961,54 @@ class HoloSphere {
626
961
  * @param {string} tableName - The table name to retrieve data from.
627
962
  * @returns {Promise<object|null>} - The parsed data from the table or null if not found.
628
963
  */
629
- // async getAllGlobal(tableName) {
630
- // return new Promise(async (resolve, reject) => {
631
- // let output = []
632
- // let counter = 0
633
- // this.gun.get(this.appname).get(tableName.toString()).once((data, key) => {
634
- // if (data) {
635
- // const maplenght = Object.keys(data).length - 1
636
- // this.gun.get(this.appname).get(tableName.toString()).map().once(async (itemdata, key) => {
637
-
638
- // counter += 1
639
- // if (itemdata) {
640
- // let parsed = await this.parse(itemdata)
641
- // output.push(parsed);
642
- // console.log('getAllGlobal: parsed: ', parsed)
643
- // }
644
-
645
- // if (counter == maplenght) {
646
- // resolve(output);
647
-
648
- // }
649
- // }
650
- // );
651
- // } else resolve(output)
652
- // })
653
- // }
654
- // )
655
- // }
656
- async getAllGlobal(lens) {
657
- if ( !lens) {
658
- console.error('getAll: Missing required parameters:', { lens });
659
- return [];
660
- }
661
-
662
- const schema = await this.getSchema(lens);
663
- if (!schema && this.strict) {
664
- console.error('getAll: Schema required in strict mode for lens:', lens);
665
- return [];
964
+ async getAllGlobal(tableName) {
965
+ if (!tableName) {
966
+ throw new Error('getAllGlobal: Missing table name parameter');
666
967
  }
667
968
 
668
969
  return new Promise((resolve) => {
669
970
  let output = [];
670
- let counter = 0;
971
+ let isResolved = false;
972
+ let timeout = setTimeout(() => {
973
+ if (!isResolved) {
974
+ isResolved = true;
975
+ resolve(output);
976
+ }
977
+ }, 5000);
671
978
 
672
- this.gun.get(this.appname).get(lens).once((data, key) => {
979
+ this.gun.get(this.appname).get(tableName).once(async (data) => {
673
980
  if (!data) {
674
- resolve(output);
981
+ clearTimeout(timeout);
982
+ isResolved = true;
983
+ resolve([]);
675
984
  return;
676
985
  }
677
986
 
678
- const mapLength = Object.keys(data).length - 1;
679
-
680
- this.gun.get(this.appname).get(lens).map().once(async (itemdata, key) => {
681
- counter += 1;
682
- if (itemdata) {
683
- try {
684
- const parsed = await this.parse(itemdata);
685
- if (schema) {
686
- const valid = this.validator.validate(schema, parsed);
687
- if (valid) {
688
- output.push(parsed);
689
- } else if (this.strict) {
690
- console.warn('Invalid data removed:', key, this.validator.errors);
691
- await this.delete(holon, lens, key);
692
- } else {
693
- console.warn('Invalid data found:', key, this.validator.errors);
694
- output.push(parsed);
695
- }
696
- } else {
697
- output.push(parsed);
698
- }
699
- } catch (error) {
700
- console.error('Error parsing data:', error);
701
- if (this.strict) {
702
- 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);
703
1000
  }
704
1001
  }
705
- }
1002
+ resolveItem();
1003
+ })
1004
+ );
706
1005
 
707
- if (counter === mapLength) {
708
- resolve(output);
709
- }
710
- });
1006
+ await Promise.all(promises);
1007
+ clearTimeout(timeout);
1008
+ if (!isResolved) {
1009
+ isResolved = true;
1010
+ resolve(output);
1011
+ }
711
1012
  });
712
1013
  });
713
1014
  }
@@ -718,7 +1019,36 @@ class HoloSphere {
718
1019
  * @returns {Promise<void>}
719
1020
  */
720
1021
  async deleteGlobal(tableName, key) {
721
- 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
+ });
722
1052
  }
723
1053
 
724
1054
  /**
@@ -727,16 +1057,65 @@ class HoloSphere {
727
1057
  * @returns {Promise<void>}
728
1058
  */
729
1059
  async deleteAllGlobal(tableName) {
730
- // return new Promise((resolve) => {
731
- this.gun.get(this.appname).get(tableName).map().once( (data, key)=> {
732
- this.gun.get(this.appname).get(tableName).get(key).put(null)
733
- })
734
- this.gun.get(this.appname).get(tableName).put(null, ack => {
735
- console.log('deleteAllGlobal: ack: ', ack)
736
- })
737
- // resolve();
738
- //});
739
- // });
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
+ });
740
1119
  }
741
1120
 
742
1121
  // ================================ COMPUTE FUNCTIONS ================================
@@ -745,44 +1124,107 @@ class HoloSphere {
745
1124
  * @param {string} holon - The holon identifier.
746
1125
  * @param {string} lens - The lens to compute.
747
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
748
1130
  */
749
- async compute(holon, lens, operation) {
1131
+ async compute(holon, lens, operation, depth = 0, maxDepth = 15) {
1132
+ // Validate required parameters
1133
+ if (!holon || !lens || !operation) {
1134
+ throw new Error('compute: Missing required parameters');
1135
+ }
750
1136
 
751
- let res = h3.getResolution(holon);
752
- if (res < 1 || res > 15) return;
753
- console.log(res)
754
- let parent = h3.cellToParent(holon, res - 1);
755
- let siblings = h3.cellToChildren(parent, res);
756
- console.log(holon, parent, siblings, res)
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
+ }
757
1144
 
758
- let content = [];
759
- let promises = [];
1145
+ if (res < 1 || res > 15) {
1146
+ throw new Error('compute: Invalid holon resolution (must be between 1 and 15)');
1147
+ }
760
1148
 
761
- for (let i = 0; i < siblings.length; i++) {
762
- promises.push(new Promise((resolve) => {
763
- let timeout = setTimeout(() => {
764
- console.log(`Timeout for sibling ${i}`);
765
- resolve(); // Resolve the promise to prevent it from hanging
766
- }, 1000); // Timeout of 5 seconds
767
-
768
- this.gun.get(this.appname).get(siblings[i]).get(lens).map().once((data, key) => {
769
- clearTimeout(timeout); // Clear the timeout if data is received
770
- if (data) {
771
- content.push(data.content);
772
- }
773
- resolve(); // Resolve after processing data
774
- });
775
- }));
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")');
776
1165
  }
777
1166
 
1167
+ const parent = h3.cellToParent(holon, res - 1);
1168
+ const siblings = h3.cellToChildren(parent, res);
1169
+
1170
+ const content = [];
1171
+ const promises = siblings.map(sibling =>
1172
+ new Promise((resolve) => {
1173
+ const timeout = setTimeout(() => {
1174
+ console.warn(`Timeout for sibling ${sibling}`);
1175
+ resolve();
1176
+ }, 10000);
1177
+
1178
+ this.gun.get(this.appname)
1179
+ .get(sibling)
1180
+ .get(lens)
1181
+ .map()
1182
+ .once((data) => {
1183
+ clearTimeout(timeout);
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);
1197
+ }
1198
+ resolve();
1199
+ });
1200
+ })
1201
+ );
1202
+
778
1203
  await Promise.all(promises);
779
- console.log('Content:', content);
780
- let computed = await this.summarize(content.join('\n'))
781
- console.log('Computed:', computed)
782
- let node = await this.gun.get(this.appname).get(parent + '_summary').put({ id: parent + '_summary', content: computed })
783
1204
 
784
- this.put(parent, lens, node);
785
- this.compute(parent, lens, operation)
1205
+ if (content.length > 0) {
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
+ });
1215
+
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
1223
+ }
1224
+ }
1225
+
1226
+ // Return successfully even if no content was found or processed
1227
+ return;
786
1228
  }
787
1229
 
788
1230
  /**
@@ -791,14 +1233,51 @@ class HoloSphere {
791
1233
  * @param {string} lens - The lens to clear.
792
1234
  */
793
1235
  async clearlens(holon, lens) {
794
- let entities = {};
1236
+ if (!holon || !lens) {
1237
+ throw new Error('clearlens: Missing required parameters');
1238
+ }
1239
+
1240
+ return new Promise((resolve, reject) => {
1241
+ try {
1242
+ const deletions = new Set();
1243
+ const timeout = setTimeout(() => {
1244
+ if (deletions.size === 0) {
1245
+ resolve(); // No data to delete
1246
+ }
1247
+ }, 1000);
795
1248
 
796
- // Get list out of Gun
797
- this.gun.get(this.appname).get(holon).get(lens).map().once((data, key) => {
798
- //entities = data;
799
- //const id = Object.keys(entities)[0] // since this would be in object form, you can manipulate it as you would like.
800
- this.gun.get(this.appname).get(holon).get(lens).put({ [key]: null })
801
- })
1249
+ this.gun.get(this.appname)
1250
+ .get(holon)
1251
+ .get(lens)
1252
+ .map()
1253
+ .once((data, key) => {
1254
+ if (data) {
1255
+ const deletion = new Promise((resolveDelete) => {
1256
+ this.gun.get(this.appname)
1257
+ .get(holon)
1258
+ .get(lens)
1259
+ .get(key)
1260
+ .put(null, ack => {
1261
+ if (ack.err) {
1262
+ console.error(`Failed to delete ${key}:`, ack.err);
1263
+ }
1264
+ resolveDelete();
1265
+ });
1266
+ });
1267
+ deletions.add(deletion);
1268
+ deletion.finally(() => {
1269
+ deletions.delete(deletion);
1270
+ if (deletions.size === 0) {
1271
+ clearTimeout(timeout);
1272
+ resolve();
1273
+ }
1274
+ });
1275
+ }
1276
+ });
1277
+ } catch (error) {
1278
+ reject(error);
1279
+ }
1280
+ });
802
1281
  }
803
1282
 
804
1283
 
@@ -811,32 +1290,29 @@ class HoloSphere {
811
1290
  if (!this.openai) {
812
1291
  return 'OpenAI not initialized, please specify the API key in the constructor.'
813
1292
  }
814
- //const run = await this.openai.beta.threads.runs.retrieve(thread.id,run.id)
815
- const assistant = await this.openai.beta.assistants.retrieve("asst_qhk79F8wV9BDNuwfOI80TqzC")
816
- const thread = await this.openai.beta.threads.create()
817
- const message = await this.openai.beta.threads.messages.create(thread.id, {
818
- role: "user",
819
- content: history
820
- })
821
- const run = await this.openai.beta.threads.runs.create(thread.id, {
822
- assistant_id: assistant.id //,
823
- //instructions: "What is the meaning of life?",
824
- });
825
1293
 
826
- let runStatus = await this.openai.beta.threads.runs.retrieve(
827
- thread.id,
828
- run.id
829
- );
830
- // Polling mechanism to see if runStatus is completed
831
- // This should be made more robust.
832
- while (runStatus.status !== "completed") {
833
- await new Promise((resolve) => setTimeout(resolve, 2000));
834
- 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');
835
1315
  }
836
- // Get the latest messages from the thread
837
- const messages = await this.openai.beta.threads.messages.list(thread.id)
838
- const summary = messages.data[0].content[0].text.value.replace(/\`\`\`json\n/, '').replace(/\`\`\`/, '').trim()
839
- return summary
840
1316
  }
841
1317
 
842
1318
  /**
@@ -849,12 +1325,12 @@ class HoloSphere {
849
1325
  async upcast(holon, lens, content) {
850
1326
  let res = h3.getResolution(holon)
851
1327
  if (res == 0) {
852
- await this.putNode(holon, lens, content)
1328
+ await this.put(holon, lens, content)
853
1329
  return content
854
1330
  }
855
1331
  else {
856
1332
  console.log('Upcasting ', holon, lens, content, res)
857
- await this.putNode(holon, lens, content)
1333
+ await this.put(holon, lens, content)
858
1334
  let parent = h3.cellToParent(holon, res - 1)
859
1335
  return this.upcast(parent, lens, content)
860
1336
  }
@@ -862,7 +1338,7 @@ class HoloSphere {
862
1338
 
863
1339
 
864
1340
  /**
865
- * Updates the parent holonagon with a new report.
1341
+ * Updates the parent holon with a new report.
866
1342
  * @param {string} id - The child holon identifier.
867
1343
  * @param {string} report - The report to update.
868
1344
  * @returns {Promise<object>} - The updated parent information.
@@ -930,10 +1406,442 @@ class HoloSphere {
930
1406
  * @param {function} callback - The callback to execute on changes.
931
1407
  */
932
1408
  async subscribe(holon, lens, callback) {
933
- this.gun.get(this.appname).get(holon).get(lens).map().on(async (data, key) => {
934
- if (data)
935
- callback( await this.parse(data), key)
936
- })
1409
+ if (!holon || !lens || !callback) {
1410
+ throw new Error('subscribe: Missing required parameters');
1411
+ }
1412
+
1413
+ const ref = this.gun.get(this.appname)
1414
+ .get(holon)
1415
+ .get(lens);
1416
+
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
+ }
1429
+ };
1430
+
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
+ };
1443
+ }
1444
+
1445
+ // Add ID generation method
1446
+ generateId() {
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());
937
1845
  }
938
1846
  }
939
1847