holosphere 1.1.5 → 1.1.6

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,6 +3,7 @@ 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
 
7
8
 
8
9
  class HoloSphere {
@@ -14,7 +15,7 @@ class HoloSphere {
14
15
  * @param {Gun|null} gunInstance - The Gun instance to use.
15
16
  */
16
17
  constructor(appname, strict = false, openaikey = null, gunInstance = null) {
17
- console.log('HoloSphere v1.1.8');
18
+ console.log('HoloSphere v1.1.6');
18
19
  this.appname = appname
19
20
  this.strict = strict;
20
21
  this.validator = new Ajv2019({
@@ -23,14 +24,17 @@ class HoloSphere {
23
24
  validateSchema: true // Always validate schemas
24
25
  });
25
26
 
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
- });
27
+ // Handle different ways of providing Gun instance or options
28
+ if (gunInstance && gunInstance.opt) {
29
+ // If an object with 'opt' property is passed, create a new Gun instance with those options
30
+ this.gun = Gun(gunInstance.opt);
31
+ } else {
32
+ // Use provided Gun instance or create new one with default options
33
+ this.gun = gunInstance || Gun({
34
+ peers: ['https://gun.holons.io/gun'],
35
+ axe: false,
36
+ });
37
+ }
34
38
 
35
39
  // Initialize SEA
36
40
  this.sea = SEA;
@@ -41,9 +45,6 @@ class HoloSphere {
41
45
  });
42
46
  }
43
47
 
44
- // Add currentSpace property to track logged in space
45
- this.currentSpace = null;
46
-
47
48
  // Initialize subscriptions
48
49
  this.subscriptions = {};
49
50
  }
@@ -105,8 +106,7 @@ class HoloSphere {
105
106
  await this.putGlobal('schemas', {
106
107
  id: lens,
107
108
  schema: schema,
108
- timestamp: Date.now(),
109
- owner: this.currentSpace?.alias
109
+ timestamp: Date.now()
110
110
  });
111
111
 
112
112
  return true;
@@ -137,148 +137,142 @@ class HoloSphere {
137
137
  * @param {string} holon - The holon identifier.
138
138
  * @param {string} lens - The lens under which to store the content.
139
139
  * @param {object} data - The data to store.
140
+ * @param {string} [password] - Optional password for private space.
140
141
  * @returns {Promise<boolean>} - Returns true if successful, false if there was an error
141
142
  */
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) {
143
+ async put(holon, lens, data, password = null) {
144
+ if (!holon || !lens || !data) {
170
145
  throw new Error('put: Missing required parameters');
171
146
  }
172
147
 
173
- if (!dataWithMeta.id) {
174
- dataWithMeta.id = this.generateId();
148
+ if (!data.id) {
149
+ data.id = this.generateId();
175
150
  }
176
151
 
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));
152
+ // Get and validate schema only in strict mode
153
+ if (this.strict) {
154
+ const schema = await this.getSchema(lens);
155
+ if (!schema) {
156
+ throw new Error('Schema required in strict mode');
157
+ }
158
+ const dataToValidate = JSON.parse(JSON.stringify(data));
182
159
  const valid = this.validator.validate(schema, dataToValidate);
183
160
 
184
161
  if (!valid) {
185
162
  const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
186
- // Always throw on schema validation failure, regardless of strict mode
187
163
  throw new Error(errorMsg);
188
164
  }
189
- } else if (this.strict) {
190
- throw new Error('Schema required in strict mode');
191
165
  }
192
166
 
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 => {
202
- if (ack.err) {
203
- reject(new Error(ack.err));
204
- } else {
205
- // Notify subscribers after successful put
206
- this.notifySubscribers({
207
- holon,
208
- lens,
209
- ...dataWithMeta
167
+ try {
168
+ const user = this.gun.user();
169
+
170
+ if (password) {
171
+ try {
172
+ await new Promise((resolve, reject) => {
173
+ user.auth(this.userName(holon), password, (ack) => {
174
+ if (ack.err) reject(new Error(ack.err));
175
+ else resolve();
176
+ });
177
+ });
178
+ } catch (loginError) {
179
+ // If authentication fails, try to create user and then authenticate
180
+ try {
181
+ await new Promise((resolve, reject) => {
182
+ user.create(this.userName(holon), password, (ack) => {
183
+ if (ack.err) {
184
+ // Don't reject if the user is already being created or already exists
185
+ if (ack.err.includes('already being created') ||
186
+ ack.err.includes('already created')) {
187
+ console.warn(`User creation note: ${ack.err}, continuing...`);
188
+ // Try to authenticate again
189
+ user.auth(this.userName(holon), password, (authAck) => {
190
+ if (authAck.err) {
191
+ if (authAck.err.includes('already being created') ||
192
+ authAck.err.includes('already created')) {
193
+ console.warn(`Auth note: ${authAck.err}, continuing...`);
194
+ resolve(); // Continue anyway
195
+ } else {
196
+ reject(new Error(authAck.err));
197
+ }
198
+ } else {
199
+ resolve();
200
+ }
201
+ });
202
+ } else {
203
+ reject(new Error(ack.err));
204
+ }
205
+ } else {
206
+ user.auth(this.userName(holon), password, (authAck) => {
207
+ if (authAck.err) reject(new Error(authAck.err));
208
+ else resolve();
209
+ });
210
+ }
210
211
  });
211
- resolve(true);
212
+ });
213
+ } catch (createError) {
214
+ // Try one last authentication
215
+ try {
216
+ await new Promise((resolve, reject) => {
217
+ setTimeout(() => {
218
+ user.auth(this.userName(holon), password, (ack) => {
219
+ if (ack.err) {
220
+ // Continue even if auth fails at this point
221
+ console.warn(`Final auth attempt note: ${ack.err}, continuing with limited functionality`);
222
+ resolve();
223
+ } else {
224
+ resolve();
225
+ }
226
+ });
227
+ }, 100); // Short delay before retry
228
+ });
229
+ } catch (finalAuthError) {
230
+ console.warn('All authentication attempts failed, continuing with limited functionality');
212
231
  }
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);
222
- }
223
-
224
- return putResult;
225
- }
226
-
227
- /**
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
233
- */
234
- async _propagateToFederation(holon, lens, data) {
235
- 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
232
+ }
233
+ }
240
234
  }
241
235
 
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()
255
- }
256
- }), ack => {
236
+ return new Promise((resolve, reject) => {
237
+ try {
238
+ const payload = JSON.stringify(data);
239
+
240
+ if (password) {
241
+ // For private data, use the authenticated user's space
242
+ user.get('private').get(lens).get(data.id).put(payload, ack => {
257
243
  if (ack.err) {
258
- console.warn(`Failed to propagate to space ${spaceId}:`, ack.err);
244
+ reject(new Error(ack.err));
245
+ } else {
246
+ this.notifySubscribers({
247
+ holon,
248
+ lens,
249
+ ...data
250
+ });
251
+ resolve(true);
259
252
  }
260
- resolve();
261
253
  });
262
-
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()
254
+ } else {
255
+ // For public data, use the regular path
256
+ this.gun.get(this.appname).get(holon).get(lens).get(data.id).put(payload, ack => {
257
+ if (ack.err) {
258
+ reject(new Error(ack.err));
259
+ } else {
260
+ this.notifySubscribers({
261
+ holon,
262
+ lens,
263
+ ...data
264
+ });
265
+ resolve(true);
273
266
  }
274
- }));
275
- })
276
- );
277
-
278
- await Promise.all(propagationPromises);
267
+ });
268
+ }
269
+ } catch (error) {
270
+ reject(error);
271
+ }
272
+ });
279
273
  } catch (error) {
280
- console.warn('Federation propagation error:', error);
281
- // Don't throw here to avoid failing the original put
274
+ console.error('Error in put:', error);
275
+ throw error;
282
276
  }
283
277
  }
284
278
 
@@ -286,151 +280,160 @@ class HoloSphere {
286
280
  * Retrieves content from the specified holon and lens.
287
281
  * @param {string} holon - The holon identifier.
288
282
  * @param {string} lens - The lens from which to retrieve content.
289
- * @returns {Promise<Array<object>>} - The retrieved content.
283
+ * @param {string} key - The specific key to retrieve.
284
+ * @param {string} [password] - Optional password for private space.
285
+ * @returns {Promise<object|null>} - The retrieved content or null if not found.
290
286
  */
291
- async getAll(holon, lens) {
292
- if (!holon || !lens) {
293
- throw new Error('getAll: Missing required parameters');
294
- }
295
-
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);
287
+ async get(holon, lens, key, password = null) {
288
+ if (!holon || !lens || !key) {
289
+ console.error('get: Missing required parameters:', { holon, lens, key });
290
+ return null;
308
291
  }
309
292
 
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);
293
+ // Only check schema in strict mode
294
+ let schema;
295
+ if (this.strict) {
296
+ schema = await this.getSchema(lens);
297
+ if (!schema) {
298
+ throw new Error('Schema required in strict mode');
317
299
  }
318
- });
300
+ }
319
301
 
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);
302
+ try {
303
+ const user = this.gun.user();
304
+
305
+ if (password) {
306
+ try {
307
+ await new Promise((resolve, reject) => {
308
+ user.auth(this.userName(holon), password, (ack) => {
309
+ if (ack.err) reject(new Error(ack.err));
310
+ else resolve();
311
+ });
312
+ });
313
+ } catch (loginError) {
314
+ // If authentication fails, try to create user and then authenticate
315
+ await new Promise((resolve, reject) => {
316
+ user.create(this.userName(holon), password, (ack) => {
317
+ if (ack.err) reject(new Error(ack.err));
318
+ else {
319
+ user.auth(this.userName(holon), password, (authAck) => {
320
+ if (authAck.err) reject(new Error(authAck.err));
321
+ else resolve();
322
+ });
323
+ }
324
+ });
325
+ });
327
326
  }
328
327
  }
329
- });
330
328
 
331
- return Array.from(combined.values());
332
- }
329
+ return new Promise((resolve) => {
330
+ const handleData = async (data) => {
331
+ if (!data) {
332
+ resolve(null);
333
+ return;
334
+ }
333
335
 
334
- /**
335
- * Gets data from federated spaces
336
- * @private
337
- * @param {string} holon - The holon identifier
338
- * @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
341
- */
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
- }
336
+ try {
337
+ const parsed = await this.parse(data);
348
338
 
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);
339
+ if (schema) {
340
+ const valid = this.validator.validate(schema, parsed);
341
+ if (!valid) {
342
+ console.error('get: Invalid data according to schema:', this.validator.errors);
343
+ if (this.strict) {
344
+ resolve(null);
345
+ return;
346
+ }
367
347
  }
368
- });
369
- });
348
+ }
370
349
 
371
- if (result) {
372
- return result;
350
+ resolve(parsed);
351
+ } catch (error) {
352
+ console.error('Error parsing data:', error);
353
+ resolve(null);
354
+ }
355
+ };
356
+
357
+ if (password) {
358
+ // For private data, use the authenticated user's space
359
+ user.get('private').get(lens).get(key).once(handleData);
360
+ } else {
361
+ // For public data, use the regular path
362
+ this.gun.get(this.appname).get(holon).get(lens).get(key).once(handleData);
373
363
  }
374
- }
364
+ });
375
365
  } catch (error) {
376
- console.warn('Federation get error:', error);
366
+ console.error('Error in get:', error);
367
+ return null;
377
368
  }
378
- return null;
379
369
  }
380
370
 
381
371
  /**
382
- * Gets all data from local space
383
- * @private
372
+ * Propagates data to federated spaces
384
373
  * @param {string} holon - The holon identifier
385
374
  * @param {string} lens - The lens identifier
386
- * @param {object} schema - The schema to validate against
387
- * @returns {Promise<Array>} - Array of local data
375
+ * @param {object} data - The data to propagate
376
+ * @param {object} [options] - Propagation options
377
+ * @returns {Promise<object>} - Result with success count and errors
388
378
  */
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
- };
379
+ async propagateToFederation(holon, lens, data, options = {}) {
380
+ return Federation.propagateToFederation(this, holon, lens, data, options);
381
+ }
407
382
 
408
- const processData = async (data, key) => {
409
- if (!data || key === '_') return;
383
+ /**
384
+ * @private
385
+ * @deprecated Use propagateToFederation instead
386
+ */
387
+ async _propagateToFederation(holon, lens, data) {
388
+ console.warn('_propagateToFederation is deprecated, use propagateToFederation instead');
389
+ return this.propagateToFederation(holon, lens, data);
390
+ }
410
391
 
411
- try {
412
- const parsed = await this.parse(data);
413
- if (!parsed || !parsed.id) return;
392
+ /**
393
+ * Retrieves all content from the specified holon and lens.
394
+ * @param {string} holon - The holon identifier.
395
+ * @param {string} lens - The lens from which to retrieve content.
396
+ * @param {string} [password] - Optional password for private space.
397
+ * @returns {Promise<Array<object>>} - The retrieved content.
398
+ */
399
+ async getAll(holon, lens, password = null) {
400
+ if (!holon || !lens) {
401
+ throw new Error('getAll: Missing required parameters');
402
+ }
414
403
 
415
- if (schema) {
416
- const valid = this.validator.validate(schema, parsed);
417
- if (valid || !this.strict) {
404
+ const schema = await this.getSchema(lens);
405
+ if (!schema && this.strict) {
406
+ throw new Error('getAll: Schema required in strict mode');
407
+ }
408
+
409
+ try {
410
+ const user = this.gun.user();
411
+
412
+ return new Promise((resolve) => {
413
+ const output = new Map();
414
+
415
+ const processData = async (data, key) => {
416
+ if (!data || key === '_') return;
417
+
418
+ try {
419
+ const parsed = await this.parse(data);
420
+ if (!parsed || !parsed.id) return;
421
+
422
+ if (schema) {
423
+ const valid = this.validator.validate(schema, parsed);
424
+ if (valid || !this.strict) {
425
+ output.set(parsed.id, parsed);
426
+ }
427
+ } else {
418
428
  output.set(parsed.id, parsed);
419
429
  }
420
- } else {
421
- output.set(parsed.id, parsed);
430
+ } catch (error) {
431
+ console.error('Error processing data:', error);
422
432
  }
423
- } catch (error) {
424
- console.error('Error processing data:', error);
425
- }
426
- };
433
+ };
427
434
 
428
- this.gun.get(this.appname)
429
- .get(holon)
430
- .get(lens)
431
- .once(async (data) => {
435
+ const handleData = async (data) => {
432
436
  if (!data) {
433
- cleanup();
434
437
  resolve([]);
435
438
  return;
436
439
  }
@@ -444,75 +447,23 @@ class HoloSphere {
444
447
 
445
448
  try {
446
449
  await Promise.all(initialPromises);
447
- cleanup();
448
450
  resolve(Array.from(output.values()));
449
451
  } catch (error) {
450
- cleanup();
452
+ console.error('Error in getAll:', error);
451
453
  resolve([]);
452
454
  }
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
- }
471
-
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
- });
455
+ };
505
456
 
506
- await Promise.all(processPromises);
507
- resolve();
508
- });
509
- })
510
- );
511
-
512
- await Promise.all(fedPromises);
513
- return Array.from(federatedData.values());
457
+ if (password) {
458
+ // For private data, use the authenticated user's space
459
+ user.get('private').get(lens).once(handleData);
460
+ } else {
461
+ // For public data, use the regular path
462
+ this.gun.get(this.appname).get(holon).get(lens).once(handleData);
463
+ }
464
+ });
514
465
  } catch (error) {
515
- console.warn('Federation getAll error:', error);
466
+ console.error('Error in getAll:', error);
516
467
  return [];
517
468
  }
518
469
  }
@@ -575,186 +526,116 @@ class HoloSphere {
575
526
  }
576
527
  }
577
528
 
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
529
  /**
666
530
  * Deletes a specific key from a given holon and lens.
667
531
  * @param {string} holon - The holon identifier.
668
532
  * @param {string} lens - The lens from which to delete the key.
669
533
  * @param {string} key - The specific key to delete.
534
+ * @param {string} [password] - Optional password for private space.
535
+ * @returns {Promise<boolean>} - Returns true if successful
670
536
  */
671
- async delete(holon, lens, key) {
537
+ async delete(holon, lens, key, password = null) {
672
538
  if (!holon || !lens || !key) {
673
539
  throw new Error('delete: Missing required parameters');
674
540
  }
675
541
 
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 => {
542
+ try {
543
+ // Get the appropriate space
544
+ const user = this.gun.user();
545
+
546
+ // Delete data from space
547
+ return new Promise((resolve, reject) => {
548
+ if (password) {
549
+ // For private data, use the authenticated user's space
550
+ user.get('private').get(lens).get(key).put(null, ack => {
698
551
  if (ack.err) {
699
552
  reject(new Error(ack.err));
700
553
  } else {
701
554
  resolve(true);
702
555
  }
703
556
  });
704
- } catch (error) {
705
- reject(error);
706
- }
707
- });
557
+ } else {
558
+ // For public data, use the regular path
559
+ this.gun.get(this.appname).get(holon).get(lens).get(key).put(null, ack => {
560
+ if (ack.err) {
561
+ reject(new Error(ack.err));
562
+ } else {
563
+ resolve(true);
564
+ }
565
+ });
566
+ }
567
+ });
568
+ } catch (error) {
569
+ console.error('Error in delete:', error);
570
+ throw error;
571
+ }
708
572
  }
709
573
 
710
574
  /**
711
575
  * Deletes all keys from a given holon and lens.
712
576
  * @param {string} holon - The holon identifier.
713
577
  * @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
578
+ * @param {string} [password] - Optional password for private space.
579
+ * @returns {Promise<boolean>} - Returns true if successful
715
580
  */
716
- async deleteAll(holon, lens) {
581
+ async deleteAll(holon, lens, password = null) {
717
582
  if (!holon || !lens) {
718
583
  console.error('deleteAll: Missing holon or lens parameter');
719
584
  return false;
720
585
  }
721
586
 
722
- return new Promise((resolve) => {
723
- let deletionPromises = [];
587
+ try {
588
+ // Get the appropriate space
589
+ const user = this.gun.user();
590
+
591
+ return new Promise((resolve) => {
592
+ let deletionPromises = [];
593
+
594
+ const dataPath = password ?
595
+ user.get('private').get(lens) :
596
+ this.gun.get(this.appname).get(holon).get(lens);
597
+
598
+ // First get all the data to find keys to delete
599
+ dataPath.once((data) => {
600
+ if (!data) {
601
+ resolve(true); // Nothing to delete
602
+ return;
603
+ }
724
604
 
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
- }
605
+ // Get all keys except Gun's metadata key '_'
606
+ const keys = Object.keys(data).filter(key => key !== '_');
731
607
 
732
- // Get all keys except Gun's metadata key '_'
733
- const keys = Object.keys(data).filter(key => key !== '_');
608
+ // Create deletion promises for each key
609
+ keys.forEach(key => {
610
+ deletionPromises.push(
611
+ new Promise((resolveDelete) => {
612
+ const deletePath = password ?
613
+ user.get('private').get(lens).get(key) :
614
+ this.gun.get(this.appname).get(holon).get(lens).get(key);
734
615
 
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
- });
616
+ deletePath.put(null, ack => {
617
+ resolveDelete(!!ack.ok); // Convert to boolean
618
+ });
619
+ })
620
+ );
621
+ });
622
+
623
+ // Wait for all deletions to complete
624
+ Promise.all(deletionPromises)
625
+ .then(results => {
626
+ const allSuccessful = results.every(result => result === true);
627
+ resolve(allSuccessful);
742
628
  })
743
- );
629
+ .catch(error => {
630
+ console.error('Error in deleteAll:', error);
631
+ resolve(false);
632
+ });
744
633
  });
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
634
  });
757
- });
635
+ } catch (error) {
636
+ console.error('Error in deleteAll:', error);
637
+ return false;
638
+ }
758
639
  }
759
640
 
760
641
  // ================================ NODE FUNCTIONS ================================
@@ -871,243 +752,418 @@ class HoloSphere {
871
752
  * Stores data in a global (non-holon-specific) table.
872
753
  * @param {string} tableName - The table name to store data in.
873
754
  * @param {object} data - The data to store. If it has an 'id' field, it will be used as the key.
755
+ * @param {string} [password] - Optional password for private space.
874
756
  * @returns {Promise<void>}
875
757
  */
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
- }
758
+ async putGlobal(tableName, data, password = null) {
759
+ try {
760
+ if (!tableName || !data) {
761
+ throw new Error('Table name and data are required');
762
+ }
882
763
 
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
- }
764
+ const user = this.gun.user();
765
+
766
+ if (password) {
767
+ try {
768
+ // Try to authenticate first
769
+ await new Promise((resolve, reject) => {
770
+ user.auth(this.userName(tableName), password, (ack) => {
771
+ if (ack.err) {
772
+ // Handle wrong username/password gracefully
773
+ if (ack.err.includes('Wrong user or password') ||
774
+ ack.err.includes('No user')) {
775
+ console.warn(`Authentication failed for ${tableName}: ${ack.err}`);
776
+ // Will try to create user next
777
+ reject(new Error(ack.err));
778
+ } else {
779
+ reject(new Error(ack.err));
780
+ }
781
+ } else {
782
+ resolve();
783
+ }
784
+ });
898
785
  });
786
+ } catch (authError) {
787
+ // If authentication fails, try to create user
788
+ try {
789
+ await new Promise((resolve, reject) => {
790
+ user.create(this.userName(tableName), password, (ack) => {
791
+ // Handle "User already created!" error gracefully
792
+ if (ack.err && !ack.err.includes('already created')) {
793
+ reject(new Error(ack.err));
794
+ } else {
795
+ // Whether user was created or already existed, try to authenticate
796
+ user.auth(this.userName(tableName), password, (authAck) => {
797
+ if (authAck.err) {
798
+ console.warn(`Authentication failed after creation for ${tableName}: ${authAck.err}`);
799
+ reject(new Error(authAck.err));
800
+ } else {
801
+ resolve();
802
+ }
803
+ });
804
+ }
805
+ });
806
+ });
807
+ } catch (createError) {
808
+ // If both auth and create fail, try one last auth attempt
809
+ await new Promise((resolve, reject) => {
810
+ user.auth(this.userName(tableName), password, (ack) => {
811
+ if (ack.err) {
812
+ console.warn(`Final authentication attempt failed for ${tableName}: ${ack.err}`);
813
+ // Continue with operation even if auth fails
814
+ resolve();
815
+ } else {
816
+ resolve();
817
+ }
818
+ });
819
+ });
820
+ }
899
821
  }
900
- } catch (error) {
901
- reject(error);
902
822
  }
903
- });
823
+
824
+ return new Promise((resolve, reject) => {
825
+ const payload = JSON.stringify(data);
826
+
827
+ if (password) {
828
+ // For private data, use the authenticated user's space
829
+ const path = user.get('private').get(tableName);
830
+
831
+ if (data.id) {
832
+ path.get(data.id).put(payload, ack => {
833
+ if (ack.err) {
834
+ reject(new Error(ack.err));
835
+ } else {
836
+ resolve();
837
+ }
838
+ });
839
+ } else {
840
+ path.put(payload, ack => {
841
+ if (ack.err) {
842
+ reject(new Error(ack.err));
843
+ } else {
844
+ resolve();
845
+ }
846
+ });
847
+ }
848
+ } else {
849
+ // For public data, use the regular path
850
+ const path = this.gun.get(this.appname).get(tableName);
851
+
852
+ if (data.id) {
853
+ path.get(data.id).put(payload, ack => {
854
+ if (ack.err) {
855
+ reject(new Error(ack.err));
856
+ } else {
857
+ resolve();
858
+ }
859
+ });
860
+ } else {
861
+ path.put(payload, ack => {
862
+ if (ack.err) {
863
+ reject(new Error(ack.err));
864
+ } else {
865
+ resolve();
866
+ }
867
+ });
868
+ }
869
+ }
870
+ });
871
+ } catch (error) {
872
+ console.error('Error in putGlobal:', error);
873
+ throw error;
874
+ }
904
875
  }
905
876
 
906
877
  /**
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
- }
878
+ * Retrieves a specific key from a global table.
879
+ * @param {string} tableName - The table name to retrieve from.
880
+ * @param {string} key - The key to retrieve.
881
+ * @param {string} [password] - Optional password for private space.
882
+ * @returns {Promise<object|null>} - The parsed data for the key or null if not found.
883
+ */
884
+ async getGlobal(tableName, key, password = null) {
885
+ try {
886
+ const user = this.gun.user();
887
+
888
+ if (password) {
919
889
  try {
920
- const parsed = this.parse(data);
921
- resolve(parsed);
922
- } catch (e) {
923
- resolve(null);
890
+ await new Promise((resolve, reject) => {
891
+ user.auth(this.userName(tableName), password, (ack) => {
892
+ if (ack.err) {
893
+ // Handle wrong username/password gracefully
894
+ if (ack.err.includes('Wrong user or password') ||
895
+ ack.err.includes('No user')) {
896
+ console.warn(`Authentication failed for ${tableName}: ${ack.err}`);
897
+ // Will try to create user next
898
+ reject(new Error(ack.err));
899
+ } else {
900
+ reject(new Error(ack.err));
901
+ }
902
+ } else {
903
+ resolve();
904
+ }
905
+ });
906
+ });
907
+ } catch (loginError) {
908
+ // If authentication fails, try to create user and then authenticate
909
+ await new Promise((resolve, reject) => {
910
+ user.create(this.userName(tableName), password, (ack) => {
911
+ // Handle "User already created!" error gracefully
912
+ if (ack.err && !ack.err.includes('already created')) {
913
+ reject(new Error(ack.err));
914
+ } else {
915
+ user.auth(this.userName(tableName), password, (authAck) => {
916
+ if (authAck.err) {
917
+ console.warn(`Authentication failed after creation for ${tableName}: ${authAck.err}`);
918
+ // Continue with operation even if auth fails
919
+ resolve();
920
+ } else {
921
+ resolve();
922
+ }
923
+ });
924
+ }
925
+ });
926
+ });
927
+ }
928
+ }
929
+
930
+ return new Promise((resolve) => {
931
+ const handleData = (data) => {
932
+ if (!data) {
933
+ resolve(null);
934
+ return;
935
+ }
936
+ try {
937
+ const parsed = this.parse(data);
938
+ resolve(parsed);
939
+ } catch (e) {
940
+ resolve(null);
941
+ }
942
+ };
943
+
944
+ if (password) {
945
+ // For private data, use the authenticated user's space
946
+ user.get('private').get(tableName).get(key).once(handleData);
947
+ } else {
948
+ // For public data, use the regular path
949
+ this.gun.get(this.appname).get(tableName).get(key).once(handleData);
924
950
  }
925
951
  });
926
- });
952
+ } catch (error) {
953
+ console.error('Error in getGlobal:', error);
954
+ return null;
955
+ }
927
956
  }
928
957
 
929
-
930
-
931
958
  /**
932
959
  * Retrieves all data from a global table.
933
960
  * @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.
961
+ * @param {string} [password] - Optional password for private space.
962
+ * @returns {Promise<Array<object>>} - The parsed data from the table as an array.
935
963
  */
936
- async getAllGlobal(tableName) {
964
+ async getAllGlobal(tableName, password = null) {
937
965
  if (!tableName) {
938
966
  throw new Error('getAllGlobal: Missing table name parameter');
939
967
  }
940
968
 
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);
969
+ try {
970
+ // Get the appropriate space
971
+ const user = this.gun.user();
950
972
 
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
- }
973
+ return new Promise((resolve) => {
974
+ let output = [];
975
+ let isResolved = false;
976
+ let timeout = setTimeout(() => {
977
+ if (!isResolved) {
978
+ isResolved = true;
979
+ resolve(output);
980
+ }
981
+ }, 5000);
958
982
 
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
- });
983
+ const handleData = async (data) => {
984
+ if (!data) {
985
+ clearTimeout(timeout);
986
+ isResolved = true;
987
+ resolve([]);
988
+ return;
989
+ }
990
+
991
+ const keys = Object.keys(data).filter(key => key !== '_');
992
+ const promises = keys.map(key =>
993
+ new Promise(async (resolveItem) => {
994
+ const itemPath = password ?
995
+ user.get('private').get(tableName).get(key) :
996
+ this.gun.get(this.appname).get(tableName).get(key);
997
+
998
+ const itemData = await new Promise(resolveData => {
999
+ itemPath.once(resolveData);
1000
+ });
1001
+
1002
+ if (itemData) {
1003
+ try {
1004
+ const parsed = await this.parse(itemData);
1005
+ if (parsed) output.push(parsed);
1006
+ } catch (error) {
1007
+ console.error('Error parsing data:', error);
1008
+ }
1009
+ }
1010
+ resolveItem();
1011
+ })
1012
+ );
1013
+
1014
+ await Promise.all(promises);
1015
+ clearTimeout(timeout);
1016
+ if (!isResolved) {
1017
+ isResolved = true;
1018
+ resolve(output);
1019
+ }
1020
+ };
965
1021
 
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);
972
- }
973
- }
974
- resolveItem();
975
- })
976
- );
977
-
978
- await Promise.all(promises);
979
- clearTimeout(timeout);
980
- if (!isResolved) {
981
- isResolved = true;
982
- resolve(output);
1022
+ if (password) {
1023
+ // For private data, use the authenticated user's space
1024
+ user.get('private').get(tableName).once(handleData);
1025
+ } else {
1026
+ // For public data, use the regular path
1027
+ this.gun.get(this.appname).get(tableName).once(handleData);
983
1028
  }
984
1029
  });
985
- });
1030
+ } catch (error) {
1031
+ console.error('Error in getAllGlobal:', error);
1032
+ return [];
1033
+ }
986
1034
  }
1035
+
987
1036
  /**
988
1037
  * Deletes a specific key from a global table.
989
1038
  * @param {string} tableName - The table name to delete from.
990
1039
  * @param {string} key - The key to delete.
991
- * @returns {Promise<void>}
1040
+ * @param {string} [password] - Optional password for private space.
1041
+ * @returns {Promise<boolean>}
992
1042
  */
993
- async deleteGlobal(tableName, key) {
1043
+ async deleteGlobal(tableName, key, password = null) {
994
1044
  if (!tableName || !key) {
995
1045
  throw new Error('deleteGlobal: Missing required parameters');
996
1046
  }
997
1047
 
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
- }
1048
+ try {
1049
+ // Get the appropriate space
1050
+ const user = this.gun.user();
1007
1051
 
1008
- return new Promise((resolve, reject) => {
1009
- try {
1010
- this.gun.get(this.appname)
1011
- .get(tableName)
1012
- .get(key)
1013
- .put(null, ack => {
1052
+ return new Promise((resolve, reject) => {
1053
+ if (password) {
1054
+ // For private data, use the authenticated user's space
1055
+ user.get('private').get(tableName).get(key).put(null, ack => {
1014
1056
  if (ack.err) {
1015
1057
  reject(new Error(ack.err));
1016
1058
  } else {
1017
1059
  resolve(true);
1018
1060
  }
1019
1061
  });
1020
- } catch (error) {
1021
- reject(error);
1022
- }
1023
- });
1062
+ } else {
1063
+ // For public data, use the regular path
1064
+ this.gun.get(this.appname).get(tableName).get(key).put(null, ack => {
1065
+ if (ack.err) {
1066
+ reject(new Error(ack.err));
1067
+ } else {
1068
+ resolve(true);
1069
+ }
1070
+ });
1071
+ }
1072
+ });
1073
+ } catch (error) {
1074
+ console.error('Error in deleteGlobal:', error);
1075
+ throw error;
1076
+ }
1024
1077
  }
1025
1078
 
1026
1079
  /**
1027
1080
  * Deletes an entire global table.
1028
1081
  * @param {string} tableName - The table name to delete.
1029
- * @returns {Promise<void>}
1082
+ * @param {string} [password] - Optional password for private space.
1083
+ * @returns {Promise<boolean>}
1030
1084
  */
1031
- async deleteAllGlobal(tableName) {
1085
+ async deleteAllGlobal(tableName, password = null) {
1032
1086
  if (!tableName) {
1033
1087
  throw new Error('deleteAllGlobal: Missing table name parameter');
1034
1088
  }
1035
1089
 
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
- }
1090
+ try {
1091
+ // Get the appropriate space
1092
+ const user = this.gun.user();
1040
1093
 
1041
- // Skip session check for spaces and federation tables
1042
- if (!['spaces', 'federation'].includes(tableName)) {
1043
- this._checkSession();
1044
- }
1094
+ return new Promise((resolve, reject) => {
1095
+ try {
1096
+ const deletions = new Set();
1097
+ let timeout = setTimeout(() => {
1098
+ if (deletions.size === 0) {
1099
+ resolve(true); // No data to delete
1100
+ }
1101
+ }, 5000);
1045
1102
 
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);
1103
+ const dataPath = password ?
1104
+ user.get('private').get(tableName) :
1105
+ this.gun.get(this.appname).get(tableName);
1054
1106
 
1055
- this.gun.get(this.appname).get(tableName).once(async (data) => {
1056
- if (!data) {
1057
- clearTimeout(timeout);
1058
- resolve(true);
1059
- return;
1060
- }
1107
+ dataPath.once(async (data) => {
1108
+ if (!data) {
1109
+ clearTimeout(timeout);
1110
+ resolve(true);
1111
+ return;
1112
+ }
1061
1113
 
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 => {
1114
+ const keys = Object.keys(data).filter(key => key !== '_');
1115
+ const promises = keys.map(key =>
1116
+ new Promise((resolveDelete) => {
1117
+ const deletePath = password ?
1118
+ user.get('private').get(tableName).get(key) :
1119
+ this.gun.get(this.appname).get(tableName).get(key);
1120
+
1121
+ deletePath.put(null, ack => {
1069
1122
  if (ack.err) {
1070
1123
  console.error(`Failed to delete ${key}:`, ack.err);
1071
1124
  }
1072
1125
  resolveDelete();
1073
1126
  });
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
- });
1127
+ })
1128
+ );
1129
+
1130
+ try {
1131
+ await Promise.all(promises);
1132
+ // Finally delete the table itself
1133
+ dataPath.put(null);
1134
+ clearTimeout(timeout);
1135
+ resolve(true);
1136
+ } catch (error) {
1137
+ reject(error);
1138
+ }
1139
+ });
1140
+ } catch (error) {
1141
+ reject(error);
1142
+ }
1143
+ });
1144
+ } catch (error) {
1145
+ console.error('Error in deleteAllGlobal:', error);
1146
+ throw error;
1147
+ }
1091
1148
  }
1092
1149
 
1093
1150
  // ================================ COMPUTE FUNCTIONS ================================
1094
1151
  /**
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) {
1152
+ * Computes operations across multiple layers up the hierarchy
1153
+ * @param {string} holon - Starting holon identifier
1154
+ * @param {string} lens - The lens to compute
1155
+ * @param {object} options - Computation options
1156
+ * @param {number} [maxLevels=15] - Maximum levels to compute up
1157
+ * @param {string} [password] - Optional password for private spaces
1158
+ */
1159
+ async computeHierarchy(holon, lens, options, maxLevels = 15, password = null) {
1104
1160
  let currentHolon = holon;
1105
1161
  let currentRes = h3.getResolution(currentHolon);
1106
1162
  const results = [];
1107
1163
 
1108
1164
  while (currentRes > 0 && maxLevels > 0) {
1109
1165
  try {
1110
- const result = await this.compute(currentHolon, lens, options);
1166
+ const result = await this.compute(currentHolon, lens, options, password);
1111
1167
  if (result) {
1112
1168
  results.push(result);
1113
1169
  }
@@ -1123,16 +1179,18 @@ class HoloSphere {
1123
1179
  return results;
1124
1180
  }
1125
1181
 
1126
- /* Computes operations on content within a holon and lens for one layer up.
1182
+ /**
1183
+ * Computes operations on content within a holon and lens for one layer up.
1127
1184
  * @param {string} holon - The holon identifier.
1128
1185
  * @param {string} lens - The lens to compute.
1129
1186
  * @param {object} options - Computation options
1130
1187
  * @param {string} options.operation - The operation to perform ('summarize', 'aggregate', 'concatenate')
1131
1188
  * @param {string[]} [options.fields] - Fields to perform operation on
1132
1189
  * @param {string} [options.targetField] - Field to store the result in
1190
+ * @param {string} [password] - Optional password for private spaces
1133
1191
  * @throws {Error} If parameters are invalid or missing
1134
1192
  */
1135
- async compute(holon, lens, options) {
1193
+ async compute(holon, lens, options, password = null) {
1136
1194
  // Validate required parameters
1137
1195
  if (!holon || !lens) {
1138
1196
  throw new Error('compute: Missing required parameters');
@@ -1187,7 +1245,7 @@ class HoloSphere {
1187
1245
 
1188
1246
  // Collect all content from siblings
1189
1247
  const contents = await Promise.all(
1190
- siblings.map(sibling => this.getAll(sibling, lens))
1248
+ siblings.map(sibling => this.getAll(sibling, lens, password))
1191
1249
  );
1192
1250
 
1193
1251
  const flatContents = contents.flat().filter(Boolean);
@@ -1249,7 +1307,7 @@ class HoloSphere {
1249
1307
  result.value = computed;
1250
1308
  }
1251
1309
 
1252
- await this.put(parent, lens, result);
1310
+ await this.put(parent, lens, result, password);
1253
1311
  return result;
1254
1312
  }
1255
1313
  } catch (error) {
@@ -1384,307 +1442,143 @@ class HoloSphere {
1384
1442
  * @param {string} holon - The holon identifier.
1385
1443
  * @param {string} lens - The lens to subscribe to.
1386
1444
  * @param {function} callback - The callback to execute on changes.
1445
+ * @returns {Promise<object>} - Subscription object with unsubscribe method
1387
1446
  */
1388
1447
  async subscribe(holon, lens, callback) {
1448
+ if (!holon || !lens || typeof callback !== 'function') {
1449
+ throw new Error('subscribe: Missing required parameters');
1450
+ }
1451
+
1389
1452
  const subscriptionId = this.generateId();
1390
- this.subscriptions[subscriptionId] =
1391
- this.gun.get(this.appname).get(holon).get(lens).map().on( async (data, key) => {
1453
+
1454
+ try {
1455
+ // Create the subscription
1456
+ const gunSubscription = this.gun.get(this.appname).get(holon).get(lens).map().on(async (data, key) => {
1392
1457
  if (data) {
1393
1458
  try {
1394
- let parsed = await this.parse(data)
1395
- callback(parsed, key)
1459
+ let parsed = await this.parse(data);
1460
+ callback(parsed, key);
1396
1461
  } catch (error) {
1397
1462
  console.error('Error in subscribe:', error);
1398
1463
  }
1399
1464
  }
1400
- })
1401
- return {
1402
- unsubscribe: () => {
1403
- this.gun.get(this.appname).get(holon).get(lens).map().off()
1404
- delete this.subscriptions[subscriptionId];
1405
- }
1406
- }
1407
- }
1408
-
1409
-
1410
- notifySubscribers(data) {
1411
- Object.values(this.subscriptions).forEach(subscription => {
1412
- if (subscription.active && this.matchesQuery(data, subscription.query)) {
1413
- subscription.callback(data);
1414
- }
1415
- });
1416
- }
1417
-
1418
- // Add ID generation method
1419
- generateId() {
1420
- return Date.now().toString(10) + Math.random().toString(2);
1421
- }
1422
-
1423
- matchesQuery(data, query) {
1424
- return data && query &&
1425
- data.holon === query.holon &&
1426
- data.lens === query.lens;
1427
- }
1428
-
1429
- /**
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
1434
- */
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
1465
+ });
1466
+
1467
+ // Store the subscription with its ID
1468
+ this.subscriptions[subscriptionId] = {
1469
+ id: subscriptionId,
1470
+ holon,
1471
+ lens,
1472
+ active: true,
1473
+ gunSubscription
1457
1474
  };
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()
1475
+
1476
+ // Return an object with unsubscribe method
1477
+ return {
1478
+ unsubscribe: () => {
1479
+ try {
1480
+ // Turn off the Gun subscription
1481
+ this.gun.get(this.appname).get(holon).get(lens).map().off();
1482
+
1483
+ // Mark as inactive and remove from subscriptions
1484
+ if (this.subscriptions[subscriptionId]) {
1485
+ this.subscriptions[subscriptionId].active = false;
1486
+ delete this.subscriptions[subscriptionId];
1487
+ }
1488
+ } catch (error) {
1489
+ console.error('Error in unsubscribe:', error);
1490
+ }
1491
+ }
1466
1492
  };
1467
-
1468
- await this.putGlobal('spaces', {
1469
- ...space,
1470
- id: spacename
1471
- });
1472
-
1473
- return true;
1474
1493
  } catch (error) {
1475
- throw new Error(`Space creation failed: ${error.message}`);
1494
+ console.error('Error creating subscription:', error);
1495
+ throw error;
1476
1496
  }
1477
1497
  }
1478
1498
 
1499
+
1479
1500
  /**
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
1501
+ * Notifies subscribers about data changes
1502
+ * @param {object} data - The data to notify about
1503
+ * @private
1484
1504
  */
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');
1505
+ notifySubscribers(data) {
1506
+ if (!data || !data.holon || !data.lens) {
1507
+ return;
1491
1508
  }
1492
-
1509
+
1493
1510
  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;
1511
+ Object.values(this.subscriptions).forEach(subscription => {
1512
+ if (subscription.active &&
1513
+ subscription.holon === data.holon &&
1514
+ subscription.lens === data.lens) {
1515
+ try {
1516
+ if (subscription.callback && typeof subscription.callback === 'function') {
1517
+ subscription.callback(data);
1518
+ }
1519
+ } catch (error) {
1520
+ console.warn('Error in subscription callback:', error);
1521
+ }
1522
+ }
1523
+ });
1513
1524
  } catch (error) {
1514
- throw new Error('Authentication failed');
1525
+ console.warn('Error notifying subscribers:', error);
1515
1526
  }
1516
1527
  }
1517
1528
 
1518
- /**
1519
- * Logs out the current space
1520
- * @returns {Promise<void>}
1521
- */
1522
- async logout() {
1523
- this.currentSpace = null;
1529
+ // Add ID generation method
1530
+ generateId() {
1531
+ return Date.now().toString(10) + Math.random().toString(2);
1524
1532
  }
1525
1533
 
1526
- /**
1527
- * Checks if the current session is valid
1528
- * @private
1529
- */
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;
1539
- }
1534
+ // ================================ FEDERATION FUNCTIONS ================================
1540
1535
 
1541
1536
  /**
1542
1537
  * Creates a federation relationship between two spaces
1543
1538
  * @param {string} spaceId1 - The first space ID
1544
1539
  * @param {string} spaceId2 - The second space ID
1540
+ * @param {string} password1 - Password for the first space
1541
+ * @param {string} [password2] - Optional password for the second space
1545
1542
  * @returns {Promise<boolean>} - True if federation was created successfully
1546
1543
  */
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
- };
1581
- }
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;
1544
+ async federate(spaceId1, spaceId2, password1, password2 = null) {
1545
+ return Federation.federate(this, spaceId1, spaceId2, password1, password2);
1590
1546
  }
1591
1547
 
1592
1548
  /**
1593
1549
  * Subscribes to federation notifications for a space
1594
1550
  * @param {string} spaceId - The space ID to subscribe to
1551
+ * @param {string} password - Password for the space
1595
1552
  * @param {function} callback - The callback to execute on notifications
1596
- * @returns {Promise<object>} - Subscription object with off() method
1553
+ * @param {object} [options] - Subscription options
1554
+ * @param {string[]} [options.lenses] - Specific lenses to subscribe to (default: all)
1555
+ * @param {number} [options.throttle] - Throttle notifications in ms (default: 0)
1556
+ * @returns {Promise<object>} - Subscription object with unsubscribe() method
1597
1557
  */
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
- };
1558
+ async subscribeFederation(spaceId, password, callback, options = {}) {
1559
+ return Federation.subscribeFederation(this, spaceId, password, callback, options);
1638
1560
  }
1639
1561
 
1640
1562
  /**
1641
1563
  * Gets federation info for a space
1642
1564
  * @param {string} spaceId - The space ID
1565
+ * @param {string} [password] - Optional password for the space
1643
1566
  * @returns {Promise<object|null>} - Federation info or null if not found
1644
1567
  */
1645
- async getFederation(spaceId) {
1646
- if (!spaceId) {
1647
- throw new Error('getFederationInfo: Missing space ID');
1648
- }
1649
- return await this.getGlobal('federation', spaceId);
1568
+ async getFederation(spaceId, password = null) {
1569
+ return Federation.getFederation(this, spaceId, password);
1650
1570
  }
1651
1571
 
1652
1572
  /**
1653
1573
  * Removes a federation relationship between spaces
1654
1574
  * @param {string} spaceId1 - The first space ID
1655
1575
  * @param {string} spaceId2 - The second space ID
1576
+ * @param {string} password1 - Password for the first space
1577
+ * @param {string} [password2] - Optional password for the second space
1656
1578
  * @returns {Promise<boolean>} - True if federation was removed successfully
1657
1579
  */
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;
1678
- }
1679
-
1680
- /**
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
1684
- */
1685
- async getChatName(spaceId) {
1686
- const spaceInfo = await this.getGlobal('spaces', spaceId);
1687
- return spaceInfo?.name || spaceId;
1580
+ async unfederate(spaceId1, spaceId2, password1, password2 = null) {
1581
+ return Federation.unfederate(this, spaceId1, spaceId2, password1, password2);
1688
1582
  }
1689
1583
 
1690
1584
  /**
@@ -1692,135 +1586,109 @@ class HoloSphere {
1692
1586
  * @param {string} holon - The holon identifier
1693
1587
  * @param {string} lens - The lens identifier
1694
1588
  * @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
1589
+ * @param {string} [password] - Optional password for accessing private data
1701
1590
  * @returns {Promise<Array>} - Combined array of local and federated data
1702
1591
  */
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 };
1592
+ async getFederated(holon, lens, options = {}, password = null) {
1593
+ return Federation.getFederated(this, holon, lens, options, password);
1594
+ }
1765
1595
 
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);
1596
+ /**
1597
+ * Closes the HoloSphere instance and cleans up resources.
1598
+ * @returns {Promise<void>}
1599
+ */
1600
+ async close() {
1601
+ try {
1602
+ if (this.gun) {
1603
+ // Unsubscribe from all subscriptions
1604
+ const subscriptionIds = Object.keys(this.subscriptions);
1605
+ for (const id of subscriptionIds) {
1606
+ try {
1607
+ const subscription = this.subscriptions[id];
1608
+ if (subscription && subscription.active) {
1609
+ // Turn off the Gun subscription
1610
+ this.gun.get(this.appname)
1611
+ .get(subscription.holon)
1612
+ .get(subscription.lens)
1613
+ .map().off();
1614
+
1615
+ // Mark as inactive
1616
+ subscription.active = false;
1770
1617
  }
1618
+ } catch (error) {
1619
+ console.warn(`Error cleaning up subscription ${id}:`, error);
1771
1620
  }
1621
+ }
1622
+
1623
+ // Clear subscriptions
1624
+ this.subscriptions = {};
1772
1625
 
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));
1626
+ // Close Gun connections
1627
+ if (this.gun.back) {
1628
+ try {
1629
+ const mesh = this.gun.back('opt.mesh');
1630
+ if (mesh && mesh.hear) {
1631
+ try {
1632
+ // Safely clear mesh.hear without modifying function properties
1633
+ const hearKeys = Object.keys(mesh.hear);
1634
+ for (const key of hearKeys) {
1635
+ // Check if it's an array before trying to clear it
1636
+ if (Array.isArray(mesh.hear[key])) {
1637
+ mesh.hear[key] = [];
1638
+ }
1639
+ }
1640
+
1641
+ // Create a new empty object for mesh.hear
1642
+ // Only if mesh.hear is not a function
1643
+ if (typeof mesh.hear !== 'function') {
1644
+ mesh.hear = {};
1645
+ }
1646
+ } catch (meshError) {
1647
+ console.warn('Error cleaning up Gun mesh hear:', meshError);
1648
+ }
1782
1649
  }
1650
+ } catch (error) {
1651
+ console.warn('Error accessing Gun mesh:', error);
1783
1652
  }
1653
+ }
1784
1654
 
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);
1655
+ // Clear all Gun instance listeners
1656
+ try {
1657
+ this.gun.off();
1658
+ } catch (error) {
1659
+ console.warn('Error turning off Gun listeners:', error);
1800
1660
  }
1661
+
1662
+ // Wait a moment for cleanup to complete
1663
+ await new Promise(resolve => setTimeout(resolve, 100));
1801
1664
  }
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;
1665
+
1666
+ console.log('HoloSphere instance closed successfully');
1667
+ } catch (error) {
1668
+ console.error('Error closing HoloSphere instance:', error);
1809
1669
  }
1670
+ }
1810
1671
 
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;
1672
+ /**
1673
+ * Gets the name of a chat/space
1674
+ * @param {string} spaceId - The space ID
1675
+ * @param {string} [password] - Optional password for the space
1676
+ * @returns {Promise<string>} - The space name or the ID if not found
1677
+ */
1678
+ async getChatName(spaceId, password = null) {
1679
+ const spaceInfo = await this.getGlobal('spaces', spaceId, password);
1680
+ return spaceInfo?.name || spaceId;
1681
+ }
1816
1682
 
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());
1683
+ /**
1684
+ * Creates a namespaced username for Gun authentication
1685
+ * @private
1686
+ * @param {string} spaceId - The space ID
1687
+ * @returns {string} - Namespaced username
1688
+ */
1689
+ userName(spaceId) {
1690
+ if (!spaceId) return null;
1691
+ return `${this.appname}:${spaceId}`;
1824
1692
  }
1825
1693
  }
1826
1694