holosphere 1.1.9 → 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
@@ -29,7 +29,7 @@ class HoloSphere {
29
29
  * @param {Gun|null} gunInstance - The Gun instance to use.
30
30
  */
31
31
  constructor(appname, strict = false, openaikey = null) {
32
- console.log('HoloSphere v1.1.9');
32
+ console.log('HoloSphere v1.1.10');
33
33
  this.appname = appname
34
34
  this.strict = strict;
35
35
  this.validator = new Ajv2019({
@@ -54,6 +54,9 @@ class HoloSphere {
54
54
 
55
55
  // Initialize subscriptions
56
56
  this.subscriptions = {};
57
+
58
+ // Initialize schema cache
59
+ this.schemaCache = new Map();
57
60
  }
58
61
 
59
62
  // ================================ SCHEMA FUNCTIONS ================================
@@ -115,6 +118,12 @@ class HoloSphere {
115
118
  schema: schema,
116
119
  timestamp: Date.now()
117
120
  });
121
+
122
+ // Update the cache with the new schema
123
+ this.schemaCache.set(lens, {
124
+ schema,
125
+ timestamp: Date.now()
126
+ });
118
127
 
119
128
  return true;
120
129
  }
@@ -122,21 +131,61 @@ class HoloSphere {
122
131
  /**
123
132
  * Retrieves the JSON schema for a specific lens.
124
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)
125
137
  * @returns {Promise<object|null>} - The retrieved schema or null if not found.
126
138
  */
127
- async getSchema(lens) {
139
+ async getSchema(lens, options = {}) {
128
140
  if (!lens) {
129
141
  throw new Error('getSchema: Missing lens parameter');
130
142
  }
131
-
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
132
158
  const schemaData = await this.getGlobal('schemas', lens);
159
+
133
160
  if (!schemaData || !schemaData.schema) {
134
161
  return null;
135
162
  }
136
-
163
+
164
+ // Update cache with fetched schema
165
+ this.schemaCache.set(lens, {
166
+ schema: schemaData.schema,
167
+ timestamp: Date.now()
168
+ });
169
+
137
170
  return schemaData.schema;
138
171
  }
139
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
+
140
189
  // ================================ CONTENT FUNCTIONS ================================
141
190
 
142
191
  /**
@@ -160,8 +209,11 @@ class HoloSphere {
160
209
  data.id = this.generateId();
161
210
  }
162
211
 
163
- // Get and validate schema only in strict mode
164
- 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) {
165
217
  const schema = await this.getSchema(lens);
166
218
  if (!schema) {
167
219
  throw new Error('Schema required in strict mode');
@@ -176,72 +228,15 @@ class HoloSphere {
176
228
  }
177
229
 
178
230
  try {
179
- const user = this.gun.user();
180
-
231
+ let user = null;
181
232
  if (password) {
182
- try {
183
- await new Promise((resolve, reject) => {
184
- user.auth(this.userName(holon), password, (ack) => {
185
- if (ack.err) reject(new Error(ack.err));
186
- else resolve();
187
- });
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();
188
238
  });
189
- } catch (loginError) {
190
- // If authentication fails, try to create user and then authenticate
191
- try {
192
- await new Promise((resolve, reject) => {
193
- user.create(this.userName(holon), password, (ack) => {
194
- if (ack.err) {
195
- // Don't reject if the user is already being created or already exists
196
- if (ack.err.includes('already being created') ||
197
- ack.err.includes('already created')) {
198
- console.warn(`User creation note: ${ack.err}, continuing...`);
199
- // Try to authenticate again
200
- user.auth(this.userName(holon), password, (authAck) => {
201
- if (authAck.err) {
202
- if (authAck.err.includes('already being created') ||
203
- authAck.err.includes('already created')) {
204
- console.warn(`Auth note: ${authAck.err}, continuing...`);
205
- resolve(); // Continue anyway
206
- } else {
207
- reject(new Error(authAck.err));
208
- }
209
- } else {
210
- resolve();
211
- }
212
- });
213
- } else {
214
- reject(new Error(ack.err));
215
- }
216
- } else {
217
- user.auth(this.userName(holon), password, (authAck) => {
218
- if (authAck.err) reject(new Error(authAck.err));
219
- else resolve();
220
- });
221
- }
222
- });
223
- });
224
- } catch (createError) {
225
- // Try one last authentication
226
- try {
227
- await new Promise((resolve, reject) => {
228
- setTimeout(() => {
229
- user.auth(this.userName(holon), password, (ack) => {
230
- if (ack.err) {
231
- // Continue even if auth fails at this point
232
- console.warn(`Final auth attempt note: ${ack.err}, continuing with limited functionality`);
233
- resolve();
234
- } else {
235
- resolve();
236
- }
237
- });
238
- }, 100); // Short delay before retry
239
- });
240
- } catch (finalAuthError) {
241
- console.warn('All authentication attempts failed, continuing with limited functionality');
242
- }
243
- }
244
- }
239
+ });
245
240
  }
246
241
 
247
242
  return new Promise((resolve, reject) => {
@@ -252,14 +247,17 @@ class HoloSphere {
252
247
  if (ack.err) {
253
248
  reject(new Error(ack.err));
254
249
  } else {
255
- this.notifySubscribers({
256
- holon,
257
- lens,
258
- ...data
259
- });
250
+ // Only notify subscribers for actual data, not references
251
+ if (!isRef) {
252
+ this.notifySubscribers({
253
+ holon,
254
+ lens,
255
+ ...data
256
+ });
257
+ }
260
258
 
261
- // Auto-propagate to federation by default
262
- const shouldPropagate = options.autoPropagate !== false;
259
+ // Auto-propagate to federation by default (if not a reference)
260
+ const shouldPropagate = options.autoPropagate !== false && !isRef;
263
261
  let propagationResult = null;
264
262
 
265
263
  if (shouldPropagate) {
@@ -288,18 +286,17 @@ class HoloSphere {
288
286
 
289
287
  resolve({
290
288
  success: true,
289
+ isReference: isRef,
291
290
  propagationResult
292
291
  });
293
292
  }
294
293
  };
295
294
 
296
- if (password) {
297
- // For private data, use the authenticated user's holon
298
- user.get('private').get(lens).get(data.id).put(payload, putCallback);
299
- } else {
300
- // For public data, use the regular path
301
- this.gun.get(this.appname).get(holon).get(lens).get(data.id).put(payload, putCallback);
302
- }
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);
303
300
  } catch (error) {
304
301
  reject(error);
305
302
  }
@@ -338,30 +335,15 @@ class HoloSphere {
338
335
  }
339
336
 
340
337
  try {
341
- const user = this.gun.user();
342
-
338
+ let user = null;
343
339
  if (password) {
344
- try {
345
- await new Promise((resolve, reject) => {
346
- user.auth(this.userName(holon), password, (ack) => {
347
- if (ack.err) reject(new Error(ack.err));
348
- else resolve();
349
- });
350
- });
351
- } catch (loginError) {
352
- // If authentication fails, try to create user and then authenticate
353
- await new Promise((resolve, reject) => {
354
- user.create(this.userName(holon), password, (ack) => {
355
- if (ack.err) reject(new Error(ack.err));
356
- else {
357
- user.auth(this.userName(holon), password, (authAck) => {
358
- if (authAck.err) reject(new Error(authAck.err));
359
- else resolve();
360
- });
361
- }
362
- });
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();
363
345
  });
364
- }
346
+ });
365
347
  }
366
348
 
367
349
  return new Promise((resolve) => {
@@ -380,84 +362,23 @@ class HoloSphere {
380
362
  }
381
363
 
382
364
  // Check if this is a reference that needs to be resolved
383
- if (resolveReferences !== false && parsed) {
384
- // Check if this is a simple reference (id + soul)
385
- if (parsed.soul) {
386
- console.log(`Resolving simple reference with soul: ${parsed.soul}`);
387
- try {
388
- // For direct soul resolution, we need to parse the soul to get the right path
389
- const soulParts = parsed.soul.split('/');
390
- if (soulParts.length >= 4) { // Expected format: appname/holon/lens/key
391
- const originHolon = soulParts[1];
392
- const originLens = soulParts[2];
393
- const originKey = soulParts[3];
394
-
395
- console.log(`Extracting from soul - holon: ${originHolon}, lens: ${originLens}, key: ${originKey}`);
396
-
397
- // Get original data using the extracted path components
398
- const originalData = await this.get(
399
- originHolon,
400
- originLens,
401
- originKey,
402
- null,
403
- { resolveReferences: false } // Prevent infinite recursion
404
- );
405
-
406
- if (originalData) {
407
- console.log(`Original data found through soul path resolution:`, originalData);
408
- resolve({
409
- ...originalData,
410
- _federation: {
411
- isReference: true,
412
- resolved: true,
413
- soul: parsed.soul,
414
- timestamp: Date.now()
415
- }
416
- });
417
- return;
418
- } else {
419
- console.warn(`Could not resolve reference: original data not found at extracted path`);
420
- }
421
- } else {
422
- console.warn(`Soul doesn't match expected format: ${parsed.soul}`);
423
- }
424
- } catch (error) {
425
- console.warn(`Error resolving reference by soul: ${error.message}`);
426
- }
427
- }
428
- // Legacy federation reference
429
- else if (parsed._federation && parsed._federation.isReference) {
430
- console.log(`Resolving legacy federation reference from ${parsed._federation.origin}`);
431
- try {
432
- const reference = parsed._federation;
433
- const originalData = await this.get(
434
- reference.origin,
435
- reference.lens,
436
- key,
437
- null,
438
- { resolveReferences: false } // Prevent infinite recursion
439
- );
440
-
441
- if (originalData) {
442
- return {
443
- ...originalData,
444
- _federation: {
445
- ...reference,
446
- resolved: true,
447
- timestamp: Date.now()
448
- }
449
- };
450
- } else {
451
- console.warn(`Could not resolve legacy reference: original data not found`);
452
- return parsed; // Return the reference if we can't resolve it
453
- }
454
- } catch (error) {
455
- console.warn(`Error resolving legacy reference: ${error.message}`);
456
- return parsed;
457
- }
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;
458
378
  }
459
379
  }
460
380
 
381
+ // Perform schema validation if needed
461
382
  if (schema) {
462
383
  const valid = this.validator.validate(schema, parsed);
463
384
  if (!valid) {
@@ -476,13 +397,11 @@ class HoloSphere {
476
397
  }
477
398
  };
478
399
 
479
- if (password) {
480
- // For private data, use the authenticated user's holon
481
- user.get('private').get(lens).get(key).once(handleData);
482
- } else {
483
- // For public data, use the regular path
484
- this.gun.get(this.appname).get(holon).get(lens).get(key).once(handleData);
485
- }
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);
486
405
  });
487
406
  } catch (error) {
488
407
  console.error('Error in get:', error);
@@ -502,7 +421,7 @@ class HoloSphere {
502
421
 
503
422
  console.log(`getNodeBySoul: Accessing soul ${soul}`);
504
423
 
505
- return new Promise((resolve) => {
424
+ return new Promise((resolve, reject) => {
506
425
  try {
507
426
  const ref = this.getNodeRef(soul);
508
427
  ref.once((data) => {
@@ -515,7 +434,7 @@ class HoloSphere {
515
434
  });
516
435
  } catch (error) {
517
436
  console.error(`getNodeBySoul error:`, error);
518
- resolve(null);
437
+ reject(error);
519
438
  }
520
439
  });
521
440
  }
@@ -550,7 +469,16 @@ class HoloSphere {
550
469
  }
551
470
 
552
471
  try {
553
- 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
+ }
554
482
 
555
483
  return new Promise((resolve) => {
556
484
  const output = new Map();
@@ -562,6 +490,26 @@ class HoloSphere {
562
490
  const parsed = await this.parse(data);
563
491
  if (!parsed || !parsed.id) return;
564
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
+
565
513
  if (schema) {
566
514
  const valid = this.validator.validate(schema, parsed);
567
515
  if (valid || !this.strict) {
@@ -597,13 +545,11 @@ class HoloSphere {
597
545
  }
598
546
  };
599
547
 
600
- if (password) {
601
- // For private data, use the authenticated user's holon
602
- user.get('private').get(lens).once(handleData);
603
- } else {
604
- // For public data, use the regular path
605
- this.gun.get(this.appname).get(holon).get(lens).once(handleData);
606
- }
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);
607
553
  });
608
554
  } catch (error) {
609
555
  console.error('Error in getAll:', error);
@@ -683,30 +629,29 @@ class HoloSphere {
683
629
  }
684
630
 
685
631
  try {
686
- // Get the appropriate holon
687
- 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
+ }
688
642
 
689
- // Delete data from holon
690
643
  return new Promise((resolve, reject) => {
691
- if (password) {
692
- // For private data, use the authenticated user's holon
693
- user.get('private').get(lens).get(key).put(null, ack => {
694
- if (ack.err) {
695
- reject(new Error(ack.err));
696
- } else {
697
- resolve(true);
698
- }
699
- });
700
- } else {
701
- // For public data, use the regular path
702
- this.gun.get(this.appname).get(holon).get(lens).get(key).put(null, ack => {
703
- if (ack.err) {
704
- reject(new Error(ack.err));
705
- } else {
706
- resolve(true);
707
- }
708
- });
709
- }
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
+ });
710
655
  });
711
656
  } catch (error) {
712
657
  console.error('Error in delete:', error);
@@ -728,8 +673,16 @@ class HoloSphere {
728
673
  }
729
674
 
730
675
  try {
731
- // Get the appropriate holon
732
- 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
+ }
733
686
 
734
687
  return new Promise((resolve) => {
735
688
  let deletionPromises = [];
@@ -826,18 +779,22 @@ class HoloSphere {
826
779
  throw new Error('getNode: Missing required parameters');
827
780
  }
828
781
 
829
- return new Promise((resolve) => {
830
- this.gun.get(this.appname)
831
- .get(holon)
832
- .get(lens)
833
- .get(key)
834
- .once((data) => {
835
- if (!data) {
836
- resolve(null);
837
- return;
838
- }
839
- resolve(data); // Return the data directly
840
- });
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
+ }
841
798
  });
842
799
  }
843
800
 
@@ -904,96 +861,29 @@ class HoloSphere {
904
861
  throw new Error('Table name and data are required');
905
862
  }
906
863
 
907
- const user = this.gun.user();
908
-
864
+ let user = null;
909
865
  if (password) {
910
- try {
911
- // Try to authenticate first
912
- await new Promise((resolve, reject) => {
913
- user.auth(this.userName(tableName), password, (ack) => {
914
- if (ack.err) {
915
- // Handle wrong username/password gracefully
916
- if (ack.err.includes('Wrong user or password') ||
917
- ack.err.includes('No user')) {
918
- console.warn(`Authentication failed for ${tableName}: ${ack.err}`);
919
- // Will try to create user next
920
- reject(new Error(ack.err));
921
- } else {
922
- reject(new Error(ack.err));
923
- }
924
- } else {
925
- resolve();
926
- }
927
- });
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();
928
871
  });
929
- } catch (authError) {
930
- // If authentication fails, try to create user
931
- try {
932
- await new Promise((resolve, reject) => {
933
- user.create(this.userName(tableName), password, (ack) => {
934
- // Handle "User already created!" error gracefully
935
- if (ack.err && !ack.err.includes('already created')) {
936
- reject(new Error(ack.err));
937
- } else {
938
- // Whether user was created or already existed, try to authenticate
939
- user.auth(this.userName(tableName), password, (authAck) => {
940
- if (authAck.err) {
941
- console.warn(`Authentication failed after creation for ${tableName}: ${authAck.err}`);
942
- reject(new Error(authAck.err));
943
- } else {
944
- resolve();
945
- }
946
- });
947
- }
948
- });
949
- });
950
- } catch (createError) {
951
- // If both auth and create fail, try one last auth attempt
952
- await new Promise((resolve, reject) => {
953
- user.auth(this.userName(tableName), password, (ack) => {
954
- if (ack.err) {
955
- console.warn(`Final authentication attempt failed for ${tableName}: ${ack.err}`);
956
- // Continue with operation even if auth fails
957
- resolve();
958
- } else {
959
- resolve();
960
- }
961
- });
962
- });
963
- }
964
- }
872
+ });
965
873
  }
966
874
 
967
875
  return new Promise((resolve, reject) => {
968
- const payload = JSON.stringify(data);
969
-
970
- if (password) {
971
- // For private data, use the authenticated user's holon
972
- const path = user.get('private').get(tableName);
876
+ try {
877
+ const payload = JSON.stringify(data);
973
878
 
974
- if (data.id) {
975
- path.get(data.id).put(payload, ack => {
976
- if (ack.err) {
977
- reject(new Error(ack.err));
978
- } else {
979
- resolve();
980
- }
981
- });
982
- } else {
983
- path.put(payload, ack => {
984
- if (ack.err) {
985
- reject(new Error(ack.err));
986
- } else {
987
- resolve();
988
- }
989
- });
990
- }
991
- } else {
992
- // For public data, use the regular path
993
- 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);
994
882
 
995
883
  if (data.id) {
996
- path.get(data.id).put(payload, ack => {
884
+ // Store at the specific key path
885
+ dataPath.get(data.id).put(payload, ack => {
886
+
997
887
  if (ack.err) {
998
888
  reject(new Error(ack.err));
999
889
  } else {
@@ -1001,7 +891,7 @@ class HoloSphere {
1001
891
  }
1002
892
  });
1003
893
  } else {
1004
- path.put(payload, ack => {
894
+ dataPath.put(payload, ack => {
1005
895
  if (ack.err) {
1006
896
  reject(new Error(ack.err));
1007
897
  } else {
@@ -1009,6 +899,8 @@ class HoloSphere {
1009
899
  }
1010
900
  });
1011
901
  }
902
+ } catch (error) {
903
+ reject(error);
1012
904
  }
1013
905
  });
1014
906
  } catch (error) {
@@ -1025,72 +917,59 @@ class HoloSphere {
1025
917
  * @returns {Promise<object|null>} - The parsed data for the key or null if not found.
1026
918
  */
1027
919
  async getGlobal(tableName, key, password = null) {
1028
- try {
1029
- const user = this.gun.user();
1030
-
920
+ try {
921
+ let user = null;
1031
922
  if (password) {
1032
- try {
1033
- await new Promise((resolve, reject) => {
1034
- user.auth(this.userName(tableName), password, (ack) => {
1035
- if (ack.err) {
1036
- // Handle wrong username/password gracefully
1037
- if (ack.err.includes('Wrong user or password') ||
1038
- ack.err.includes('No user')) {
1039
- console.warn(`Authentication failed for ${tableName}: ${ack.err}`);
1040
- // Will try to create user next
1041
- reject(new Error(ack.err));
1042
- } else {
1043
- reject(new Error(ack.err));
1044
- }
1045
- } else {
1046
- resolve();
1047
- }
1048
- });
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();
1049
928
  });
1050
- } catch (loginError) {
1051
- // If authentication fails, try to create user and then authenticate
1052
- await new Promise((resolve, reject) => {
1053
- user.create(this.userName(tableName), password, (ack) => {
1054
- // Handle "User already created!" error gracefully
1055
- if (ack.err && !ack.err.includes('already created')) {
1056
- reject(new Error(ack.err));
1057
- } else {
1058
- user.auth(this.userName(tableName), password, (authAck) => {
1059
- if (authAck.err) {
1060
- console.warn(`Authentication failed after creation for ${tableName}: ${authAck.err}`);
1061
- // Continue with operation even if auth fails
1062
- resolve();
1063
- } else {
1064
- resolve();
1065
- }
1066
- });
1067
- }
1068
- });
1069
- });
1070
- }
929
+ });
1071
930
  }
1072
931
 
1073
932
  return new Promise((resolve) => {
1074
- const handleData = (data) => {
933
+ const handleData = async (data) => {
1075
934
  if (!data) {
1076
935
  resolve(null);
1077
936
  return;
1078
937
  }
938
+
1079
939
  try {
1080
- 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
+
1081
961
  resolve(parsed);
1082
962
  } catch (e) {
963
+ console.error('Error parsing data in getGlobal:', e);
1083
964
  resolve(null);
1084
965
  }
1085
966
  };
1086
967
 
1087
- if (password) {
1088
- // For private data, use the authenticated user's holon
1089
- user.get('private').get(tableName).get(key).once(handleData);
1090
- } else {
1091
- // For public data, use the regular path
1092
- this.gun.get(this.appname).get(tableName).get(key).once(handleData);
1093
- }
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);
1094
973
  });
1095
974
  } catch (error) {
1096
975
  console.error('Error in getGlobal:', error);
@@ -1110,8 +989,16 @@ class HoloSphere {
1110
989
  }
1111
990
 
1112
991
  try {
1113
- // Get the appropriate holon
1114
- 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
+ }
1115
1002
 
1116
1003
  return new Promise((resolve) => {
1117
1004
  let output = [];
@@ -1145,7 +1032,23 @@ class HoloSphere {
1145
1032
  if (itemData) {
1146
1033
  try {
1147
1034
  const parsed = await this.parse(itemData);
1148
- 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
+ }
1149
1052
  } catch (error) {
1150
1053
  console.error('Error parsing data:', error);
1151
1054
  }
@@ -1162,13 +1065,11 @@ class HoloSphere {
1162
1065
  }
1163
1066
  };
1164
1067
 
1165
- if (password) {
1166
- // For private data, use the authenticated user's holon
1167
- user.get('private').get(tableName).once(handleData);
1168
- } else {
1169
- // For public data, use the regular path
1170
- this.gun.get(this.appname).get(tableName).once(handleData);
1171
- }
1068
+ const dataPath = password ?
1069
+ user.get('private').get(tableName) :
1070
+ this.gun.get(this.appname).get(tableName);
1071
+
1072
+ dataPath.once(handleData);
1172
1073
  });
1173
1074
  } catch (error) {
1174
1075
  console.error('Error in getAllGlobal:', error);
@@ -1189,29 +1090,45 @@ class HoloSphere {
1189
1090
  }
1190
1091
 
1191
1092
  try {
1192
- // Get the appropriate holon
1193
- 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
+ }
1194
1105
 
1195
1106
  return new Promise((resolve, reject) => {
1196
- if (password) {
1197
- // For private data, use the authenticated user's holon
1198
- user.get('private').get(tableName).get(key).put(null, ack => {
1199
- if (ack.err) {
1200
- reject(new Error(ack.err));
1201
- } else {
1202
- resolve(true);
1203
- }
1204
- });
1205
- } else {
1206
- // For public data, use the regular path
1207
- 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);
1208
1120
  if (ack.err) {
1121
+ console.error('deleteGlobal - Deletion error:', ack.err);
1209
1122
  reject(new Error(ack.err));
1210
1123
  } else {
1211
- resolve(true);
1124
+ // Verify deletion
1125
+ dataPath.get(key).once((deletedData) => {
1126
+ console.log('deleteGlobal - Data after deletion:', deletedData);
1127
+ resolve(true);
1128
+ });
1212
1129
  }
1213
1130
  });
1214
- }
1131
+ });
1215
1132
  });
1216
1133
  } catch (error) {
1217
1134
  console.error('Error in deleteGlobal:', error);
@@ -1231,8 +1148,16 @@ class HoloSphere {
1231
1148
  }
1232
1149
 
1233
1150
  try {
1234
- // Get the appropriate holon
1235
- 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
+ }
1236
1161
 
1237
1162
  return new Promise((resolve, reject) => {
1238
1163
  try {
@@ -1256,7 +1181,7 @@ class HoloSphere {
1256
1181
 
1257
1182
  const keys = Object.keys(data).filter(key => key !== '_');
1258
1183
  const promises = keys.map(key =>
1259
- new Promise((resolveDelete) => {
1184
+ new Promise((resolveDelete, rejectDelete) => {
1260
1185
  const deletePath = password ?
1261
1186
  user.get('private').get(tableName).get(key) :
1262
1187
  this.gun.get(this.appname).get(tableName).get(key);
@@ -1264,8 +1189,10 @@ class HoloSphere {
1264
1189
  deletePath.put(null, ack => {
1265
1190
  if (ack.err) {
1266
1191
  console.error(`Failed to delete ${key}:`, ack.err);
1192
+ rejectDelete(new Error(ack.err));
1193
+ } else {
1194
+ resolveDelete();
1267
1195
  }
1268
- resolveDelete();
1269
1196
  });
1270
1197
  })
1271
1198
  );
@@ -1290,6 +1217,159 @@ class HoloSphere {
1290
1217
  }
1291
1218
  }
1292
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
+
1293
1373
  // ================================ COMPUTE FUNCTIONS ================================
1294
1374
  /**
1295
1375
  * Computes operations across multiple layers up the hierarchy
@@ -1497,8 +1577,7 @@ class HoloSphere {
1497
1577
  }
1498
1578
 
1499
1579
  /**
1500
- * Upcasts content to parent holonagons recursively using federation and soul references.
1501
- * This is the modern implementation that uses federation references instead of duplicating data.
1580
+ * Upcasts content to parent holonagons recursively using references.
1502
1581
  * @param {string} holon - The current holon identifier.
1503
1582
  * @param {string} lens - The lens under which to upcast.
1504
1583
  * @param {object} content - The content to upcast.
@@ -1519,18 +1598,10 @@ class HoloSphere {
1519
1598
  // Get the parent cell
1520
1599
  let parent = h3.cellToParent(holon, res - 1);
1521
1600
 
1522
- // Create federation relationship if it doesn't exist
1523
- await this.federate(holon, parent);
1524
-
1525
- // Create a soul reference to store in the parent
1526
- const soul = `${this.appname}/${holon}/${lens}/${content.id}`;
1527
- const reference = {
1528
- id: content.id,
1529
- soul: soul
1530
- };
1601
+ // Create a reference to store in the parent
1602
+ const reference = this.createReference(holon, lens, content);
1531
1603
 
1532
1604
  // Store the reference in the parent cell
1533
- // We use { autoPropagate: false } to prevent circular propagation
1534
1605
  await this.put(parent, lens, reference, null, {
1535
1606
  autoPropagate: false
1536
1607
  });
@@ -1613,8 +1684,12 @@ class HoloSphere {
1613
1684
  * @returns {Promise<object>} - Subscription object with unsubscribe method
1614
1685
  */
1615
1686
  async subscribe(holon, lens, callback) {
1616
- if (!holon || !lens || typeof callback !== 'function') {
1617
- 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');
1618
1693
  }
1619
1694
 
1620
1695
  const subscriptionId = this.generateId();
@@ -1622,10 +1697,35 @@ class HoloSphere {
1622
1697
  try {
1623
1698
  // Create the subscription
1624
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
+
1625
1705
  if (data) {
1626
1706
  try {
1627
1707
  let parsed = await this.parse(data);
1628
- 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
+ }
1629
1729
  } catch (error) {
1630
1730
  console.error('Error in subscribe:', error);
1631
1731
  }
@@ -1638,6 +1738,7 @@ class HoloSphere {
1638
1738
  holon,
1639
1739
  lens,
1640
1740
  active: true,
1741
+ callback,
1641
1742
  gunSubscription
1642
1743
  };
1643
1744
 
@@ -1645,14 +1746,18 @@ class HoloSphere {
1645
1746
  return {
1646
1747
  unsubscribe: () => {
1647
1748
  try {
1648
- // Turn off the Gun subscription
1649
- this.gun.get(this.appname).get(holon).get(lens).map().off();
1650
-
1651
- // Mark as inactive and remove from subscriptions
1749
+ // Mark as inactive first to prevent any new callbacks
1652
1750
  if (this.subscriptions[subscriptionId]) {
1653
1751
  this.subscriptions[subscriptionId].active = false;
1654
- delete this.subscriptions[subscriptionId];
1655
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];
1656
1761
  } catch (error) {
1657
1762
  console.error('Error in unsubscribe:', error);
1658
1763
  }
@@ -1664,7 +1769,6 @@ class HoloSphere {
1664
1769
  }
1665
1770
  }
1666
1771
 
1667
-
1668
1772
  /**
1669
1773
  * Notifies subscribers about data changes
1670
1774
  * @param {object} data - The data to notify about
@@ -1840,11 +1944,10 @@ class HoloSphere {
1840
1944
  try {
1841
1945
  const subscription = this.subscriptions[id];
1842
1946
  if (subscription && subscription.active) {
1843
- // Turn off the Gun subscription
1844
- this.gun.get(this.appname)
1845
- .get(subscription.holon)
1846
- .get(subscription.lens)
1847
- .map().off();
1947
+ // Turn off the Gun subscription using the stored reference
1948
+ if (subscription.gunSubscription) {
1949
+ subscription.gunSubscription.off();
1950
+ }
1848
1951
 
1849
1952
  // Mark as inactive
1850
1953
  subscription.active = false;
@@ -1856,29 +1959,66 @@ class HoloSphere {
1856
1959
 
1857
1960
  // Clear subscriptions
1858
1961
  this.subscriptions = {};
1962
+
1963
+ // Clear schema cache
1964
+ this.clearSchemaCache();
1859
1965
 
1860
1966
  // Close Gun connections
1861
1967
  if (this.gun.back) {
1862
1968
  try {
1969
+ // Clean up mesh connections
1863
1970
  const mesh = this.gun.back('opt.mesh');
1864
- if (mesh && mesh.hear) {
1865
- try {
1866
- // Safely clear mesh.hear without modifying function properties
1867
- const hearKeys = Object.keys(mesh.hear);
1868
- for (const key of hearKeys) {
1869
- // Check if it's an array before trying to clear it
1870
- if (Array.isArray(mesh.hear[key])) {
1871
- 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 = {};
1872
1988
  }
1989
+ } catch (meshError) {
1990
+ console.warn('Error cleaning up Gun mesh hear:', meshError);
1873
1991
  }
1874
-
1875
- // Create a new empty object for mesh.hear
1876
- // Only if mesh.hear is not a function
1877
- if (typeof mesh.hear !== 'function') {
1878
- 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();
1879
2019
  }
1880
- } catch (meshError) {
1881
- console.warn('Error cleaning up Gun mesh hear:', meshError);
2020
+ } catch (webError) {
2021
+ console.warn('Error closing web server:', webError);
1882
2022
  }
1883
2023
  }
1884
2024
  } catch (error) {