holosphere 1.1.5 → 1.1.7

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
@@ -3,7 +3,9 @@ import OpenAI from 'openai';
3
3
  import Gun from 'gun'
4
4
  import SEA from 'gun/sea.js'
5
5
  import Ajv2019 from 'ajv/dist/2019.js'
6
+ import * as Federation from './federation.js';
6
7
 
8
+ export { federateMessage, getFederatedMessages, updateFederatedMessages, removeNotify } from './federation.js';
7
9
 
8
10
  class HoloSphere {
9
11
  /**
@@ -14,7 +16,7 @@ class HoloSphere {
14
16
  * @param {Gun|null} gunInstance - The Gun instance to use.
15
17
  */
16
18
  constructor(appname, strict = false, openaikey = null, gunInstance = null) {
17
- console.log('HoloSphere v1.1.8');
19
+ console.log('HoloSphere v1.1.7');
18
20
  this.appname = appname
19
21
  this.strict = strict;
20
22
  this.validator = new Ajv2019({
@@ -23,14 +25,17 @@ class HoloSphere {
23
25
  validateSchema: true // Always validate schemas
24
26
  });
25
27
 
26
- // Use provided Gun instance or create new one
27
- this.gun = gunInstance || Gun({
28
- peers: ['https://gun.holons.io/gun'],
29
- axe: false,
30
- // uuid: (content) => { // generate a unique id for each node
31
- // console.log('uuid', content);
32
- // return content;}
33
- });
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
+ }
34
39
 
35
40
  // Initialize SEA
36
41
  this.sea = SEA;
@@ -41,9 +46,6 @@ class HoloSphere {
41
46
  });
42
47
  }
43
48
 
44
- // Add currentSpace property to track logged in space
45
- this.currentSpace = null;
46
-
47
49
  // Initialize subscriptions
48
50
  this.subscriptions = {};
49
51
  }
@@ -105,8 +107,7 @@ class HoloSphere {
105
107
  await this.putGlobal('schemas', {
106
108
  id: lens,
107
109
  schema: schema,
108
- timestamp: Date.now(),
109
- owner: this.currentSpace?.alias
110
+ timestamp: Date.now()
110
111
  });
111
112
 
112
113
  return true;
@@ -137,300 +138,439 @@ class HoloSphere {
137
138
  * @param {string} holon - The holon identifier.
138
139
  * @param {string} lens - The lens under which to store the content.
139
140
  * @param {object} data - The data to store.
141
+ * @param {string} [password] - Optional password for private holon.
142
+ * @param {object} [options] - Additional options
143
+ * @param {boolean} [options.autoPropagate=true] - Whether to automatically propagate to federated holons (default: true)
144
+ * @param {object} [options.propagationOptions] - Options to pass to propagate
145
+ * @param {boolean} [options.propagationOptions.useReferences=true] - Whether to use references instead of duplicating data
140
146
  * @returns {Promise<boolean>} - Returns true if successful, false if there was an error
141
147
  */
142
- async put(holon, lens, data) {
143
- // Check authentication for data operations
144
- if (!this.currentSpace) {
145
- throw new Error('Unauthorized to modify this data');
146
- }
147
- this._checkSession();
148
-
149
- // If updating existing data, check ownership
150
- if (data.id) {
151
- const existing = await this.get(holon, lens, data.id);
152
- if (existing && existing.owner &&
153
- existing.owner !== this.currentSpace.alias &&
154
- !existing.federation) { // Skip ownership check for federated data
155
- throw new Error('Unauthorized to modify this data');
156
- }
157
- }
158
-
159
- // Add owner and federation information to data
160
- const dataWithMeta = {
161
- ...data,
162
- owner: this.currentSpace.alias,
163
- federation: {
164
- origin: this.currentSpace.alias,
165
- timestamp: Date.now()
166
- }
167
- };
168
-
169
- if (!holon || !lens || !dataWithMeta) {
170
- throw new Error('put: Missing required parameters');
148
+ async put(holon, lens, data, password = null, options = {}) {
149
+ if (!holon || !lens || !data) {
150
+ throw new Error('put: Missing required parameters:', holon, lens, data );
171
151
  }
172
152
 
173
- if (!dataWithMeta.id) {
174
- dataWithMeta.id = this.generateId();
153
+ if (!data.id) {
154
+ data.id = this.generateId();
175
155
  }
176
156
 
177
- // Get and validate schema first
178
- const schema = await this.getSchema(lens);
179
- if (schema) {
180
- // Deep clone data to avoid modifying the original
181
- const dataToValidate = JSON.parse(JSON.stringify(dataWithMeta));
157
+ // Get and validate schema only in strict mode
158
+ if (this.strict) {
159
+ const schema = await this.getSchema(lens);
160
+ if (!schema) {
161
+ throw new Error('Schema required in strict mode');
162
+ }
163
+ const dataToValidate = JSON.parse(JSON.stringify(data));
182
164
  const valid = this.validator.validate(schema, dataToValidate);
183
165
 
184
166
  if (!valid) {
185
167
  const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
186
- // Always throw on schema validation failure, regardless of strict mode
187
168
  throw new Error(errorMsg);
188
169
  }
189
- } else if (this.strict) {
190
- throw new Error('Schema required in strict mode');
191
170
  }
192
171
 
193
- // Store data in current space
194
- const putResult = await new Promise((resolve, reject) => {
195
- try {
196
- const payload = JSON.stringify(dataWithMeta);
197
- this.gun.get(this.appname)
198
- .get(holon)
199
- .get(lens)
200
- .get(dataWithMeta.id)
201
- .put(payload, ack => {
172
+ try {
173
+ const user = this.gun.user();
174
+
175
+ 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
+ });
182
+ });
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
+ }
240
+
241
+ return new Promise((resolve, reject) => {
242
+ try {
243
+ const payload = JSON.stringify(data);
244
+
245
+ const putCallback = async (ack) => {
202
246
  if (ack.err) {
203
247
  reject(new Error(ack.err));
204
248
  } else {
205
- // Notify subscribers after successful put
206
249
  this.notifySubscribers({
207
250
  holon,
208
251
  lens,
209
- ...dataWithMeta
252
+ ...data
253
+ });
254
+
255
+ // Auto-propagate to federation by default
256
+ const shouldPropagate = options.autoPropagate !== false;
257
+ let propagationResult = null;
258
+
259
+ if (shouldPropagate) {
260
+ try {
261
+ // Default to using references
262
+ const propagationOptions = {
263
+ useReferences: true,
264
+ ...options.propagationOptions
265
+ };
266
+
267
+ propagationResult = await this.propagate(
268
+ holon,
269
+ lens,
270
+ data,
271
+ propagationOptions
272
+ );
273
+
274
+ // Still resolve with true even if propagation had errors
275
+ if (propagationResult.errors > 0) {
276
+ console.warn('Auto-propagation had errors:', propagationResult);
277
+ }
278
+ } catch (propError) {
279
+ console.warn('Error in auto-propagation:', propError);
280
+ }
281
+ }
282
+
283
+ resolve({
284
+ success: true,
285
+ propagationResult
210
286
  });
211
- resolve(true);
212
287
  }
213
- });
214
- } catch (error) {
215
- reject(error);
216
- }
217
- });
218
-
219
- // If successful, propagate to federated spaces
220
- if (putResult) {
221
- await this._propagateToFederation(holon, lens, dataWithMeta);
288
+ };
289
+
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
+ }
297
+ } catch (error) {
298
+ reject(error);
299
+ }
300
+ });
301
+ } catch (error) {
302
+ console.error('Error in put:', error);
303
+ throw error;
222
304
  }
223
-
224
- return putResult;
225
305
  }
226
306
 
227
307
  /**
228
- * Propagates data to federated spaces
229
- * @private
230
- * @param {string} holon - The holon identifier
231
- * @param {string} lens - The lens identifier
232
- * @param {object} data - The data to propagate
308
+ * Retrieves content from the specified holon and lens.
309
+ * @param {string} holon - The holon identifier.
310
+ * @param {string} lens - The lens from which to retrieve content.
311
+ * @param {string} key - The specific key to retrieve.
312
+ * @param {string} [password] - Optional password for private holon.
313
+ * @param {object} [options] - Additional options
314
+ * @param {boolean} [options.resolveReferences=true] - Whether to automatically resolve federation references
315
+ * @returns {Promise<object|null>} - The retrieved content or null if not found.
233
316
  */
234
- async _propagateToFederation(holon, lens, data) {
317
+ async get(holon, lens, key, password = null, options = {}) {
318
+ if (!holon || !lens || !key) {
319
+ console.error('get: Missing required parameters:', { holon, lens, key });
320
+ return null;
321
+ }
322
+
323
+ const { resolveReferences = true } = options;
324
+
325
+ // Only check schema in strict mode
326
+ let schema;
327
+ if (this.strict) {
328
+ schema = await this.getSchema(lens);
329
+ if (!schema) {
330
+ throw new Error('Schema required in strict mode');
331
+ }
332
+ }
333
+
235
334
  try {
236
- // Get federation info for current space
237
- const fedInfo = await this.getFederation(this.currentSpace.alias);
238
- if (!fedInfo || !fedInfo.notify || fedInfo.notify.length === 0) {
239
- return; // No federation to propagate to
335
+ const user = this.gun.user();
336
+
337
+ 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
+ });
357
+ });
358
+ }
240
359
  }
241
360
 
242
- // Propagate to each federated space
243
- const propagationPromises = fedInfo.notify.map(spaceId =>
244
- new Promise((resolve) => {
245
- // Store data in the federated space's lens
246
- this.gun.get(this.appname)
247
- .get(spaceId)
248
- .get(lens)
249
- .get(data.id)
250
- .put(JSON.stringify({
251
- ...data,
252
- federation: {
253
- ...data.federation,
254
- notified: Date.now()
361
+ return new Promise((resolve) => {
362
+ const handleData = async (data) => {
363
+ if (!data) {
364
+ resolve(null);
365
+ return;
366
+ }
367
+
368
+ try {
369
+ const parsed = await this.parse(data);
370
+
371
+ if (!parsed) {
372
+ resolve(null);
373
+ return;
374
+ }
375
+
376
+ // 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
+ }
255
421
  }
256
- }), ack => {
257
- if (ack.err) {
258
- console.warn(`Failed to propagate to space ${spaceId}:`, ack.err);
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
+ }
259
452
  }
260
- resolve();
261
- });
453
+ }
262
454
 
263
- // Also store in federation lens for notifications
264
- this.gun.get(this.appname)
265
- .get(spaceId)
266
- .get('federation')
267
- .get(data.id)
268
- .put(JSON.stringify({
269
- ...data,
270
- federation: {
271
- ...data.federation,
272
- notified: Date.now()
455
+ if (schema) {
456
+ const valid = this.validator.validate(schema, parsed);
457
+ if (!valid) {
458
+ console.error('get: Invalid data according to schema:', this.validator.errors);
459
+ if (this.strict) {
460
+ resolve(null);
461
+ return;
462
+ }
273
463
  }
274
- }));
275
- })
276
- );
464
+ }
465
+
466
+ resolve(parsed);
467
+ } catch (error) {
468
+ console.error('Error parsing data:', error);
469
+ resolve(null);
470
+ }
471
+ };
277
472
 
278
- await Promise.all(propagationPromises);
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
+ }
480
+ });
279
481
  } catch (error) {
280
- console.warn('Federation propagation error:', error);
281
- // Don't throw here to avoid failing the original put
482
+ console.error('Error in get:', error);
483
+ return null;
282
484
  }
283
485
  }
284
486
 
285
487
  /**
286
- * Retrieves content from the specified holon and lens.
287
- * @param {string} holon - The holon identifier.
288
- * @param {string} lens - The lens from which to retrieve content.
289
- * @returns {Promise<Array<object>>} - The retrieved content.
488
+ * Retrieves a node directly using its soul path
489
+ * @param {string} soul - The soul path of the node
490
+ * @returns {Promise<any>} - The retrieved node or null if not found.
290
491
  */
291
- async getAll(holon, lens) {
292
- if (!holon || !lens) {
293
- throw new Error('getAll: Missing required parameters');
492
+ async getNodeBySoul(soul) {
493
+ if (!soul) {
494
+ throw new Error('getNodeBySoul: Missing soul parameter');
294
495
  }
295
496
 
296
- const schema = await this.getSchema(lens);
297
- if (!schema && this.strict) {
298
- throw new Error('getAll: Schema required in strict mode');
299
- }
300
-
301
- // Get local data
302
- const localData = await this._getAllLocal(holon, lens, schema);
303
-
304
- // If authenticated, get federated data
305
- let federatedData = [];
306
- if (this.currentSpace) {
307
- federatedData = await this._getAllFederated(holon, lens, schema);
308
- }
497
+ console.log(`getNodeBySoul: Accessing soul ${soul}`);
309
498
 
310
- // Combine and deduplicate data based on ID
311
- const combined = new Map();
312
-
313
- // Add local data first
314
- localData.forEach(item => {
315
- if (item.id) {
316
- combined.set(item.id, item);
317
- }
318
- });
319
-
320
- // Add federated data, potentially overwriting local data if newer
321
- federatedData.forEach(item => {
322
- if (item.id) {
323
- const existing = combined.get(item.id);
324
- if (!existing ||
325
- (item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
326
- combined.set(item.id, item);
327
- }
499
+ return new Promise((resolve) => {
500
+ try {
501
+ const ref = this.getNodeRef(soul);
502
+ ref.once((data) => {
503
+ console.log(`getNodeBySoul: Retrieved data:`, data);
504
+ if (!data) {
505
+ resolve(null);
506
+ return;
507
+ }
508
+ resolve(data); // Return the data directly
509
+ });
510
+ } catch (error) {
511
+ console.error(`getNodeBySoul error:`, error);
512
+ resolve(null);
328
513
  }
329
514
  });
330
-
331
- return Array.from(combined.values());
332
515
  }
333
516
 
334
517
  /**
335
- * Gets data from federated spaces
336
- * @private
518
+ * Propagates data to federated holons
337
519
  * @param {string} holon - The holon identifier
338
520
  * @param {string} lens - The lens identifier
339
- * @param {string} key - The key to get
340
- * @returns {Promise<object|null>} - The federated data or null if not found
521
+ * @param {object} data - The data to propagate
522
+ * @param {object} [options] - Propagation options
523
+ * @returns {Promise<object>} - Result with success count and errors
341
524
  */
342
- async _getFederatedData(holon, lens, key) {
343
- try {
344
- const fedInfo = await this.getFederation(this.currentSpace.alias);
345
- if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
346
- return null;
347
- }
348
-
349
- // Try each federated space
350
- for (const spaceId of fedInfo.federation) {
351
- const result = await new Promise((resolve) => {
352
- this.gun.get(this.appname)
353
- .get(spaceId)
354
- .get(lens)
355
- .get(key)
356
- .once(async (data) => {
357
- if (!data) {
358
- resolve(null);
359
- return;
360
- }
361
- try {
362
- const parsed = await this.parse(data);
363
- resolve(parsed);
364
- } catch (error) {
365
- console.warn(`Error parsing federated data from ${spaceId}:`, error);
366
- resolve(null);
367
- }
368
- });
369
- });
370
-
371
- if (result) {
372
- return result;
373
- }
374
- }
375
- } catch (error) {
376
- console.warn('Federation get error:', error);
377
- }
378
- return null;
525
+ async propagate(holon, lens, data, options = {}) {
526
+ return Federation.propagate(this, holon, lens, data, options);
379
527
  }
380
528
 
381
529
  /**
382
- * Gets all data from local space
383
- * @private
384
- * @param {string} holon - The holon identifier
385
- * @param {string} lens - The lens identifier
386
- * @param {object} schema - The schema to validate against
387
- * @returns {Promise<Array>} - Array of local data
530
+ * Retrieves all content from the specified holon and lens.
531
+ * @param {string} holon - The holon identifier.
532
+ * @param {string} lens - The lens from which to retrieve content.
533
+ * @param {string} [password] - Optional password for private holon.
534
+ * @returns {Promise<Array<object>>} - The retrieved content.
388
535
  */
389
- async _getAllLocal(holon, lens, schema) {
390
- return new Promise((resolve) => {
391
- const output = new Map();
392
- let isResolved = false;
393
- let listener = null;
394
-
395
- const hardTimeout = setTimeout(() => {
396
- cleanup();
397
- resolve(Array.from(output.values()));
398
- }, 5000);
399
-
400
- const cleanup = () => {
401
- if (listener) {
402
- listener.off();
403
- }
404
- clearTimeout(hardTimeout);
405
- isResolved = true;
406
- };
536
+ async getAll(holon, lens, password = null) {
537
+ if (!holon || !lens) {
538
+ throw new Error('getAll: Missing required parameters');
539
+ }
407
540
 
408
- const processData = async (data, key) => {
409
- if (!data || key === '_') return;
541
+ const schema = await this.getSchema(lens);
542
+ if (!schema && this.strict) {
543
+ throw new Error('getAll: Schema required in strict mode');
544
+ }
410
545
 
411
- try {
412
- const parsed = await this.parse(data);
413
- if (!parsed || !parsed.id) return;
546
+ try {
547
+ const user = this.gun.user();
548
+
549
+ return new Promise((resolve) => {
550
+ const output = new Map();
414
551
 
415
- if (schema) {
416
- const valid = this.validator.validate(schema, parsed);
417
- if (valid || !this.strict) {
552
+ const processData = async (data, key) => {
553
+ if (!data || key === '_') return;
554
+
555
+ try {
556
+ const parsed = await this.parse(data);
557
+ if (!parsed || !parsed.id) return;
558
+
559
+ if (schema) {
560
+ const valid = this.validator.validate(schema, parsed);
561
+ if (valid || !this.strict) {
562
+ output.set(parsed.id, parsed);
563
+ }
564
+ } else {
418
565
  output.set(parsed.id, parsed);
419
566
  }
420
- } else {
421
- output.set(parsed.id, parsed);
567
+ } catch (error) {
568
+ console.error('Error processing data:', error);
422
569
  }
423
- } catch (error) {
424
- console.error('Error processing data:', error);
425
- }
426
- };
570
+ };
427
571
 
428
- this.gun.get(this.appname)
429
- .get(holon)
430
- .get(lens)
431
- .once(async (data) => {
572
+ const handleData = async (data) => {
432
573
  if (!data) {
433
- cleanup();
434
574
  resolve([]);
435
575
  return;
436
576
  }
@@ -444,75 +584,23 @@ class HoloSphere {
444
584
 
445
585
  try {
446
586
  await Promise.all(initialPromises);
447
- cleanup();
448
587
  resolve(Array.from(output.values()));
449
588
  } catch (error) {
450
- cleanup();
589
+ console.error('Error in getAll:', error);
451
590
  resolve([]);
452
591
  }
453
- });
454
- });
455
- }
456
-
457
- /**
458
- * Gets all data from federated spaces
459
- * @private
460
- * @param {string} holon - The holon identifier
461
- * @param {string} lens - The lens identifier
462
- * @param {object} schema - The schema to validate against
463
- * @returns {Promise<Array>} - Array of federated data
464
- */
465
- async _getAllFederated(holon, lens, schema) {
466
- try {
467
- const fedInfo = await this.getFederation(this.currentSpace.alias);
468
- if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
469
- return [];
470
- }
592
+ };
471
593
 
472
- const federatedData = new Map();
473
-
474
- // Get data from each federated space
475
- const fedPromises = fedInfo.federation.map(spaceId =>
476
- new Promise((resolve) => {
477
- this.gun.get(this.appname)
478
- .get(spaceId)
479
- .get(lens)
480
- .once(async (data) => {
481
- if (!data) {
482
- resolve();
483
- return;
484
- }
485
-
486
- const processPromises = Object.keys(data)
487
- .filter(key => key !== '_')
488
- .map(async key => {
489
- try {
490
- const parsed = await this.parse(data[key]);
491
- if (parsed && parsed.id) {
492
- if (schema) {
493
- const valid = this.validator.validate(schema, parsed);
494
- if (valid || !this.strict) {
495
- federatedData.set(parsed.id, parsed);
496
- }
497
- } else {
498
- federatedData.set(parsed.id, parsed);
499
- }
500
- }
501
- } catch (error) {
502
- console.warn(`Error processing federated data from ${spaceId}:`, error);
503
- }
504
- });
505
-
506
- await Promise.all(processPromises);
507
- resolve();
508
- });
509
- })
510
- );
511
-
512
- await Promise.all(fedPromises);
513
- return Array.from(federatedData.values());
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
+ }
601
+ });
514
602
  } catch (error) {
515
- console.warn('Federation getAll error:', error);
603
+ console.error('Error in getAll:', error);
516
604
  return [];
517
605
  }
518
606
  }
@@ -575,186 +663,116 @@ class HoloSphere {
575
663
  }
576
664
  }
577
665
 
578
- /**
579
- * Retrieves a specific key from the specified holon and lens.
580
- * @param {string} holon - The holon identifier.
581
- * @param {string} lens - The lens from which to retrieve the key.
582
- * @param {string} key - The specific key to retrieve.
583
- * @returns {Promise<object|null>} - The retrieved content or null if not found.
584
- */
585
- async get(holon, lens, key) {
586
- if (!holon || !lens || !key) {
587
- console.error('get: Missing required parameters:', { holon, lens, key });
588
- return null;
589
- }
590
-
591
- // Get schema for validation
592
- const schema = await this.getSchema(lens);
593
-
594
- // First try to get from current space
595
- const localResult = await new Promise((resolve) => {
596
- let timeout = setTimeout(() => {
597
- console.warn('get: Operation timed out');
598
- resolve(null);
599
- }, 5000);
600
-
601
- this.gun.get(this.appname)
602
- .get(holon)
603
- .get(lens)
604
- .get(key)
605
- .once(async (data) => {
606
- clearTimeout(timeout);
607
-
608
- if (!data) {
609
- resolve(null);
610
- return;
611
- }
612
-
613
- try {
614
- const parsed = await this.parse(data);
615
-
616
- // Validate against schema if one exists
617
- if (schema) {
618
- const valid = this.validator.validate(schema, parsed);
619
- if (!valid) {
620
- console.error('get: Invalid data according to schema:', this.validator.errors);
621
- if (this.strict) {
622
- resolve(null);
623
- return;
624
- }
625
- }
626
- }
627
-
628
- // Check if user has access - only allow if:
629
- // 1. No owner (public data)
630
- // 2. User is the owner
631
- // 3. User is in shared list
632
- // 4. Data is from federation
633
- if (parsed.owner &&
634
- this.currentSpace?.alias !== parsed.owner &&
635
- (!parsed.shared || !parsed.shared.includes(this.currentSpace?.alias)) &&
636
- (!parsed.federation || !parsed.federation.origin)) {
637
- resolve(null);
638
- return;
639
- }
640
-
641
- resolve(parsed);
642
- } catch (error) {
643
- console.error('Error parsing data:', error);
644
- resolve(null);
645
- }
646
- });
647
- });
648
-
649
- // If found locally, return it
650
- if (localResult) {
651
- return localResult;
652
- }
653
-
654
- // If not found locally and we're authenticated, try federated spaces
655
- if (this.currentSpace) {
656
- const fedResult = await this._getFederatedData(holon, lens, key);
657
- if (fedResult) {
658
- return fedResult;
659
- }
660
- }
661
-
662
- return null;
663
- }
664
-
665
666
  /**
666
667
  * Deletes a specific key from a given holon and lens.
667
668
  * @param {string} holon - The holon identifier.
668
669
  * @param {string} lens - The lens from which to delete the key.
669
670
  * @param {string} key - The specific key to delete.
671
+ * @param {string} [password] - Optional password for private holon.
672
+ * @returns {Promise<boolean>} - Returns true if successful
670
673
  */
671
- async delete(holon, lens, key) {
674
+ async delete(holon, lens, key, password = null) {
672
675
  if (!holon || !lens || !key) {
673
676
  throw new Error('delete: Missing required parameters');
674
677
  }
675
678
 
676
- if (!this.currentSpace) {
677
- throw new Error('Unauthorized to delete this data');
678
- }
679
- this._checkSession();
680
-
681
- // Check ownership before delete
682
- const data = await this.get(holon, lens, key);
683
- if (!data) {
684
- return true; // Nothing to delete
685
- }
686
-
687
- if (data.owner && data.owner !== this.currentSpace.alias) {
688
- throw new Error('Unauthorized to delete this data');
689
- }
690
-
691
- return new Promise((resolve, reject) => {
692
- try {
693
- this.gun.get(this.appname)
694
- .get(holon)
695
- .get(lens)
696
- .get(key)
697
- .put(null, ack => {
679
+ try {
680
+ // Get the appropriate holon
681
+ const user = this.gun.user();
682
+
683
+ // Delete data from holon
684
+ 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 => {
698
688
  if (ack.err) {
699
689
  reject(new Error(ack.err));
700
690
  } else {
701
691
  resolve(true);
702
692
  }
703
693
  });
704
- } catch (error) {
705
- reject(error);
706
- }
707
- });
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
+ }
704
+ });
705
+ } catch (error) {
706
+ console.error('Error in delete:', error);
707
+ throw error;
708
+ }
708
709
  }
709
710
 
710
711
  /**
711
712
  * Deletes all keys from a given holon and lens.
712
713
  * @param {string} holon - The holon identifier.
713
714
  * @param {string} lens - The lens from which to delete all keys.
714
- * @returns {Promise<boolean>} - Returns true if successful, false if there was an error
715
+ * @param {string} [password] - Optional password for private holon.
716
+ * @returns {Promise<boolean>} - Returns true if successful
715
717
  */
716
- async deleteAll(holon, lens) {
718
+ async deleteAll(holon, lens, password = null) {
717
719
  if (!holon || !lens) {
718
720
  console.error('deleteAll: Missing holon or lens parameter');
719
721
  return false;
720
722
  }
721
723
 
722
- return new Promise((resolve) => {
723
- let deletionPromises = [];
724
+ try {
725
+ // Get the appropriate holon
726
+ const user = this.gun.user();
727
+
728
+ return new Promise((resolve) => {
729
+ let deletionPromises = [];
730
+
731
+ const dataPath = password ?
732
+ user.get('private').get(lens) :
733
+ this.gun.get(this.appname).get(holon).get(lens);
734
+
735
+ // First get all the data to find keys to delete
736
+ dataPath.once((data) => {
737
+ if (!data) {
738
+ resolve(true); // Nothing to delete
739
+ return;
740
+ }
724
741
 
725
- // First get all the data to find keys to delete
726
- this.gun.get(this.appname).get(holon).get(lens).once((data) => {
727
- if (!data) {
728
- resolve(true); // Nothing to delete
729
- return;
730
- }
742
+ // Get all keys except Gun's metadata key '_'
743
+ const keys = Object.keys(data).filter(key => key !== '_');
731
744
 
732
- // Get all keys except Gun's metadata key '_'
733
- const keys = Object.keys(data).filter(key => key !== '_');
745
+ // Create deletion promises for each key
746
+ keys.forEach(key => {
747
+ deletionPromises.push(
748
+ new Promise((resolveDelete) => {
749
+ const deletePath = password ?
750
+ user.get('private').get(lens).get(key) :
751
+ this.gun.get(this.appname).get(holon).get(lens).get(key);
734
752
 
735
- // Create deletion promises for each key
736
- keys.forEach(key => {
737
- deletionPromises.push(
738
- new Promise((resolveDelete) => {
739
- this.gun.get(this.appname).get(holon).get(lens).get(key).put(null, ack => {
740
- resolveDelete(!!ack.ok); // Convert to boolean
741
- });
753
+ deletePath.put(null, ack => {
754
+ resolveDelete(!!ack.ok); // Convert to boolean
755
+ });
756
+ })
757
+ );
758
+ });
759
+
760
+ // Wait for all deletions to complete
761
+ Promise.all(deletionPromises)
762
+ .then(results => {
763
+ const allSuccessful = results.every(result => result === true);
764
+ resolve(allSuccessful);
742
765
  })
743
- );
766
+ .catch(error => {
767
+ console.error('Error in deleteAll:', error);
768
+ resolve(false);
769
+ });
744
770
  });
745
-
746
- // Wait for all deletions to complete
747
- Promise.all(deletionPromises)
748
- .then(results => {
749
- const allSuccessful = results.every(result => result === true);
750
- resolve(allSuccessful);
751
- })
752
- .catch(error => {
753
- console.error('Error in deleteAll:', error);
754
- resolve(false);
755
- });
756
771
  });
757
- });
772
+ } catch (error) {
773
+ console.error('Error in deleteAll:', error);
774
+ return false;
775
+ }
758
776
  }
759
777
 
760
778
  // ================================ NODE FUNCTIONS ================================
@@ -871,243 +889,418 @@ class HoloSphere {
871
889
  * Stores data in a global (non-holon-specific) table.
872
890
  * @param {string} tableName - The table name to store data in.
873
891
  * @param {object} data - The data to store. If it has an 'id' field, it will be used as the key.
892
+ * @param {string} [password] - Optional password for private holon.
874
893
  * @returns {Promise<void>}
875
894
  */
876
- async putGlobal(tableName, data) {
877
- return new Promise((resolve, reject) => {
878
- try {
879
- if (!tableName || !data) {
880
- throw new Error('Table name and data are required');
881
- }
895
+ async putGlobal(tableName, data, password = null) {
896
+ try {
897
+ if (!tableName || !data) {
898
+ throw new Error('Table name and data are required');
899
+ }
882
900
 
883
- if (data.id) {
884
- this.gun.get(this.appname).get(tableName).get(data.id).put(JSON.stringify(data), ack => {
885
- if (ack.err) {
886
- reject(new Error(ack.err));
887
- } else {
888
- resolve();
889
- }
890
- });
891
- } else {
892
- this.gun.get(this.appname).get(tableName).put(JSON.stringify(data), ack => {
893
- if (ack.err) {
894
- reject(new Error(ack.err));
895
- } else {
896
- resolve();
897
- }
901
+ const user = this.gun.user();
902
+
903
+ 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
+ });
898
922
  });
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
+ }
899
958
  }
900
- } catch (error) {
901
- reject(error);
902
959
  }
903
- });
960
+
961
+ 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);
967
+
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);
988
+
989
+ if (data.id) {
990
+ path.get(data.id).put(payload, ack => {
991
+ if (ack.err) {
992
+ reject(new Error(ack.err));
993
+ } else {
994
+ resolve();
995
+ }
996
+ });
997
+ } else {
998
+ path.put(payload, ack => {
999
+ if (ack.err) {
1000
+ reject(new Error(ack.err));
1001
+ } else {
1002
+ resolve();
1003
+ }
1004
+ });
1005
+ }
1006
+ }
1007
+ });
1008
+ } catch (error) {
1009
+ console.error('Error in putGlobal:', error);
1010
+ throw error;
1011
+ }
904
1012
  }
905
1013
 
906
1014
  /**
907
- * Retrieves a specific key from a global table.
908
- * @param {string} tableName - The table name to retrieve from.
909
- * @param {string} key - The key to retrieve.
910
- * @returns {Promise<object|null>} - The parsed data for the key or null if not found.
911
- */
912
- async getGlobal(tableName, key) {
913
- return new Promise((resolve) => {
914
- this.gun.get(this.appname).get(tableName).get(key).once((data) => {
915
- if (!data) {
916
- resolve(null);
917
- return;
918
- }
1015
+ * Retrieves a specific key from a global table.
1016
+ * @param {string} tableName - The table name to retrieve from.
1017
+ * @param {string} key - The key to retrieve.
1018
+ * @param {string} [password] - Optional password for private holon.
1019
+ * @returns {Promise<object|null>} - The parsed data for the key or null if not found.
1020
+ */
1021
+ async getGlobal(tableName, key, password = null) {
1022
+ try {
1023
+ const user = this.gun.user();
1024
+
1025
+ if (password) {
919
1026
  try {
920
- const parsed = this.parse(data);
921
- resolve(parsed);
922
- } catch (e) {
923
- resolve(null);
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
+ });
1063
+ });
1064
+ }
1065
+ }
1066
+
1067
+ return new Promise((resolve) => {
1068
+ const handleData = (data) => {
1069
+ if (!data) {
1070
+ resolve(null);
1071
+ return;
1072
+ }
1073
+ try {
1074
+ const parsed = this.parse(data);
1075
+ resolve(parsed);
1076
+ } catch (e) {
1077
+ resolve(null);
1078
+ }
1079
+ };
1080
+
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);
924
1087
  }
925
1088
  });
926
- });
1089
+ } catch (error) {
1090
+ console.error('Error in getGlobal:', error);
1091
+ return null;
1092
+ }
927
1093
  }
928
1094
 
929
-
930
-
931
1095
  /**
932
1096
  * Retrieves all data from a global table.
933
1097
  * @param {string} tableName - The table name to retrieve data from.
934
- * @returns {Promise<object|null>} - The parsed data from the table or null if not found.
1098
+ * @param {string} [password] - Optional password for private holon.
1099
+ * @returns {Promise<Array<object>>} - The parsed data from the table as an array.
935
1100
  */
936
- async getAllGlobal(tableName) {
1101
+ async getAllGlobal(tableName, password = null) {
937
1102
  if (!tableName) {
938
1103
  throw new Error('getAllGlobal: Missing table name parameter');
939
1104
  }
940
1105
 
941
- return new Promise((resolve) => {
942
- let output = [];
943
- let isResolved = false;
944
- let timeout = setTimeout(() => {
945
- if (!isResolved) {
946
- isResolved = true;
947
- resolve(output);
948
- }
949
- }, 5000);
1106
+ try {
1107
+ // Get the appropriate holon
1108
+ const user = this.gun.user();
950
1109
 
951
- this.gun.get(this.appname).get(tableName).once(async (data) => {
952
- if (!data) {
953
- clearTimeout(timeout);
954
- isResolved = true;
955
- resolve([]);
956
- return;
957
- }
1110
+ return new Promise((resolve) => {
1111
+ let output = [];
1112
+ let isResolved = false;
1113
+ let timeout = setTimeout(() => {
1114
+ if (!isResolved) {
1115
+ isResolved = true;
1116
+ resolve(output);
1117
+ }
1118
+ }, 5000);
958
1119
 
959
- const keys = Object.keys(data).filter(key => key !== '_');
960
- const promises = keys.map(key =>
961
- new Promise(async (resolveItem) => {
962
- const itemData = await new Promise(resolveData => {
963
- this.gun.get(this.appname).get(tableName).get(key).once(resolveData);
964
- });
1120
+ const handleData = async (data) => {
1121
+ if (!data) {
1122
+ clearTimeout(timeout);
1123
+ isResolved = true;
1124
+ resolve([]);
1125
+ return;
1126
+ }
965
1127
 
966
- if (itemData) {
967
- try {
968
- const parsed = await this.parse(itemData);
969
- if (parsed) output.push(parsed);
970
- } catch (error) {
971
- console.error('Error parsing data:', error);
1128
+ const keys = Object.keys(data).filter(key => key !== '_');
1129
+ const promises = keys.map(key =>
1130
+ new Promise(async (resolveItem) => {
1131
+ const itemPath = password ?
1132
+ user.get('private').get(tableName).get(key) :
1133
+ this.gun.get(this.appname).get(tableName).get(key);
1134
+
1135
+ const itemData = await new Promise(resolveData => {
1136
+ itemPath.once(resolveData);
1137
+ });
1138
+
1139
+ if (itemData) {
1140
+ try {
1141
+ const parsed = await this.parse(itemData);
1142
+ if (parsed) output.push(parsed);
1143
+ } catch (error) {
1144
+ console.error('Error parsing data:', error);
1145
+ }
972
1146
  }
973
- }
974
- resolveItem();
975
- })
976
- );
977
-
978
- await Promise.all(promises);
979
- clearTimeout(timeout);
980
- if (!isResolved) {
981
- isResolved = true;
982
- resolve(output);
1147
+ resolveItem();
1148
+ })
1149
+ );
1150
+
1151
+ await Promise.all(promises);
1152
+ clearTimeout(timeout);
1153
+ if (!isResolved) {
1154
+ isResolved = true;
1155
+ resolve(output);
1156
+ }
1157
+ };
1158
+
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);
983
1165
  }
984
1166
  });
985
- });
1167
+ } catch (error) {
1168
+ console.error('Error in getAllGlobal:', error);
1169
+ return [];
1170
+ }
986
1171
  }
1172
+
987
1173
  /**
988
1174
  * Deletes a specific key from a global table.
989
1175
  * @param {string} tableName - The table name to delete from.
990
1176
  * @param {string} key - The key to delete.
991
- * @returns {Promise<void>}
1177
+ * @param {string} [password] - Optional password for private holon.
1178
+ * @returns {Promise<boolean>}
992
1179
  */
993
- async deleteGlobal(tableName, key) {
1180
+ async deleteGlobal(tableName, key, password = null) {
994
1181
  if (!tableName || !key) {
995
1182
  throw new Error('deleteGlobal: Missing required parameters');
996
1183
  }
997
1184
 
998
- // Only check authentication for non-spaces tables
999
- if (tableName !== 'spaces' && !this.currentSpace) {
1000
- throw new Error('Unauthorized to delete this data');
1001
- }
1002
-
1003
- // Skip session check for spaces table
1004
- if (tableName !== 'spaces') {
1005
- this._checkSession();
1006
- }
1185
+ try {
1186
+ // Get the appropriate holon
1187
+ const user = this.gun.user();
1007
1188
 
1008
- return new Promise((resolve, reject) => {
1009
- try {
1010
- this.gun.get(this.appname)
1011
- .get(tableName)
1012
- .get(key)
1013
- .put(null, ack => {
1189
+ 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 => {
1014
1193
  if (ack.err) {
1015
1194
  reject(new Error(ack.err));
1016
1195
  } else {
1017
1196
  resolve(true);
1018
1197
  }
1019
1198
  });
1020
- } catch (error) {
1021
- reject(error);
1022
- }
1023
- });
1199
+ } else {
1200
+ // For public data, use the regular path
1201
+ this.gun.get(this.appname).get(tableName).get(key).put(null, ack => {
1202
+ if (ack.err) {
1203
+ reject(new Error(ack.err));
1204
+ } else {
1205
+ resolve(true);
1206
+ }
1207
+ });
1208
+ }
1209
+ });
1210
+ } catch (error) {
1211
+ console.error('Error in deleteGlobal:', error);
1212
+ throw error;
1213
+ }
1024
1214
  }
1025
1215
 
1026
1216
  /**
1027
1217
  * Deletes an entire global table.
1028
1218
  * @param {string} tableName - The table name to delete.
1029
- * @returns {Promise<void>}
1219
+ * @param {string} [password] - Optional password for private holon.
1220
+ * @returns {Promise<boolean>}
1030
1221
  */
1031
- async deleteAllGlobal(tableName) {
1222
+ async deleteAllGlobal(tableName, password = null) {
1032
1223
  if (!tableName) {
1033
1224
  throw new Error('deleteAllGlobal: Missing table name parameter');
1034
1225
  }
1035
1226
 
1036
- // Only check authentication for non-spaces and non-federation tables
1037
- if (!['spaces', 'federation'].includes(tableName) && !this.currentSpace) {
1038
- throw new Error('Unauthorized to delete this data');
1039
- }
1227
+ try {
1228
+ // Get the appropriate holon
1229
+ const user = this.gun.user();
1040
1230
 
1041
- // Skip session check for spaces and federation tables
1042
- if (!['spaces', 'federation'].includes(tableName)) {
1043
- this._checkSession();
1044
- }
1231
+ return new Promise((resolve, reject) => {
1232
+ try {
1233
+ const deletions = new Set();
1234
+ let timeout = setTimeout(() => {
1235
+ if (deletions.size === 0) {
1236
+ resolve(true); // No data to delete
1237
+ }
1238
+ }, 5000);
1045
1239
 
1046
- return new Promise((resolve, reject) => {
1047
- try {
1048
- const deletions = new Set();
1049
- let timeout = setTimeout(() => {
1050
- if (deletions.size === 0) {
1051
- resolve(true); // No data to delete
1052
- }
1053
- }, 5000);
1240
+ const dataPath = password ?
1241
+ user.get('private').get(tableName) :
1242
+ this.gun.get(this.appname).get(tableName);
1054
1243
 
1055
- this.gun.get(this.appname).get(tableName).once(async (data) => {
1056
- if (!data) {
1057
- clearTimeout(timeout);
1058
- resolve(true);
1059
- return;
1060
- }
1244
+ dataPath.once(async (data) => {
1245
+ if (!data) {
1246
+ clearTimeout(timeout);
1247
+ resolve(true);
1248
+ return;
1249
+ }
1061
1250
 
1062
- const keys = Object.keys(data).filter(key => key !== '_');
1063
- const promises = keys.map(key =>
1064
- new Promise((resolveDelete) => {
1065
- this.gun.get(this.appname)
1066
- .get(tableName)
1067
- .get(key)
1068
- .put(null, ack => {
1251
+ const keys = Object.keys(data).filter(key => key !== '_');
1252
+ const promises = keys.map(key =>
1253
+ new Promise((resolveDelete) => {
1254
+ const deletePath = password ?
1255
+ user.get('private').get(tableName).get(key) :
1256
+ this.gun.get(this.appname).get(tableName).get(key);
1257
+
1258
+ deletePath.put(null, ack => {
1069
1259
  if (ack.err) {
1070
1260
  console.error(`Failed to delete ${key}:`, ack.err);
1071
1261
  }
1072
1262
  resolveDelete();
1073
1263
  });
1074
- })
1075
- );
1076
-
1077
- try {
1078
- await Promise.all(promises);
1079
- // Finally delete the table itself
1080
- this.gun.get(this.appname).get(tableName).put(null);
1081
- clearTimeout(timeout);
1082
- resolve(true);
1083
- } catch (error) {
1084
- reject(error);
1085
- }
1086
- });
1087
- } catch (error) {
1088
- reject(error);
1089
- }
1090
- });
1264
+ })
1265
+ );
1266
+
1267
+ try {
1268
+ await Promise.all(promises);
1269
+ // Finally delete the table itself
1270
+ dataPath.put(null);
1271
+ clearTimeout(timeout);
1272
+ resolve(true);
1273
+ } catch (error) {
1274
+ reject(error);
1275
+ }
1276
+ });
1277
+ } catch (error) {
1278
+ reject(error);
1279
+ }
1280
+ });
1281
+ } catch (error) {
1282
+ console.error('Error in deleteAllGlobal:', error);
1283
+ throw error;
1284
+ }
1091
1285
  }
1092
1286
 
1093
1287
  // ================================ COMPUTE FUNCTIONS ================================
1094
1288
  /**
1095
-
1096
- /**
1097
- * Computes operations across multiple layers up the hierarchy
1098
- * @param {string} holon - Starting holon identifier
1099
- * @param {string} lens - The lens to compute
1100
- * @param {object} options - Computation options
1101
- * @param {number} [maxLevels=15] - Maximum levels to compute up
1102
- */
1103
- async computeHierarchy(holon, lens, options, maxLevels = 15) {
1289
+ * Computes operations across multiple layers up the hierarchy
1290
+ * @param {string} holon - Starting holon identifier
1291
+ * @param {string} lens - The lens to compute
1292
+ * @param {object} options - Computation options
1293
+ * @param {number} [maxLevels=15] - Maximum levels to compute up
1294
+ * @param {string} [password] - Optional password for private holons
1295
+ */
1296
+ async computeHierarchy(holon, lens, options, maxLevels = 15, password = null) {
1104
1297
  let currentHolon = holon;
1105
1298
  let currentRes = h3.getResolution(currentHolon);
1106
1299
  const results = [];
1107
1300
 
1108
1301
  while (currentRes > 0 && maxLevels > 0) {
1109
1302
  try {
1110
- const result = await this.compute(currentHolon, lens, options);
1303
+ const result = await this.compute(currentHolon, lens, options, password);
1111
1304
  if (result) {
1112
1305
  results.push(result);
1113
1306
  }
@@ -1123,16 +1316,18 @@ class HoloSphere {
1123
1316
  return results;
1124
1317
  }
1125
1318
 
1126
- /* Computes operations on content within a holon and lens for one layer up.
1319
+ /**
1320
+ * Computes operations on content within a holon and lens for one layer up.
1127
1321
  * @param {string} holon - The holon identifier.
1128
1322
  * @param {string} lens - The lens to compute.
1129
1323
  * @param {object} options - Computation options
1130
1324
  * @param {string} options.operation - The operation to perform ('summarize', 'aggregate', 'concatenate')
1131
1325
  * @param {string[]} [options.fields] - Fields to perform operation on
1132
1326
  * @param {string} [options.targetField] - Field to store the result in
1327
+ * @param {string} [password] - Optional password for private holons
1133
1328
  * @throws {Error} If parameters are invalid or missing
1134
1329
  */
1135
- async compute(holon, lens, options) {
1330
+ async compute(holon, lens, options, password = null) {
1136
1331
  // Validate required parameters
1137
1332
  if (!holon || !lens) {
1138
1333
  throw new Error('compute: Missing required parameters');
@@ -1187,7 +1382,7 @@ class HoloSphere {
1187
1382
 
1188
1383
  // Collect all content from siblings
1189
1384
  const contents = await Promise.all(
1190
- siblings.map(sibling => this.getAll(sibling, lens))
1385
+ siblings.map(sibling => this.getAll(sibling, lens, password))
1191
1386
  );
1192
1387
 
1193
1388
  const flatContents = contents.flat().filter(Boolean);
@@ -1249,7 +1444,7 @@ class HoloSphere {
1249
1444
  result.value = computed;
1250
1445
  }
1251
1446
 
1252
- await this.put(parent, lens, result);
1447
+ await this.put(parent, lens, result, password);
1253
1448
  return result;
1254
1449
  }
1255
1450
  } catch (error) {
@@ -1296,27 +1491,52 @@ class HoloSphere {
1296
1491
  }
1297
1492
 
1298
1493
  /**
1299
- * Upcasts content to parent holonagons recursively.
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.
1300
1496
  * @param {string} holon - The current holon identifier.
1301
1497
  * @param {string} lens - The lens under which to upcast.
1302
1498
  * @param {object} content - The content to upcast.
1303
- * @returns {Promise<object>} - The upcasted content.
1499
+ * @param {number} [maxLevels=15] - Maximum levels to upcast.
1500
+ * @returns {Promise<object>} - The original content.
1304
1501
  */
1305
- async upcast(holon, lens, content) {
1306
- let res = h3.getResolution(holon)
1307
- if (res == 0) {
1308
- await this.put(holon, lens, content)
1309
- return content
1502
+ async upcast(holon, lens, content, maxLevels = 15) {
1503
+ // Store the actual content at the original resolution
1504
+ await this.put(holon, lens, content);
1505
+
1506
+ let res = h3.getResolution(holon);
1507
+
1508
+ // If already at the highest level (res 0) or reached max levels, we're done
1509
+ if (res === 0 || maxLevels <= 0) {
1510
+ return content;
1310
1511
  }
1311
- else {
1312
- console.log('Upcasting ', holon, lens, content, res)
1313
- await this.put(holon, lens, content)
1314
- let parent = h3.cellToParent(holon, res - 1)
1315
- return this.upcast(parent, lens, content)
1512
+
1513
+ // Get the parent cell
1514
+ let parent = h3.cellToParent(holon, res - 1);
1515
+
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
+ };
1525
+
1526
+ // Store the reference in the parent cell
1527
+ // We use { autoPropagate: false } to prevent circular propagation
1528
+ await this.put(parent, lens, reference, null, {
1529
+ autoPropagate: false
1530
+ });
1531
+
1532
+ // Continue upcasting with the parent
1533
+ if (res > 1 && maxLevels > 1) {
1534
+ return this.upcast(parent, lens, reference, maxLevels - 1);
1316
1535
  }
1536
+
1537
+ return content;
1317
1538
  }
1318
1539
 
1319
-
1320
1540
  /**
1321
1541
  * Updates the parent holon with a new report.
1322
1542
  * @param {string} id - The child holon identifier.
@@ -1384,35 +1604,88 @@ class HoloSphere {
1384
1604
  * @param {string} holon - The holon identifier.
1385
1605
  * @param {string} lens - The lens to subscribe to.
1386
1606
  * @param {function} callback - The callback to execute on changes.
1607
+ * @returns {Promise<object>} - Subscription object with unsubscribe method
1387
1608
  */
1388
1609
  async subscribe(holon, lens, callback) {
1610
+ if (!holon || !lens || typeof callback !== 'function') {
1611
+ throw new Error('subscribe: Missing required parameters');
1612
+ }
1613
+
1389
1614
  const subscriptionId = this.generateId();
1390
- this.subscriptions[subscriptionId] =
1391
- this.gun.get(this.appname).get(holon).get(lens).map().on( async (data, key) => {
1615
+
1616
+ try {
1617
+ // Create the subscription
1618
+ const gunSubscription = this.gun.get(this.appname).get(holon).get(lens).map().on(async (data, key) => {
1392
1619
  if (data) {
1393
1620
  try {
1394
- let parsed = await this.parse(data)
1395
- callback(parsed, key)
1621
+ let parsed = await this.parse(data);
1622
+ callback(parsed, key);
1396
1623
  } catch (error) {
1397
1624
  console.error('Error in subscribe:', error);
1398
1625
  }
1399
1626
  }
1400
- })
1401
- return {
1402
- unsubscribe: () => {
1403
- this.gun.get(this.appname).get(holon).get(lens).map().off()
1404
- delete this.subscriptions[subscriptionId];
1405
- }
1627
+ });
1628
+
1629
+ // Store the subscription with its ID
1630
+ this.subscriptions[subscriptionId] = {
1631
+ id: subscriptionId,
1632
+ holon,
1633
+ lens,
1634
+ active: true,
1635
+ gunSubscription
1636
+ };
1637
+
1638
+ // Return an object with unsubscribe method
1639
+ return {
1640
+ unsubscribe: () => {
1641
+ 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
1646
+ if (this.subscriptions[subscriptionId]) {
1647
+ this.subscriptions[subscriptionId].active = false;
1648
+ delete this.subscriptions[subscriptionId];
1649
+ }
1650
+ } catch (error) {
1651
+ console.error('Error in unsubscribe:', error);
1652
+ }
1653
+ }
1654
+ };
1655
+ } catch (error) {
1656
+ console.error('Error creating subscription:', error);
1657
+ throw error;
1406
1658
  }
1407
1659
  }
1408
1660
 
1409
1661
 
1662
+ /**
1663
+ * Notifies subscribers about data changes
1664
+ * @param {object} data - The data to notify about
1665
+ * @private
1666
+ */
1410
1667
  notifySubscribers(data) {
1411
- Object.values(this.subscriptions).forEach(subscription => {
1412
- if (subscription.active && this.matchesQuery(data, subscription.query)) {
1413
- subscription.callback(data);
1414
- }
1415
- });
1668
+ if (!data || !data.holon || !data.lens) {
1669
+ return;
1670
+ }
1671
+
1672
+ try {
1673
+ Object.values(this.subscriptions).forEach(subscription => {
1674
+ if (subscription.active &&
1675
+ subscription.holon === data.holon &&
1676
+ subscription.lens === data.lens) {
1677
+ try {
1678
+ if (subscription.callback && typeof subscription.callback === 'function') {
1679
+ subscription.callback(data);
1680
+ }
1681
+ } catch (error) {
1682
+ console.warn('Error in subscription callback:', error);
1683
+ }
1684
+ }
1685
+ });
1686
+ } catch (error) {
1687
+ console.warn('Error notifying subscribers:', error);
1688
+ }
1416
1689
  }
1417
1690
 
1418
1691
  // Add ID generation method
@@ -1420,407 +1693,219 @@ class HoloSphere {
1420
1693
  return Date.now().toString(10) + Math.random().toString(2);
1421
1694
  }
1422
1695
 
1423
- matchesQuery(data, query) {
1424
- return data && query &&
1425
- data.holon === query.holon &&
1426
- data.lens === query.lens;
1427
- }
1696
+ // ================================ FEDERATION FUNCTIONS ================================
1428
1697
 
1429
1698
  /**
1430
- * Creates a new space with the given credentials
1431
- * @param {string} spacename - The space identifier/username
1432
- * @param {string} password - The space password
1433
- * @returns {Promise<boolean>} - True if space was created successfully
1699
+ * Creates a federation relationship between two holons
1700
+ * @param {string} holonId1 - The first holon ID
1701
+ * @param {string} holonId2 - The second holon ID
1702
+ * @param {string} password1 - Password for the first holon
1703
+ * @param {string} [password2] - Optional password for the second holon
1704
+ * @param {boolean} [bidirectional=true] - Whether to set up bidirectional notifications automatically
1705
+ * @returns {Promise<boolean>} - True if federation was created successfully
1434
1706
  */
1435
- async createSpace(spacename, password) {
1436
- if (!spacename || !password) {
1437
- throw new Error('Invalid credentials format');
1438
- }
1439
-
1440
- // Check if space already exists
1441
- const existingSpace = await this.getGlobal('spaces', spacename);
1442
- if (existingSpace) {
1443
- throw new Error('Space already exists');
1444
- }
1445
-
1446
- try {
1447
- // Generate key pair
1448
- const pair = await Gun.SEA.pair();
1449
-
1450
- // Create auth record with SEA
1451
- const salt = await Gun.SEA.random(64).toString('base64');
1452
- const hash = await Gun.SEA.work(password, salt);
1453
- const auth = {
1454
- salt: salt,
1455
- hash: hash,
1456
- pub: pair.pub
1457
- };
1458
-
1459
- // Create space record with encrypted data
1460
- const space = {
1461
- alias: spacename,
1462
- auth: auth,
1463
- epub: pair.epub,
1464
- pub: pair.pub,
1465
- created: Date.now()
1466
- };
1467
-
1468
- await this.putGlobal('spaces', {
1469
- ...space,
1470
- id: spacename
1471
- });
1472
-
1473
- return true;
1474
- } catch (error) {
1475
- throw new Error(`Space creation failed: ${error.message}`);
1476
- }
1707
+ async federate(holonId1, holonId2, password1, password2 = null, bidirectional = true) {
1708
+ return Federation.federate(this, holonId1, holonId2, password1, password2, bidirectional);
1477
1709
  }
1478
1710
 
1479
1711
  /**
1480
- * Logs in to a space with the given credentials
1481
- * @param {string} spacename - The space identifier/username
1482
- * @param {string} password - The space password
1483
- * @returns {Promise<boolean>} - True if login was successful
1712
+ * Subscribes to federation notifications for a holon
1713
+ * @param {string} holonId - The holon ID to subscribe to
1714
+ * @param {string} password - Password for the holon
1715
+ * @param {function} callback - The callback to execute on notifications
1716
+ * @param {object} [options] - Subscription options
1717
+ * @param {string[]} [options.lenses] - Specific lenses to subscribe to (default: all)
1718
+ * @param {number} [options.throttle] - Throttle notifications in ms (default: 0)
1719
+ * @returns {Promise<object>} - Subscription object with unsubscribe() method
1484
1720
  */
1485
- async login(spacename, password) {
1486
- // Validate input
1487
- if (!spacename || !password ||
1488
- typeof spacename !== 'string' ||
1489
- typeof password !== 'string') {
1490
- throw new Error('Invalid credentials format');
1491
- }
1492
-
1493
- try {
1494
- // Get space record
1495
- const space = await this.getGlobal('spaces', spacename);
1496
- if (!space || !space.auth) {
1497
- throw new Error('Invalid spacename or password');
1498
- }
1499
-
1500
- // Verify password using SEA
1501
- const hash = await Gun.SEA.work(password, space.auth.salt);
1502
- if (hash !== space.auth.hash) {
1503
- throw new Error('Invalid spacename or password');
1504
- }
1505
-
1506
- // Set current space with expiration
1507
- this.currentSpace = {
1508
- ...space,
1509
- exp: Date.now() + (24 * 60 * 60 * 1000) // 24 hour expiration
1510
- };
1511
-
1512
- return true;
1513
- } catch (error) {
1514
- throw new Error('Authentication failed');
1515
- }
1721
+ async subscribeFederation(holonId, password, callback, options = {}) {
1722
+ return Federation.subscribeFederation(this, holonId, password, callback, options);
1516
1723
  }
1517
1724
 
1518
1725
  /**
1519
- * Logs out the current space
1520
- * @returns {Promise<void>}
1726
+ * Gets federation info for a holon
1727
+ * @param {string} holonId - The holon ID
1728
+ * @param {string} [password] - Optional password for the holon
1729
+ * @returns {Promise<object|null>} - Federation info or null if not found
1521
1730
  */
1522
- async logout() {
1523
- this.currentSpace = null;
1731
+ async getFederation(holonId, password = null) {
1732
+ return Federation.getFederation(this, holonId, password);
1524
1733
  }
1525
1734
 
1526
1735
  /**
1527
- * Checks if the current session is valid
1528
- * @private
1736
+ * Removes a federation relationship between holons
1737
+ * @param {string} holonId1 - The first holon ID
1738
+ * @param {string} holonId2 - The second holon ID
1739
+ * @param {string} password1 - Password for the first holon
1740
+ * @param {string} [password2] - Optional password for the second holon
1741
+ * @returns {Promise<boolean>} - True if federation was removed successfully
1529
1742
  */
1530
- _checkSession() {
1531
- if (!this.currentSpace) {
1532
- throw new Error('No active session');
1533
- }
1534
- if (this.currentSpace.exp < Date.now()) {
1535
- this.currentSpace = null;
1536
- throw new Error('Session expired');
1537
- }
1538
- return true;
1743
+ async unfederate(holonId1, holonId2, password1, password2 = null) {
1744
+ return await Federation.unfederate(this, holonId1, holonId2, password1, password2);
1539
1745
  }
1540
1746
 
1541
1747
  /**
1542
- * Creates a federation relationship between two spaces
1543
- * @param {string} spaceId1 - The first space ID
1544
- * @param {string} spaceId2 - The second space ID
1545
- * @returns {Promise<boolean>} - True if federation was created successfully
1748
+ * Removes a notification relationship between two spaces
1749
+ * This removes spaceId2 from the notify list of spaceId1
1750
+ *
1751
+ * @param {string} holonId1 - The space to modify (remove from its notify list)
1752
+ * @param {string} holonId2 - The space to be removed from notifications
1753
+ * @param {string} [password1] - Optional password for the first space
1754
+ * @returns {Promise<boolean>} - True if notification was removed successfully
1546
1755
  */
1547
- async federate(spaceId1, spaceId2) {
1548
- if (!spaceId1 || !spaceId2) {
1549
- throw new Error('federate: Missing required parameters');
1550
- }
1551
-
1552
- // Get existing federation info for both spaces
1553
- let fedInfo1 = await this.getGlobal('federation', spaceId1);
1554
- let fedInfo2 = await this.getGlobal('federation', spaceId2);
1555
-
1556
- // Check if federation already exists
1557
- if (fedInfo1 && fedInfo1.federation && fedInfo1.federation.includes(spaceId2)) {
1558
- throw new Error('Federation already exists');
1559
- }
1560
-
1561
- // Create or update federation info for first space
1562
- if (!fedInfo1) {
1563
- fedInfo1 = {
1564
- id: spaceId1,
1565
- name: spaceId1,
1566
- federation: [],
1567
- notify: []
1568
- };
1569
- }
1570
- if (!fedInfo1.federation) fedInfo1.federation = [];
1571
- fedInfo1.federation.push(spaceId2);
1572
-
1573
- // Create or update federation info for second space
1574
- if (!fedInfo2) {
1575
- fedInfo2 = {
1576
- id: spaceId2,
1577
- name: spaceId2,
1578
- federation: [],
1579
- notify: []
1580
- };
1756
+ async removeNotify(holonId1, holonId2, password1 = null) {
1757
+ console.log(`HoloSphere.removeNotify called: ${holonId1}, ${holonId2}`);
1758
+ try {
1759
+ const result = await Federation.removeNotify(this, holonId1, holonId2, password1);
1760
+ console.log(`HoloSphere.removeNotify completed successfully: ${result}`);
1761
+ return result;
1762
+ } catch (error) {
1763
+ console.error(`HoloSphere.removeNotify failed:`, error);
1764
+ throw error;
1581
1765
  }
1582
- if (!fedInfo2.notify) fedInfo2.notify = [];
1583
- fedInfo2.notify.push(spaceId1);
1584
-
1585
- // Save both federation records
1586
- await this.putGlobal('federation', fedInfo1);
1587
- await this.putGlobal('federation', fedInfo2);
1588
-
1589
- return true;
1590
1766
  }
1591
1767
 
1592
1768
  /**
1593
- * Subscribes to federation notifications for a space
1594
- * @param {string} spaceId - The space ID to subscribe to
1595
- * @param {function} callback - The callback to execute on notifications
1596
- * @returns {Promise<object>} - Subscription object with off() method
1769
+ * Get and aggregate data from federated holons
1770
+ * @param {string} holon The holon name
1771
+ * @param {string} lens The lens name
1772
+ * @param {Object} options Options for retrieval and aggregation
1773
+ * @returns {Promise<Array>} Combined array of local and federated data
1597
1774
  */
1598
- async subscribeFederation(spaceId, callback) {
1599
- if (!spaceId || !callback) {
1600
- throw new Error('subscribeFederation: Missing required parameters');
1601
- }
1602
-
1603
- // Get federation info
1604
- const fedInfo = await this.getGlobal('federation', spaceId);
1605
- if (!fedInfo) {
1606
- throw new Error('No federation info found for space');
1607
- }
1608
-
1609
- // Create subscription for each federated space
1610
- const subscriptions = [];
1611
- if (fedInfo.federation && fedInfo.federation.length > 0) {
1612
- for (const federatedSpace of fedInfo.federation) {
1613
- // Subscribe to all lenses in the federated space
1614
- const sub = await this.subscribe(federatedSpace, '*', async (data) => {
1615
- try {
1616
- // Only notify if the data has federation info and is from the federated space
1617
- if (data && data.federation && data.federation.origin === federatedSpace) {
1618
- await callback(data);
1619
- }
1620
- } catch (error) {
1621
- console.warn('Federation notification error:', error);
1622
- }
1623
- });
1624
- subscriptions.push(sub);
1625
- }
1626
- }
1627
-
1628
- // Return combined subscription object
1629
- return {
1630
- off: () => {
1631
- subscriptions.forEach(sub => {
1632
- if (sub && typeof sub.off === 'function') {
1633
- sub.off();
1634
- }
1635
- });
1636
- }
1637
- };
1775
+ async getFederated(holon, lens, options = {}) {
1776
+ return Federation.getFederated(this, holon, lens, options);
1638
1777
  }
1639
1778
 
1640
1779
  /**
1641
- * Gets federation info for a space
1642
- * @param {string} spaceId - The space ID
1643
- * @returns {Promise<object|null>} - Federation info or null if not found
1780
+ * Tracks a federated message across different chats
1781
+ * @param {string} originalChatId - The ID of the original chat
1782
+ * @param {string} messageId - The ID of the original message
1783
+ * @param {string} federatedChatId - The ID of the federated chat
1784
+ * @param {string} federatedMessageId - The ID of the message in the federated chat
1785
+ * @param {string} type - The type of message (e.g., 'quest', 'announcement')
1786
+ * @returns {Promise<void>}
1644
1787
  */
1645
- async getFederation(spaceId) {
1646
- if (!spaceId) {
1647
- throw new Error('getFederationInfo: Missing space ID');
1648
- }
1649
- return await this.getGlobal('federation', spaceId);
1788
+ async federateMessage(originalChatId, messageId, federatedChatId, federatedMessageId, type = 'generic') {
1789
+ return Federation.federateMessage(this, originalChatId, messageId, federatedChatId, federatedMessageId, type);
1650
1790
  }
1651
1791
 
1652
1792
  /**
1653
- * Removes a federation relationship between spaces
1654
- * @param {string} spaceId1 - The first space ID
1655
- * @param {string} spaceId2 - The second space ID
1656
- * @returns {Promise<boolean>} - True if federation was removed successfully
1793
+ * Gets all federated messages for a given original message
1794
+ * @param {string} originalChatId - The ID of the original chat
1795
+ * @param {string} messageId - The ID of the original message
1796
+ * @returns {Promise<Object|null>} The tracking information for the message
1657
1797
  */
1658
- async unfederate(spaceId1, spaceId2) {
1659
- if (!spaceId1 || !spaceId2) {
1660
- throw new Error('unfederate: Missing required parameters');
1661
- }
1662
-
1663
- // Get federation info for both spaces
1664
- const fedInfo1 = await this.getGlobal('federation', spaceId1);
1665
- const fedInfo2 = await this.getGlobal('federation', spaceId2);
1666
-
1667
- if (fedInfo1) {
1668
- fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
1669
- await this.putGlobal('federation', fedInfo1);
1670
- }
1671
-
1672
- if (fedInfo2) {
1673
- fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
1674
- await this.putGlobal('federation', fedInfo2);
1675
- }
1676
-
1677
- return true;
1798
+ async getFederatedMessages(originalChatId, messageId) {
1799
+ return Federation.getFederatedMessages(this, originalChatId, messageId);
1678
1800
  }
1679
1801
 
1680
1802
  /**
1681
- * Gets the name of a chat/space
1682
- * @param {string} spaceId - The space ID
1683
- * @returns {Promise<string>} - The space name or the ID if not found
1803
+ * Updates a federated message across all federated chats
1804
+ * @param {string} originalChatId - The ID of the original chat
1805
+ * @param {string} messageId - The ID of the original message
1806
+ * @param {Function} updateCallback - Function to update the message in each chat
1807
+ * @returns {Promise<void>}
1684
1808
  */
1685
- async getChatName(spaceId) {
1686
- const spaceInfo = await this.getGlobal('spaces', spaceId);
1687
- return spaceInfo?.name || spaceId;
1809
+ async updateFederatedMessages(originalChatId, messageId, updateCallback) {
1810
+ return Federation.updateFederatedMessages(this, originalChatId, messageId, updateCallback);
1688
1811
  }
1689
1812
 
1690
1813
  /**
1691
- * Gets data from a holon and lens, including data from federated spaces with optional aggregation
1692
- * @param {string} holon - The holon identifier
1693
- * @param {string} lens - The lens identifier
1694
- * @param {object} options - Options for data retrieval and aggregation
1695
- * @param {boolean} options.aggregate - Whether to aggregate items with matching IDs (default: false)
1696
- * @param {string} options.idField - Field to use as identifier for aggregation (default: 'id')
1697
- * @param {string[]} options.sumFields - Numeric fields to sum during aggregation (e.g., ['received', 'sent'])
1698
- * @param {string[]} options.concatArrays - Array fields to concatenate during aggregation (e.g., ['wants', 'offers'])
1699
- * @param {boolean} options.removeDuplicates - Whether to remove duplicates when not aggregating (default: true)
1700
- * @param {function} options.mergeStrategy - Custom function to merge items during aggregation
1701
- * @returns {Promise<Array>} - Combined array of local and federated data
1814
+ * Resets the federation settings for a holon
1815
+ * @param {string} holonId - The holon ID
1816
+ * @param {string} [password] - Optional password for the holon
1817
+ * @returns {Promise<boolean>} - True if federation was reset successfully
1702
1818
  */
1703
- async getFederated(holon, lens, options = {}) {
1704
- // Validate required parameters
1705
- if (!holon || !lens) {
1706
- throw new Error('getFederated: Missing required parameters');
1707
- }
1708
-
1709
- const {
1710
- aggregate = false,
1711
- idField = 'id',
1712
- sumFields = [],
1713
- concatArrays = [],
1714
- removeDuplicates = true,
1715
- mergeStrategy = null
1716
- } = options;
1717
-
1718
- // Get federation info for current space
1719
- const fedInfo = await this.getFederation(this.currentSpace?.alias);
1720
-
1721
- // Get local data
1722
- const localData = await this.getAll(holon, lens);
1723
-
1724
- // If no federation or not authenticated, return local data only
1725
- if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
1726
- return localData;
1727
- }
1728
-
1729
- // Get data from each federated space
1730
- const federatedData = await Promise.all(
1731
- fedInfo.federation.map(async (federatedSpace) => {
1732
- try {
1733
- const data = await this.getAll(federatedSpace, lens);
1734
- return data || [];
1735
- } catch (error) {
1736
- console.warn(`Error getting data from federated space ${federatedSpace}:`, error);
1737
- return [];
1738
- }
1739
- })
1740
- );
1741
-
1742
- // Combine all data
1743
- const allData = [...localData, ...federatedData.flat()];
1744
-
1745
- // If aggregating, use enhanced aggregation logic
1746
- if (aggregate) {
1747
- const aggregated = new Map();
1748
-
1749
- for (const item of allData) {
1750
- const itemId = item[idField];
1751
- if (!itemId) continue;
1752
-
1753
- const existing = aggregated.get(itemId);
1754
- if (!existing) {
1755
- aggregated.set(itemId, { ...item });
1756
- } else {
1757
- // If custom merge strategy is provided, use it
1758
- if (mergeStrategy && typeof mergeStrategy === 'function') {
1759
- aggregated.set(itemId, mergeStrategy(existing, item));
1760
- continue;
1761
- }
1762
-
1763
- // Enhanced default merge strategy
1764
- const merged = { ...existing };
1819
+ async resetFederation(holonId, password = null) {
1820
+ return Federation.resetFederation(this, holonId, password);
1821
+ }
1765
1822
 
1766
- // Sum numeric fields
1767
- for (const field of sumFields) {
1768
- if (typeof item[field] === 'number') {
1769
- merged[field] = (merged[field] || 0) + (item[field] || 0);
1823
+ // ================================ END FEDERATION FUNCTIONS ================================
1824
+ /**
1825
+ * Closes the HoloSphere instance and cleans up resources.
1826
+ * @returns {Promise<void>}
1827
+ */
1828
+ async close() {
1829
+ try {
1830
+ if (this.gun) {
1831
+ // Unsubscribe from all subscriptions
1832
+ const subscriptionIds = Object.keys(this.subscriptions);
1833
+ for (const id of subscriptionIds) {
1834
+ try {
1835
+ const subscription = this.subscriptions[id];
1836
+ 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();
1842
+
1843
+ // Mark as inactive
1844
+ subscription.active = false;
1770
1845
  }
1846
+ } catch (error) {
1847
+ console.warn(`Error cleaning up subscription ${id}:`, error);
1771
1848
  }
1849
+ }
1772
1850
 
1773
- // Concatenate and deduplicate array fields
1774
- for (const field of concatArrays) {
1775
- if (Array.isArray(item[field])) {
1776
- const combinedArray = [
1777
- ...(merged[field] || []),
1778
- ...(item[field] || [])
1779
- ];
1780
- // Remove duplicates if elements are primitive
1781
- merged[field] = Array.from(new Set(combinedArray));
1851
+ // Clear subscriptions
1852
+ this.subscriptions = {};
1853
+
1854
+ // Close Gun connections
1855
+ if (this.gun.back) {
1856
+ try {
1857
+ 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] = [];
1866
+ }
1867
+ }
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 = {};
1873
+ }
1874
+ } catch (meshError) {
1875
+ console.warn('Error cleaning up Gun mesh hear:', meshError);
1876
+ }
1782
1877
  }
1878
+ } catch (error) {
1879
+ console.warn('Error accessing Gun mesh:', error);
1783
1880
  }
1881
+ }
1784
1882
 
1785
- // Update federation metadata
1786
- merged.federation = {
1787
- ...merged.federation,
1788
- timestamp: Math.max(
1789
- merged.federation?.timestamp || 0,
1790
- item.federation?.timestamp || 0
1791
- ),
1792
- origins: Array.from(new Set([
1793
- ...(merged.federation?.origins || [merged.federation?.origin]),
1794
- ...(item.federation?.origins || [item.federation?.origin])
1795
- ]).filter(Boolean))
1796
- };
1797
-
1798
- // Update the aggregated item
1799
- aggregated.set(itemId, merged);
1883
+ // Clear all Gun instance listeners
1884
+ try {
1885
+ this.gun.off();
1886
+ } catch (error) {
1887
+ console.warn('Error turning off Gun listeners:', error);
1800
1888
  }
1889
+
1890
+ // Wait a moment for cleanup to complete
1891
+ await new Promise(resolve => setTimeout(resolve, 100));
1801
1892
  }
1802
-
1803
- return Array.from(aggregated.values());
1804
- }
1805
-
1806
- // If not aggregating, optionally remove duplicates based on idField
1807
- if (!removeDuplicates) {
1808
- return allData;
1893
+
1894
+ console.log('HoloSphere instance closed successfully');
1895
+ } catch (error) {
1896
+ console.error('Error closing HoloSphere instance:', error);
1809
1897
  }
1898
+ }
1810
1899
 
1811
- // Remove duplicates keeping the most recent version
1812
- const uniqueMap = new Map();
1813
- allData.forEach(item => {
1814
- const id = item[idField];
1815
- if (!id) return;
1816
-
1817
- const existing = uniqueMap.get(id);
1818
- if (!existing ||
1819
- (item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
1820
- uniqueMap.set(id, item);
1821
- }
1822
- });
1823
- return Array.from(uniqueMap.values());
1900
+ /**
1901
+ * Creates a namespaced username for Gun authentication
1902
+ * @private
1903
+ * @param {string} holonId - The holon ID
1904
+ * @returns {string} - Namespaced username
1905
+ */
1906
+ userName(holonId) {
1907
+ if (!holonId) return null;
1908
+ return `${this.appname}:${holonId}`;
1824
1909
  }
1825
1910
  }
1826
1911