holosphere 1.1.8 → 1.1.10

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,11 +1,24 @@
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'
5
4
  import Ajv2019 from 'ajv/dist/2019.js'
6
5
  import * as Federation from './federation.js';
7
6
 
8
- export { federateMessage, getFederatedMessages, updateFederatedMessages, removeNotify } from './federation.js';
7
+ /**
8
+ * HoloSphere is a cutting-edge holonic communication infrastructure designed to facilitate
9
+ * the creation, validation, and sharing of information.. By leveraging advanced technologies
10
+ * such as H3 for geospatial indexing, OpenAI for natural language processing, and Gun for decentralized
11
+ * data storage, HoloSphere provides a robust platform for developers to build innovative stygmergic applications.
12
+ *
13
+ * Key Features:
14
+ * - Geospatial indexing using H3
15
+ * - Natural language processing with OpenAI
16
+ * - Decentralized data storage with Gun
17
+ * - JSON schema validation with Ajv
18
+ * - Federation capabilities for distributed systems
19
+ *
20
+ *
21
+ */
9
22
 
10
23
  class HoloSphere {
11
24
  /**
@@ -15,8 +28,8 @@ class HoloSphere {
15
28
  * @param {string|null} openaikey - The OpenAI API key.
16
29
  * @param {Gun|null} gunInstance - The Gun instance to use.
17
30
  */
18
- constructor(appname, strict = false, openaikey = null, gunInstance = null) {
19
- console.log('HoloSphere v1.1.7');
31
+ constructor(appname, strict = false, openaikey = null) {
32
+ console.log('HoloSphere v1.1.10');
20
33
  this.appname = appname
21
34
  this.strict = strict;
22
35
  this.validator = new Ajv2019({
@@ -25,20 +38,13 @@ class HoloSphere {
25
38
  validateSchema: true // Always validate schemas
26
39
  });
27
40
 
28
- // Handle different ways of providing Gun instance or options
29
- if (gunInstance && gunInstance.opt) {
30
- // If an object with 'opt' property is passed, create a new Gun instance with those options
31
- this.gun = Gun(gunInstance.opt);
32
- } else {
33
- // Use provided Gun instance or create new one with default options
34
- this.gun = gunInstance || Gun({
35
- peers: ['https://gun.holons.io/gun'],
36
- axe: false,
37
- });
38
- }
39
-
40
- // Initialize SEA
41
- this.sea = SEA;
41
+
42
+ // Use provided Gun instance or create new one with default options
43
+ this.gun = Gun({
44
+ peers: ['https://gun.holons.io/gun'],
45
+ axe: false,
46
+ });
47
+
42
48
 
43
49
  if (openaikey != null) {
44
50
  this.openai = new OpenAI({
@@ -48,6 +54,9 @@ class HoloSphere {
48
54
 
49
55
  // Initialize subscriptions
50
56
  this.subscriptions = {};
57
+
58
+ // Initialize schema cache
59
+ this.schemaCache = new Map();
51
60
  }
52
61
 
53
62
  // ================================ SCHEMA FUNCTIONS ================================
@@ -109,6 +118,12 @@ class HoloSphere {
109
118
  schema: schema,
110
119
  timestamp: Date.now()
111
120
  });
121
+
122
+ // Update the cache with the new schema
123
+ this.schemaCache.set(lens, {
124
+ schema,
125
+ timestamp: Date.now()
126
+ });
112
127
 
113
128
  return true;
114
129
  }
@@ -116,21 +131,61 @@ class HoloSphere {
116
131
  /**
117
132
  * Retrieves the JSON schema for a specific lens.
118
133
  * @param {string} lens - The lens identifier.
134
+ * @param {object} [options] - Additional options
135
+ * @param {boolean} [options.useCache=true] - Whether to use the schema cache
136
+ * @param {number} [options.maxCacheAge=3600000] - Maximum cache age in milliseconds (default: 1 hour)
119
137
  * @returns {Promise<object|null>} - The retrieved schema or null if not found.
120
138
  */
121
- async getSchema(lens) {
139
+ async getSchema(lens, options = {}) {
122
140
  if (!lens) {
123
141
  throw new Error('getSchema: Missing lens parameter');
124
142
  }
125
-
143
+
144
+ const { useCache = true, maxCacheAge = 3600000 } = options;
145
+
146
+ // Check cache first if enabled
147
+ if (useCache && this.schemaCache.has(lens)) {
148
+ const cached = this.schemaCache.get(lens);
149
+ const cacheAge = Date.now() - cached.timestamp;
150
+
151
+ // Use cache if it's fresh enough
152
+ if (cacheAge < maxCacheAge) {
153
+ return cached.schema;
154
+ }
155
+ }
156
+
157
+ // Cache miss or expired, fetch from storage
126
158
  const schemaData = await this.getGlobal('schemas', lens);
159
+
127
160
  if (!schemaData || !schemaData.schema) {
128
161
  return null;
129
162
  }
130
-
163
+
164
+ // Update cache with fetched schema
165
+ this.schemaCache.set(lens, {
166
+ schema: schemaData.schema,
167
+ timestamp: Date.now()
168
+ });
169
+
131
170
  return schemaData.schema;
132
171
  }
133
172
 
173
+ /**
174
+ * Clears the schema cache or a specific schema from the cache.
175
+ * @param {string} [lens] - Optional lens to clear from cache. If not provided, clears entire cache.
176
+ * @returns {boolean} - Returns true if successful
177
+ */
178
+ clearSchemaCache(lens = null) {
179
+ if (lens) {
180
+ // Clear specific schema
181
+ return this.schemaCache.delete(lens);
182
+ } else {
183
+ // Clear entire cache
184
+ this.schemaCache.clear();
185
+ return true;
186
+ }
187
+ }
188
+
134
189
  // ================================ CONTENT FUNCTIONS ================================
135
190
 
136
191
  /**
@@ -154,8 +209,11 @@ class HoloSphere {
154
209
  data.id = this.generateId();
155
210
  }
156
211
 
157
- // Get and validate schema only in strict mode
158
- if (this.strict) {
212
+ // Check if this is a reference we're storing
213
+ const isRef = this.isReference(data);
214
+
215
+ // Get and validate schema only in strict mode for non-references
216
+ if (this.strict && !isRef) {
159
217
  const schema = await this.getSchema(lens);
160
218
  if (!schema) {
161
219
  throw new Error('Schema required in strict mode');
@@ -170,72 +228,15 @@ class HoloSphere {
170
228
  }
171
229
 
172
230
  try {
173
- const user = this.gun.user();
174
-
231
+ let user = null;
175
232
  if (password) {
176
- try {
177
- await new Promise((resolve, reject) => {
178
- user.auth(this.userName(holon), password, (ack) => {
179
- if (ack.err) reject(new Error(ack.err));
180
- else resolve();
181
- });
233
+ user = this.gun.user();
234
+ await new Promise((resolve, reject) => {
235
+ user.auth(this.userName(holon), password, (ack) => {
236
+ if (ack.err) reject(new Error(ack.err));
237
+ else resolve();
182
238
  });
183
- } catch (loginError) {
184
- // If authentication fails, try to create user and then authenticate
185
- try {
186
- await new Promise((resolve, reject) => {
187
- user.create(this.userName(holon), password, (ack) => {
188
- if (ack.err) {
189
- // Don't reject if the user is already being created or already exists
190
- if (ack.err.includes('already being created') ||
191
- ack.err.includes('already created')) {
192
- console.warn(`User creation note: ${ack.err}, continuing...`);
193
- // Try to authenticate again
194
- user.auth(this.userName(holon), password, (authAck) => {
195
- if (authAck.err) {
196
- if (authAck.err.includes('already being created') ||
197
- authAck.err.includes('already created')) {
198
- console.warn(`Auth note: ${authAck.err}, continuing...`);
199
- resolve(); // Continue anyway
200
- } else {
201
- reject(new Error(authAck.err));
202
- }
203
- } else {
204
- resolve();
205
- }
206
- });
207
- } else {
208
- reject(new Error(ack.err));
209
- }
210
- } else {
211
- user.auth(this.userName(holon), password, (authAck) => {
212
- if (authAck.err) reject(new Error(authAck.err));
213
- else resolve();
214
- });
215
- }
216
- });
217
- });
218
- } catch (createError) {
219
- // Try one last authentication
220
- try {
221
- await new Promise((resolve, reject) => {
222
- setTimeout(() => {
223
- user.auth(this.userName(holon), password, (ack) => {
224
- if (ack.err) {
225
- // Continue even if auth fails at this point
226
- console.warn(`Final auth attempt note: ${ack.err}, continuing with limited functionality`);
227
- resolve();
228
- } else {
229
- resolve();
230
- }
231
- });
232
- }, 100); // Short delay before retry
233
- });
234
- } catch (finalAuthError) {
235
- console.warn('All authentication attempts failed, continuing with limited functionality');
236
- }
237
- }
238
- }
239
+ });
239
240
  }
240
241
 
241
242
  return new Promise((resolve, reject) => {
@@ -246,14 +247,17 @@ class HoloSphere {
246
247
  if (ack.err) {
247
248
  reject(new Error(ack.err));
248
249
  } else {
249
- this.notifySubscribers({
250
- holon,
251
- lens,
252
- ...data
253
- });
250
+ // Only notify subscribers for actual data, not references
251
+ if (!isRef) {
252
+ this.notifySubscribers({
253
+ holon,
254
+ lens,
255
+ ...data
256
+ });
257
+ }
254
258
 
255
- // Auto-propagate to federation by default
256
- const shouldPropagate = options.autoPropagate !== false;
259
+ // Auto-propagate to federation by default (if not a reference)
260
+ const shouldPropagate = options.autoPropagate !== false && !isRef;
257
261
  let propagationResult = null;
258
262
 
259
263
  if (shouldPropagate) {
@@ -282,18 +286,17 @@ class HoloSphere {
282
286
 
283
287
  resolve({
284
288
  success: true,
289
+ isReference: isRef,
285
290
  propagationResult
286
291
  });
287
292
  }
288
293
  };
289
294
 
290
- if (password) {
291
- // For private data, use the authenticated user's holon
292
- user.get('private').get(lens).get(data.id).put(payload, putCallback);
293
- } else {
294
- // For public data, use the regular path
295
- this.gun.get(this.appname).get(holon).get(lens).get(data.id).put(payload, putCallback);
296
- }
295
+ const dataPath = password ?
296
+ user.get('private').get(lens).get(data.id) :
297
+ this.gun.get(this.appname).get(holon).get(lens).get(data.id);
298
+
299
+ dataPath.put(payload, putCallback);
297
300
  } catch (error) {
298
301
  reject(error);
299
302
  }
@@ -332,30 +335,15 @@ class HoloSphere {
332
335
  }
333
336
 
334
337
  try {
335
- const user = this.gun.user();
336
-
338
+ let user = null;
337
339
  if (password) {
338
- try {
339
- await new Promise((resolve, reject) => {
340
- user.auth(this.userName(holon), password, (ack) => {
341
- if (ack.err) reject(new Error(ack.err));
342
- else resolve();
343
- });
344
- });
345
- } catch (loginError) {
346
- // If authentication fails, try to create user and then authenticate
347
- await new Promise((resolve, reject) => {
348
- user.create(this.userName(holon), password, (ack) => {
349
- if (ack.err) reject(new Error(ack.err));
350
- else {
351
- user.auth(this.userName(holon), password, (authAck) => {
352
- if (authAck.err) reject(new Error(authAck.err));
353
- else resolve();
354
- });
355
- }
356
- });
340
+ user = this.gun.user();
341
+ await new Promise((resolve, reject) => {
342
+ user.auth(this.userName(holon), password, (ack) => {
343
+ if (ack.err) reject(new Error(ack.err));
344
+ else resolve();
357
345
  });
358
- }
346
+ });
359
347
  }
360
348
 
361
349
  return new Promise((resolve) => {
@@ -374,84 +362,23 @@ class HoloSphere {
374
362
  }
375
363
 
376
364
  // Check if this is a reference that needs to be resolved
377
- if (resolveReferences !== false && parsed) {
378
- // Check if this is a simple reference (id + soul)
379
- if (parsed.soul) {
380
- console.log(`Resolving simple reference with soul: ${parsed.soul}`);
381
- try {
382
- // For direct soul resolution, we need to parse the soul to get the right path
383
- const soulParts = parsed.soul.split('/');
384
- if (soulParts.length >= 4) { // Expected format: appname/holon/lens/key
385
- const originHolon = soulParts[1];
386
- const originLens = soulParts[2];
387
- const originKey = soulParts[3];
388
-
389
- console.log(`Extracting from soul - holon: ${originHolon}, lens: ${originLens}, key: ${originKey}`);
390
-
391
- // Get original data using the extracted path components
392
- const originalData = await this.get(
393
- originHolon,
394
- originLens,
395
- originKey,
396
- null,
397
- { resolveReferences: false } // Prevent infinite recursion
398
- );
399
-
400
- if (originalData) {
401
- console.log(`Original data found through soul path resolution:`, originalData);
402
- resolve({
403
- ...originalData,
404
- _federation: {
405
- isReference: true,
406
- resolved: true,
407
- soul: parsed.soul,
408
- timestamp: Date.now()
409
- }
410
- });
411
- return;
412
- } else {
413
- console.warn(`Could not resolve reference: original data not found at extracted path`);
414
- }
415
- } else {
416
- console.warn(`Soul doesn't match expected format: ${parsed.soul}`);
417
- }
418
- } catch (error) {
419
- console.warn(`Error resolving reference by soul: ${error.message}`);
420
- }
421
- }
422
- // Legacy federation reference
423
- else if (parsed._federation && parsed._federation.isReference) {
424
- console.log(`Resolving legacy federation reference from ${parsed._federation.origin}`);
425
- try {
426
- const reference = parsed._federation;
427
- const originalData = await this.get(
428
- reference.origin,
429
- reference.lens,
430
- key,
431
- null,
432
- { resolveReferences: false } // Prevent infinite recursion
433
- );
434
-
435
- if (originalData) {
436
- return {
437
- ...originalData,
438
- _federation: {
439
- ...reference,
440
- resolved: true,
441
- timestamp: Date.now()
442
- }
443
- };
444
- } else {
445
- console.warn(`Could not resolve legacy reference: original data not found`);
446
- return parsed; // Return the reference if we can't resolve it
447
- }
448
- } catch (error) {
449
- console.warn(`Error resolving legacy reference: ${error.message}`);
450
- return parsed;
451
- }
365
+ if (resolveReferences && this.isReference(parsed)) {
366
+ const resolved = await this.resolveReference(parsed, {
367
+ followReferences: true // Always follow nested references when resolving
368
+ });
369
+
370
+ if (schema && resolved._federation) {
371
+ // Skip schema validation for resolved references
372
+ resolve(resolved);
373
+ return;
374
+ } else if (resolved !== parsed) {
375
+ // Reference was resolved successfully
376
+ resolve(resolved);
377
+ return;
452
378
  }
453
379
  }
454
380
 
381
+ // Perform schema validation if needed
455
382
  if (schema) {
456
383
  const valid = this.validator.validate(schema, parsed);
457
384
  if (!valid) {
@@ -470,13 +397,11 @@ class HoloSphere {
470
397
  }
471
398
  };
472
399
 
473
- if (password) {
474
- // For private data, use the authenticated user's holon
475
- user.get('private').get(lens).get(key).once(handleData);
476
- } else {
477
- // For public data, use the regular path
478
- this.gun.get(this.appname).get(holon).get(lens).get(key).once(handleData);
479
- }
400
+ const dataPath = password ?
401
+ user.get('private').get(lens).get(key) :
402
+ this.gun.get(this.appname).get(holon).get(lens).get(key);
403
+
404
+ dataPath.once(handleData);
480
405
  });
481
406
  } catch (error) {
482
407
  console.error('Error in get:', error);
@@ -496,7 +421,7 @@ class HoloSphere {
496
421
 
497
422
  console.log(`getNodeBySoul: Accessing soul ${soul}`);
498
423
 
499
- return new Promise((resolve) => {
424
+ return new Promise((resolve, reject) => {
500
425
  try {
501
426
  const ref = this.getNodeRef(soul);
502
427
  ref.once((data) => {
@@ -509,7 +434,7 @@ class HoloSphere {
509
434
  });
510
435
  } catch (error) {
511
436
  console.error(`getNodeBySoul error:`, error);
512
- resolve(null);
437
+ reject(error);
513
438
  }
514
439
  });
515
440
  }
@@ -544,7 +469,16 @@ class HoloSphere {
544
469
  }
545
470
 
546
471
  try {
547
- const user = this.gun.user();
472
+ let user = null;
473
+ if (password) {
474
+ user = this.gun.user();
475
+ await new Promise((resolve, reject) => {
476
+ user.auth(this.userName(holon), password, (ack) => {
477
+ if (ack.err) reject(new Error(ack.err));
478
+ else resolve();
479
+ });
480
+ });
481
+ }
548
482
 
549
483
  return new Promise((resolve) => {
550
484
  const output = new Map();
@@ -556,6 +490,26 @@ class HoloSphere {
556
490
  const parsed = await this.parse(data);
557
491
  if (!parsed || !parsed.id) return;
558
492
 
493
+ // Check if this is a reference that needs to be resolved
494
+ if (this.isReference(parsed)) {
495
+ const resolved = await this.resolveReference(parsed, {
496
+ followReferences: true // Always follow references
497
+ });
498
+
499
+ if (resolved !== parsed) {
500
+ // Reference was resolved successfully
501
+ if (schema) {
502
+ const valid = this.validator.validate(schema, resolved);
503
+ if (valid || !this.strict) {
504
+ output.set(resolved.id, resolved);
505
+ }
506
+ } else {
507
+ output.set(resolved.id, resolved);
508
+ }
509
+ return;
510
+ }
511
+ }
512
+
559
513
  if (schema) {
560
514
  const valid = this.validator.validate(schema, parsed);
561
515
  if (valid || !this.strict) {
@@ -591,13 +545,11 @@ class HoloSphere {
591
545
  }
592
546
  };
593
547
 
594
- if (password) {
595
- // For private data, use the authenticated user's holon
596
- user.get('private').get(lens).once(handleData);
597
- } else {
598
- // For public data, use the regular path
599
- this.gun.get(this.appname).get(holon).get(lens).once(handleData);
600
- }
548
+ const dataPath = password ?
549
+ user.get('private').get(lens) :
550
+ this.gun.get(this.appname).get(holon).get(lens);
551
+
552
+ dataPath.once(handleData);
601
553
  });
602
554
  } catch (error) {
603
555
  console.error('Error in getAll:', error);
@@ -677,30 +629,29 @@ class HoloSphere {
677
629
  }
678
630
 
679
631
  try {
680
- // Get the appropriate holon
681
- const user = this.gun.user();
632
+ let user = null;
633
+ if (password) {
634
+ user = this.gun.user();
635
+ await new Promise((resolve, reject) => {
636
+ user.auth(this.userName(holon), password, (ack) => {
637
+ if (ack.err) reject(new Error(ack.err));
638
+ else resolve();
639
+ });
640
+ });
641
+ }
682
642
 
683
- // Delete data from holon
684
643
  return new Promise((resolve, reject) => {
685
- if (password) {
686
- // For private data, use the authenticated user's holon
687
- user.get('private').get(lens).get(key).put(null, ack => {
688
- if (ack.err) {
689
- reject(new Error(ack.err));
690
- } else {
691
- resolve(true);
692
- }
693
- });
694
- } else {
695
- // For public data, use the regular path
696
- this.gun.get(this.appname).get(holon).get(lens).get(key).put(null, ack => {
697
- if (ack.err) {
698
- reject(new Error(ack.err));
699
- } else {
700
- resolve(true);
701
- }
702
- });
703
- }
644
+ const dataPath = password ?
645
+ user.get('private').get(lens).get(key) :
646
+ this.gun.get(this.appname).get(holon).get(lens).get(key);
647
+
648
+ dataPath.put(null, ack => {
649
+ if (ack.err) {
650
+ reject(new Error(ack.err));
651
+ } else {
652
+ resolve(true);
653
+ }
654
+ });
704
655
  });
705
656
  } catch (error) {
706
657
  console.error('Error in delete:', error);
@@ -722,8 +673,16 @@ class HoloSphere {
722
673
  }
723
674
 
724
675
  try {
725
- // Get the appropriate holon
726
- const user = this.gun.user();
676
+ let user = null;
677
+ if (password) {
678
+ user = this.gun.user();
679
+ await new Promise((resolve, reject) => {
680
+ user.auth(this.userName(holon), password, (ack) => {
681
+ if (ack.err) reject(new Error(ack.err));
682
+ else resolve();
683
+ });
684
+ });
685
+ }
727
686
 
728
687
  return new Promise((resolve) => {
729
688
  let deletionPromises = [];
@@ -820,18 +779,22 @@ class HoloSphere {
820
779
  throw new Error('getNode: Missing required parameters');
821
780
  }
822
781
 
823
- return new Promise((resolve) => {
824
- this.gun.get(this.appname)
825
- .get(holon)
826
- .get(lens)
827
- .get(key)
828
- .once((data) => {
829
- if (!data) {
830
- resolve(null);
831
- return;
832
- }
833
- resolve(data); // Return the data directly
834
- });
782
+ return new Promise((resolve, reject) => {
783
+ try {
784
+ this.gun.get(this.appname)
785
+ .get(holon)
786
+ .get(lens)
787
+ .get(key)
788
+ .once((data) => {
789
+ if (!data) {
790
+ resolve(null);
791
+ return;
792
+ }
793
+ resolve(data); // Return the data directly
794
+ });
795
+ } catch (error) {
796
+ reject(error);
797
+ }
835
798
  });
836
799
  }
837
800
 
@@ -898,96 +861,29 @@ class HoloSphere {
898
861
  throw new Error('Table name and data are required');
899
862
  }
900
863
 
901
- const user = this.gun.user();
902
-
864
+ let user = null;
903
865
  if (password) {
904
- try {
905
- // Try to authenticate first
906
- await new Promise((resolve, reject) => {
907
- user.auth(this.userName(tableName), password, (ack) => {
908
- if (ack.err) {
909
- // Handle wrong username/password gracefully
910
- if (ack.err.includes('Wrong user or password') ||
911
- ack.err.includes('No user')) {
912
- console.warn(`Authentication failed for ${tableName}: ${ack.err}`);
913
- // Will try to create user next
914
- reject(new Error(ack.err));
915
- } else {
916
- reject(new Error(ack.err));
917
- }
918
- } else {
919
- resolve();
920
- }
921
- });
866
+ user = this.gun.user();
867
+ await new Promise((resolve, reject) => {
868
+ user.auth(this.userName(tableName), password, (ack) => {
869
+ if (ack.err) reject(new Error(ack.err));
870
+ else resolve();
922
871
  });
923
- } catch (authError) {
924
- // If authentication fails, try to create user
925
- try {
926
- await new Promise((resolve, reject) => {
927
- user.create(this.userName(tableName), password, (ack) => {
928
- // Handle "User already created!" error gracefully
929
- if (ack.err && !ack.err.includes('already created')) {
930
- reject(new Error(ack.err));
931
- } else {
932
- // Whether user was created or already existed, try to authenticate
933
- user.auth(this.userName(tableName), password, (authAck) => {
934
- if (authAck.err) {
935
- console.warn(`Authentication failed after creation for ${tableName}: ${authAck.err}`);
936
- reject(new Error(authAck.err));
937
- } else {
938
- resolve();
939
- }
940
- });
941
- }
942
- });
943
- });
944
- } catch (createError) {
945
- // If both auth and create fail, try one last auth attempt
946
- await new Promise((resolve, reject) => {
947
- user.auth(this.userName(tableName), password, (ack) => {
948
- if (ack.err) {
949
- console.warn(`Final authentication attempt failed for ${tableName}: ${ack.err}`);
950
- // Continue with operation even if auth fails
951
- resolve();
952
- } else {
953
- resolve();
954
- }
955
- });
956
- });
957
- }
958
- }
872
+ });
959
873
  }
960
874
 
961
875
  return new Promise((resolve, reject) => {
962
- const payload = JSON.stringify(data);
963
-
964
- if (password) {
965
- // For private data, use the authenticated user's holon
966
- const path = user.get('private').get(tableName);
876
+ try {
877
+ const payload = JSON.stringify(data);
967
878
 
968
- if (data.id) {
969
- path.get(data.id).put(payload, ack => {
970
- if (ack.err) {
971
- reject(new Error(ack.err));
972
- } else {
973
- resolve();
974
- }
975
- });
976
- } else {
977
- path.put(payload, ack => {
978
- if (ack.err) {
979
- reject(new Error(ack.err));
980
- } else {
981
- resolve();
982
- }
983
- });
984
- }
985
- } else {
986
- // For public data, use the regular path
987
- const path = this.gun.get(this.appname).get(tableName);
879
+ const dataPath = password ?
880
+ user.get('private').get(tableName) :
881
+ this.gun.get(this.appname).get(tableName);
988
882
 
989
883
  if (data.id) {
990
- path.get(data.id).put(payload, ack => {
884
+ // Store at the specific key path
885
+ dataPath.get(data.id).put(payload, ack => {
886
+
991
887
  if (ack.err) {
992
888
  reject(new Error(ack.err));
993
889
  } else {
@@ -995,7 +891,7 @@ class HoloSphere {
995
891
  }
996
892
  });
997
893
  } else {
998
- path.put(payload, ack => {
894
+ dataPath.put(payload, ack => {
999
895
  if (ack.err) {
1000
896
  reject(new Error(ack.err));
1001
897
  } else {
@@ -1003,6 +899,8 @@ class HoloSphere {
1003
899
  }
1004
900
  });
1005
901
  }
902
+ } catch (error) {
903
+ reject(error);
1006
904
  }
1007
905
  });
1008
906
  } catch (error) {
@@ -1019,72 +917,59 @@ class HoloSphere {
1019
917
  * @returns {Promise<object|null>} - The parsed data for the key or null if not found.
1020
918
  */
1021
919
  async getGlobal(tableName, key, password = null) {
1022
- try {
1023
- const user = this.gun.user();
1024
-
920
+ try {
921
+ let user = null;
1025
922
  if (password) {
1026
- try {
1027
- await new Promise((resolve, reject) => {
1028
- user.auth(this.userName(tableName), password, (ack) => {
1029
- if (ack.err) {
1030
- // Handle wrong username/password gracefully
1031
- if (ack.err.includes('Wrong user or password') ||
1032
- ack.err.includes('No user')) {
1033
- console.warn(`Authentication failed for ${tableName}: ${ack.err}`);
1034
- // Will try to create user next
1035
- reject(new Error(ack.err));
1036
- } else {
1037
- reject(new Error(ack.err));
1038
- }
1039
- } else {
1040
- resolve();
1041
- }
1042
- });
1043
- });
1044
- } catch (loginError) {
1045
- // If authentication fails, try to create user and then authenticate
1046
- await new Promise((resolve, reject) => {
1047
- user.create(this.userName(tableName), password, (ack) => {
1048
- // Handle "User already created!" error gracefully
1049
- if (ack.err && !ack.err.includes('already created')) {
1050
- reject(new Error(ack.err));
1051
- } else {
1052
- user.auth(this.userName(tableName), password, (authAck) => {
1053
- if (authAck.err) {
1054
- console.warn(`Authentication failed after creation for ${tableName}: ${authAck.err}`);
1055
- // Continue with operation even if auth fails
1056
- resolve();
1057
- } else {
1058
- resolve();
1059
- }
1060
- });
1061
- }
1062
- });
923
+ user = this.gun.user();
924
+ await new Promise((resolve, reject) => {
925
+ user.auth(this.userName(tableName), password, (ack) => {
926
+ if (ack.err) reject(new Error(ack.err));
927
+ else resolve();
1063
928
  });
1064
- }
929
+ });
1065
930
  }
1066
931
 
1067
932
  return new Promise((resolve) => {
1068
- const handleData = (data) => {
933
+ const handleData = async (data) => {
1069
934
  if (!data) {
1070
935
  resolve(null);
1071
936
  return;
1072
937
  }
938
+
1073
939
  try {
1074
- const parsed = this.parse(data);
940
+ // The data should be a stringified JSON from putGlobal
941
+ const parsed = await this.parse(data);
942
+
943
+ if (!parsed) {
944
+ resolve(null);
945
+ return;
946
+ }
947
+
948
+ // Check if this is a reference that needs to be resolved
949
+ if (this.isReference(parsed)) {
950
+ const resolved = await this.resolveReference(parsed, {
951
+ followReferences: true // Always follow references
952
+ });
953
+
954
+ if (resolved !== parsed) {
955
+ // Reference was resolved successfully
956
+ resolve(resolved);
957
+ return;
958
+ }
959
+ }
960
+
1075
961
  resolve(parsed);
1076
962
  } catch (e) {
963
+ console.error('Error parsing data in getGlobal:', e);
1077
964
  resolve(null);
1078
965
  }
1079
966
  };
1080
967
 
1081
- if (password) {
1082
- // For private data, use the authenticated user's holon
1083
- user.get('private').get(tableName).get(key).once(handleData);
1084
- } else {
1085
- // For public data, use the regular path
1086
- this.gun.get(this.appname).get(tableName).get(key).once(handleData);
1087
- }
968
+ const dataPath = password ?
969
+ user.get('private').get(tableName) :
970
+ this.gun.get(this.appname).get(tableName);
971
+
972
+ dataPath.get(key).once(handleData);
1088
973
  });
1089
974
  } catch (error) {
1090
975
  console.error('Error in getGlobal:', error);
@@ -1104,8 +989,16 @@ class HoloSphere {
1104
989
  }
1105
990
 
1106
991
  try {
1107
- // Get the appropriate holon
1108
- const user = this.gun.user();
992
+ let user = null;
993
+ if (password) {
994
+ user = this.gun.user();
995
+ await new Promise((resolve, reject) => {
996
+ user.auth(this.userName(tableName), password, (ack) => {
997
+ if (ack.err) reject(new Error(ack.err));
998
+ else resolve();
999
+ });
1000
+ });
1001
+ }
1109
1002
 
1110
1003
  return new Promise((resolve) => {
1111
1004
  let output = [];
@@ -1139,7 +1032,23 @@ class HoloSphere {
1139
1032
  if (itemData) {
1140
1033
  try {
1141
1034
  const parsed = await this.parse(itemData);
1142
- if (parsed) output.push(parsed);
1035
+ if (parsed) {
1036
+ // Check if this is a reference that needs to be resolved
1037
+ if (this.isReference(parsed)) {
1038
+ const resolved = await this.resolveReference(parsed, {
1039
+ followReferences: true // Always follow references
1040
+ });
1041
+
1042
+ if (resolved !== parsed) {
1043
+ // Reference was resolved successfully
1044
+ output.push(resolved);
1045
+ } else {
1046
+ output.push(parsed);
1047
+ }
1048
+ } else {
1049
+ output.push(parsed);
1050
+ }
1051
+ }
1143
1052
  } catch (error) {
1144
1053
  console.error('Error parsing data:', error);
1145
1054
  }
@@ -1156,13 +1065,11 @@ class HoloSphere {
1156
1065
  }
1157
1066
  };
1158
1067
 
1159
- if (password) {
1160
- // For private data, use the authenticated user's holon
1161
- user.get('private').get(tableName).once(handleData);
1162
- } else {
1163
- // For public data, use the regular path
1164
- this.gun.get(this.appname).get(tableName).once(handleData);
1165
- }
1068
+ const dataPath = password ?
1069
+ user.get('private').get(tableName) :
1070
+ this.gun.get(this.appname).get(tableName);
1071
+
1072
+ dataPath.once(handleData);
1166
1073
  });
1167
1074
  } catch (error) {
1168
1075
  console.error('Error in getAllGlobal:', error);
@@ -1183,29 +1090,45 @@ class HoloSphere {
1183
1090
  }
1184
1091
 
1185
1092
  try {
1186
- // Get the appropriate holon
1187
- const user = this.gun.user();
1093
+ console.log('deleteGlobal - Starting deletion:', { tableName, key, hasPassword: !!password });
1094
+
1095
+ let user = null;
1096
+ if (password) {
1097
+ user = this.gun.user();
1098
+ await new Promise((resolve, reject) => {
1099
+ user.auth(this.userName(tableName), password, (ack) => {
1100
+ if (ack.err) reject(new Error(ack.err));
1101
+ else resolve();
1102
+ });
1103
+ });
1104
+ }
1188
1105
 
1189
1106
  return new Promise((resolve, reject) => {
1190
- if (password) {
1191
- // For private data, use the authenticated user's holon
1192
- user.get('private').get(tableName).get(key).put(null, ack => {
1193
- if (ack.err) {
1194
- reject(new Error(ack.err));
1195
- } else {
1196
- resolve(true);
1197
- }
1198
- });
1199
- } else {
1200
- // For public data, use the regular path
1201
- this.gun.get(this.appname).get(tableName).get(key).put(null, ack => {
1107
+ const dataPath = password ?
1108
+ user.get('private').get(tableName) :
1109
+ this.gun.get(this.appname).get(tableName);
1110
+
1111
+ console.log('deleteGlobal - Constructed base path:', dataPath._.back);
1112
+
1113
+ // First verify the data exists
1114
+ dataPath.get(key).once((data) => {
1115
+ console.log('deleteGlobal - Data before deletion:', data);
1116
+
1117
+ // Now perform the deletion
1118
+ dataPath.get(key).put(null, ack => {
1119
+ console.log('deleteGlobal - Deletion acknowledgment:', ack);
1202
1120
  if (ack.err) {
1121
+ console.error('deleteGlobal - Deletion error:', ack.err);
1203
1122
  reject(new Error(ack.err));
1204
1123
  } else {
1205
- resolve(true);
1124
+ // Verify deletion
1125
+ dataPath.get(key).once((deletedData) => {
1126
+ console.log('deleteGlobal - Data after deletion:', deletedData);
1127
+ resolve(true);
1128
+ });
1206
1129
  }
1207
1130
  });
1208
- }
1131
+ });
1209
1132
  });
1210
1133
  } catch (error) {
1211
1134
  console.error('Error in deleteGlobal:', error);
@@ -1225,8 +1148,16 @@ class HoloSphere {
1225
1148
  }
1226
1149
 
1227
1150
  try {
1228
- // Get the appropriate holon
1229
- const user = this.gun.user();
1151
+ let user = null;
1152
+ if (password) {
1153
+ user = this.gun.user();
1154
+ await new Promise((resolve, reject) => {
1155
+ user.auth(this.userName(tableName), password, (ack) => {
1156
+ if (ack.err) reject(new Error(ack.err));
1157
+ else resolve();
1158
+ });
1159
+ });
1160
+ }
1230
1161
 
1231
1162
  return new Promise((resolve, reject) => {
1232
1163
  try {
@@ -1250,7 +1181,7 @@ class HoloSphere {
1250
1181
 
1251
1182
  const keys = Object.keys(data).filter(key => key !== '_');
1252
1183
  const promises = keys.map(key =>
1253
- new Promise((resolveDelete) => {
1184
+ new Promise((resolveDelete, rejectDelete) => {
1254
1185
  const deletePath = password ?
1255
1186
  user.get('private').get(tableName).get(key) :
1256
1187
  this.gun.get(this.appname).get(tableName).get(key);
@@ -1258,8 +1189,10 @@ class HoloSphere {
1258
1189
  deletePath.put(null, ack => {
1259
1190
  if (ack.err) {
1260
1191
  console.error(`Failed to delete ${key}:`, ack.err);
1192
+ rejectDelete(new Error(ack.err));
1193
+ } else {
1194
+ resolveDelete();
1261
1195
  }
1262
- resolveDelete();
1263
1196
  });
1264
1197
  })
1265
1198
  );
@@ -1284,6 +1217,159 @@ class HoloSphere {
1284
1217
  }
1285
1218
  }
1286
1219
 
1220
+ // ================================ REFERENCE FUNCTIONS ================================
1221
+
1222
+ /**
1223
+ * Creates a soul reference object for a data item
1224
+ * @param {string} holon - The holon where the original data is stored
1225
+ * @param {string} lens - The lens where the original data is stored
1226
+ * @param {object} data - The data to create a reference for
1227
+ * @returns {object} - A reference object with id and soul
1228
+ */
1229
+ createReference(holon, lens, data) {
1230
+ if (!holon || !lens || !data || !data.id) {
1231
+ throw new Error('createReference: Missing required parameters');
1232
+ }
1233
+
1234
+ const soul = `${this.appname}/${holon}/${lens}/${data.id}`;
1235
+ return {
1236
+ id: data.id,
1237
+ soul: soul
1238
+ };
1239
+ }
1240
+
1241
+ /**
1242
+ * Parses a soul path into its components
1243
+ * @param {string} soul - The soul path to parse
1244
+ * @returns {object|null} - The parsed components or null if invalid format
1245
+ */
1246
+ parseSoulPath(soul) {
1247
+ if (!soul || typeof soul !== 'string') {
1248
+ return null;
1249
+ }
1250
+
1251
+ const soulParts = soul.split('/');
1252
+ if (soulParts.length < 4) {
1253
+ return null;
1254
+ }
1255
+
1256
+ return {
1257
+ appname: soulParts[0],
1258
+ holon: soulParts[1],
1259
+ lens: soulParts[2],
1260
+ key: soulParts[3]
1261
+ };
1262
+ }
1263
+
1264
+ /**
1265
+ * Checks if an object is a reference
1266
+ * @param {object} data - The data to check
1267
+ * @returns {boolean} - True if the object is a reference
1268
+ */
1269
+ isReference(data) {
1270
+ if (!data || typeof data !== 'object') {
1271
+ return false;
1272
+ }
1273
+
1274
+ // Check for direct soul reference
1275
+ if (data.soul && typeof data.soul === 'string' && data.id) {
1276
+ return true;
1277
+ }
1278
+
1279
+ // Check for legacy federation reference
1280
+ if (data._federation && data._federation.isReference) {
1281
+ return true;
1282
+ }
1283
+
1284
+ return false;
1285
+ }
1286
+
1287
+ /**
1288
+ * Resolves a reference to its actual data
1289
+ * @param {object} reference - The reference to resolve
1290
+ * @param {object} [options] - Optional parameters
1291
+ * @param {boolean} [options.followReferences=true] - Whether to follow nested references
1292
+ * @returns {Promise<object|null>} - The resolved data or null if not found
1293
+ */
1294
+ async resolveReference(reference, options = {}) {
1295
+ if (!this.isReference(reference)) {
1296
+ return reference; // Not a reference, return as is
1297
+ }
1298
+
1299
+ const { followReferences = true } = options;
1300
+
1301
+ try {
1302
+ // Handle direct soul reference
1303
+ if (reference.soul) {
1304
+ const soulInfo = this.parseSoulPath(reference.soul);
1305
+ if (!soulInfo) {
1306
+ console.warn(`Invalid soul format: ${reference.soul}`);
1307
+ return reference;
1308
+ }
1309
+
1310
+ console.log(`Resolving reference with soul: ${reference.soul}`);
1311
+
1312
+ // Get original data using the extracted path components
1313
+ const originalData = await this.get(
1314
+ soulInfo.holon,
1315
+ soulInfo.lens,
1316
+ soulInfo.key,
1317
+ null,
1318
+ { resolveReferences: followReferences } // Control recursion
1319
+ );
1320
+
1321
+ if (originalData) {
1322
+ console.log(`Original data found through soul path resolution`);
1323
+ return {
1324
+ ...originalData,
1325
+ _federation: {
1326
+ isReference: true,
1327
+ resolved: true,
1328
+ soul: reference.soul,
1329
+ timestamp: Date.now()
1330
+ }
1331
+ };
1332
+ } else {
1333
+ console.warn(`Could not resolve reference: original data not found at extracted path`);
1334
+ return reference; // Return original reference if resolution fails
1335
+ }
1336
+ }
1337
+
1338
+ // Handle legacy federation reference
1339
+ else if (reference._federation && reference._federation.isReference) {
1340
+ const fedRef = reference._federation;
1341
+ console.log(`Resolving legacy federation reference from ${fedRef.origin}`);
1342
+
1343
+ const originalData = await this.get(
1344
+ fedRef.origin,
1345
+ fedRef.lens,
1346
+ reference.id || fedRef.key,
1347
+ null,
1348
+ { resolveReferences: followReferences }
1349
+ );
1350
+
1351
+ if (originalData) {
1352
+ return {
1353
+ ...originalData,
1354
+ _federation: {
1355
+ ...fedRef,
1356
+ resolved: true,
1357
+ timestamp: Date.now()
1358
+ }
1359
+ };
1360
+ } else {
1361
+ console.warn(`Could not resolve legacy reference: original data not found`);
1362
+ return reference;
1363
+ }
1364
+ }
1365
+
1366
+ return reference;
1367
+ } catch (error) {
1368
+ console.error(`Error resolving reference: ${error.message}`, error);
1369
+ return reference;
1370
+ }
1371
+ }
1372
+
1287
1373
  // ================================ COMPUTE FUNCTIONS ================================
1288
1374
  /**
1289
1375
  * Computes operations across multiple layers up the hierarchy
@@ -1491,8 +1577,7 @@ class HoloSphere {
1491
1577
  }
1492
1578
 
1493
1579
  /**
1494
- * Upcasts content to parent holonagons recursively using federation and soul references.
1495
- * This is the modern implementation that uses federation references instead of duplicating data.
1580
+ * Upcasts content to parent holonagons recursively using references.
1496
1581
  * @param {string} holon - The current holon identifier.
1497
1582
  * @param {string} lens - The lens under which to upcast.
1498
1583
  * @param {object} content - The content to upcast.
@@ -1513,18 +1598,10 @@ class HoloSphere {
1513
1598
  // Get the parent cell
1514
1599
  let parent = h3.cellToParent(holon, res - 1);
1515
1600
 
1516
- // Create federation relationship if it doesn't exist
1517
- await this.federate(holon, parent);
1518
-
1519
- // Create a soul reference to store in the parent
1520
- const soul = `${this.appname}/${holon}/${lens}/${content.id}`;
1521
- const reference = {
1522
- id: content.id,
1523
- soul: soul
1524
- };
1601
+ // Create a reference to store in the parent
1602
+ const reference = this.createReference(holon, lens, content);
1525
1603
 
1526
1604
  // Store the reference in the parent cell
1527
- // We use { autoPropagate: false } to prevent circular propagation
1528
1605
  await this.put(parent, lens, reference, null, {
1529
1606
  autoPropagate: false
1530
1607
  });
@@ -1607,8 +1684,12 @@ class HoloSphere {
1607
1684
  * @returns {Promise<object>} - Subscription object with unsubscribe method
1608
1685
  */
1609
1686
  async subscribe(holon, lens, callback) {
1610
- if (!holon || !lens || typeof callback !== 'function') {
1611
- throw new Error('subscribe: Missing required parameters');
1687
+ if (!holon || !lens) {
1688
+ throw new Error('subscribe: Missing holon or lens parameters:', holon, lens);
1689
+ }
1690
+
1691
+ if (!callback || typeof callback !== 'function') {
1692
+ throw new Error('subscribe: Callback must be a function');
1612
1693
  }
1613
1694
 
1614
1695
  const subscriptionId = this.generateId();
@@ -1616,10 +1697,35 @@ class HoloSphere {
1616
1697
  try {
1617
1698
  // Create the subscription
1618
1699
  const gunSubscription = this.gun.get(this.appname).get(holon).get(lens).map().on(async (data, key) => {
1700
+ // Check if subscription is still active before processing
1701
+ if (!this.subscriptions[subscriptionId]?.active) {
1702
+ return;
1703
+ }
1704
+
1619
1705
  if (data) {
1620
1706
  try {
1621
1707
  let parsed = await this.parse(data);
1622
- callback(parsed, key);
1708
+
1709
+ // Check if the parsed data is a reference that needs resolution
1710
+ if (parsed && this.isReference(parsed)) {
1711
+ const resolved = await this.resolveReference(parsed, {
1712
+ followReferences: true // Always follow references
1713
+ });
1714
+
1715
+ if (resolved !== parsed) {
1716
+ // Reference was resolved successfully
1717
+ // Check again if subscription is still active
1718
+ if (this.subscriptions[subscriptionId]?.active) {
1719
+ callback(resolved, key);
1720
+ }
1721
+ return;
1722
+ }
1723
+ }
1724
+
1725
+ // Check again if subscription is still active before final callback
1726
+ if (this.subscriptions[subscriptionId]?.active) {
1727
+ callback(parsed, key);
1728
+ }
1623
1729
  } catch (error) {
1624
1730
  console.error('Error in subscribe:', error);
1625
1731
  }
@@ -1632,6 +1738,7 @@ class HoloSphere {
1632
1738
  holon,
1633
1739
  lens,
1634
1740
  active: true,
1741
+ callback,
1635
1742
  gunSubscription
1636
1743
  };
1637
1744
 
@@ -1639,14 +1746,18 @@ class HoloSphere {
1639
1746
  return {
1640
1747
  unsubscribe: () => {
1641
1748
  try {
1642
- // Turn off the Gun subscription
1643
- this.gun.get(this.appname).get(holon).get(lens).map().off();
1644
-
1645
- // Mark as inactive and remove from subscriptions
1749
+ // Mark as inactive first to prevent any new callbacks
1646
1750
  if (this.subscriptions[subscriptionId]) {
1647
1751
  this.subscriptions[subscriptionId].active = false;
1648
- delete this.subscriptions[subscriptionId];
1649
1752
  }
1753
+
1754
+ // Turn off the Gun subscription using the stored reference
1755
+ if (this.subscriptions[subscriptionId]?.gunSubscription) {
1756
+ this.subscriptions[subscriptionId].gunSubscription.off();
1757
+ }
1758
+
1759
+ // Remove from subscriptions
1760
+ delete this.subscriptions[subscriptionId];
1650
1761
  } catch (error) {
1651
1762
  console.error('Error in unsubscribe:', error);
1652
1763
  }
@@ -1658,7 +1769,6 @@ class HoloSphere {
1658
1769
  }
1659
1770
  }
1660
1771
 
1661
-
1662
1772
  /**
1663
1773
  * Notifies subscribers about data changes
1664
1774
  * @param {object} data - The data to notify about
@@ -1834,11 +1944,10 @@ class HoloSphere {
1834
1944
  try {
1835
1945
  const subscription = this.subscriptions[id];
1836
1946
  if (subscription && subscription.active) {
1837
- // Turn off the Gun subscription
1838
- this.gun.get(this.appname)
1839
- .get(subscription.holon)
1840
- .get(subscription.lens)
1841
- .map().off();
1947
+ // Turn off the Gun subscription using the stored reference
1948
+ if (subscription.gunSubscription) {
1949
+ subscription.gunSubscription.off();
1950
+ }
1842
1951
 
1843
1952
  // Mark as inactive
1844
1953
  subscription.active = false;
@@ -1850,29 +1959,66 @@ class HoloSphere {
1850
1959
 
1851
1960
  // Clear subscriptions
1852
1961
  this.subscriptions = {};
1962
+
1963
+ // Clear schema cache
1964
+ this.clearSchemaCache();
1853
1965
 
1854
1966
  // Close Gun connections
1855
1967
  if (this.gun.back) {
1856
1968
  try {
1969
+ // Clean up mesh connections
1857
1970
  const mesh = this.gun.back('opt.mesh');
1858
- if (mesh && mesh.hear) {
1859
- try {
1860
- // Safely clear mesh.hear without modifying function properties
1861
- const hearKeys = Object.keys(mesh.hear);
1862
- for (const key of hearKeys) {
1863
- // Check if it's an array before trying to clear it
1864
- if (Array.isArray(mesh.hear[key])) {
1865
- mesh.hear[key] = [];
1971
+ if (mesh) {
1972
+ // Clean up mesh.hear
1973
+ if (mesh.hear) {
1974
+ try {
1975
+ // Safely clear mesh.hear without modifying function properties
1976
+ const hearKeys = Object.keys(mesh.hear);
1977
+ for (const key of hearKeys) {
1978
+ // Check if it's an array before trying to clear it
1979
+ if (Array.isArray(mesh.hear[key])) {
1980
+ mesh.hear[key] = [];
1981
+ }
1982
+ }
1983
+
1984
+ // Create a new empty object for mesh.hear
1985
+ // Only if mesh.hear is not a function
1986
+ if (typeof mesh.hear !== 'function') {
1987
+ mesh.hear = {};
1866
1988
  }
1989
+ } catch (meshError) {
1990
+ console.warn('Error cleaning up Gun mesh hear:', meshError);
1867
1991
  }
1868
-
1869
- // Create a new empty object for mesh.hear
1870
- // Only if mesh.hear is not a function
1871
- if (typeof mesh.hear !== 'function') {
1872
- mesh.hear = {};
1992
+ }
1993
+
1994
+ // Close any open sockets in the mesh
1995
+ if (mesh.way) {
1996
+ try {
1997
+ Object.values(mesh.way).forEach(connection => {
1998
+ if (connection && connection.wire && connection.wire.close) {
1999
+ connection.wire.close();
2000
+ }
2001
+ });
2002
+ } catch (sockError) {
2003
+ console.warn('Error closing mesh sockets:', sockError);
2004
+ }
2005
+ }
2006
+
2007
+ // Clear the peers list
2008
+ if (mesh.opt && mesh.opt.peers) {
2009
+ mesh.opt.peers = {};
2010
+ }
2011
+ }
2012
+
2013
+ // Attempt to clean up any TCP connections
2014
+ if (this.gun.back('opt.web')) {
2015
+ try {
2016
+ const server = this.gun.back('opt.web');
2017
+ if (server && server.close) {
2018
+ server.close();
1873
2019
  }
1874
- } catch (meshError) {
1875
- console.warn('Error cleaning up Gun mesh hear:', meshError);
2020
+ } catch (webError) {
2021
+ console.warn('Error closing web server:', webError);
1876
2022
  }
1877
2023
  }
1878
2024
  } catch (error) {