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/federation.js +67 -21
- package/holosphere.js +578 -438
- package/package.json +1 -1
- package/test/auth.test.js +35 -19
- package/test/delete.test.js +1 -0
- package/test/holosphere.test.js +189 -5
- package/test/reference.test.js +211 -0
- package/test/subscription.test.js +329 -0
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.
|
|
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
|
-
//
|
|
164
|
-
|
|
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
|
-
|
|
180
|
-
|
|
231
|
+
let user = null;
|
|
181
232
|
if (password) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
342
|
-
|
|
338
|
+
let user = null;
|
|
343
339
|
if (password) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
437
|
+
reject(error);
|
|
519
438
|
}
|
|
520
439
|
});
|
|
521
440
|
}
|
|
@@ -550,7 +469,16 @@ class HoloSphere {
|
|
|
550
469
|
}
|
|
551
470
|
|
|
552
471
|
try {
|
|
553
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
687
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
732
|
-
|
|
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
|
-
|
|
831
|
-
.get(
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
908
|
-
|
|
864
|
+
let user = null;
|
|
909
865
|
if (password) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
920
|
+
try {
|
|
921
|
+
let user = null;
|
|
1031
922
|
if (password) {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
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)
|
|
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
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
//
|
|
1207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1235
|
-
|
|
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
|
|
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
|
|
1523
|
-
|
|
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
|
|
1617
|
-
throw new Error('subscribe: Missing
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1845
|
-
.
|
|
1846
|
-
|
|
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
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
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 (
|
|
1881
|
-
console.warn('Error
|
|
2020
|
+
} catch (webError) {
|
|
2021
|
+
console.warn('Error closing web server:', webError);
|
|
1882
2022
|
}
|
|
1883
2023
|
}
|
|
1884
2024
|
} catch (error) {
|