holosphere 1.1.4 → 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,6 +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) {
18
+ console.log('HoloSphere v1.1.6');
17
19
  this.appname = appname
18
20
  this.strict = strict;
19
21
  this.validator = new Ajv2019({
@@ -22,14 +24,17 @@ class HoloSphere {
22
24
  validateSchema: true // Always validate schemas
23
25
  });
24
26
 
25
- // Use provided Gun instance or create new one
26
- this.gun = gunInstance || Gun({
27
- peers: ['https://gun.holons.io/gun', 'https://59.src.eco/gun'],
28
- axe: false,
29
- // uuid: (content) => { // generate a unique id for each node
30
- // console.log('uuid', content);
31
- // return content;}
32
- });
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
+ }
33
38
 
34
39
  // Initialize SEA
35
40
  this.sea = SEA;
@@ -40,9 +45,6 @@ class HoloSphere {
40
45
  });
41
46
  }
42
47
 
43
- // Add currentSpace property to track logged in space
44
- this.currentSpace = null;
45
-
46
48
  // Initialize subscriptions
47
49
  this.subscriptions = {};
48
50
  }
@@ -65,68 +67,49 @@ class HoloSphere {
65
67
  throw new Error('setSchema: Schema must have a type field');
66
68
  }
67
69
 
68
- if (this.strict) {
69
- const metaSchema = {
70
- type: 'object',
71
- required: ['type', 'properties'],
70
+ const metaSchema = {
71
+ type: 'object',
72
+ required: ['type', 'properties'],
73
+ properties: {
74
+ type: { type: 'string' },
72
75
  properties: {
73
- type: { type: 'string' },
74
- properties: {
76
+ type: 'object',
77
+ additionalProperties: {
75
78
  type: 'object',
76
- additionalProperties: {
77
- type: 'object',
78
- required: ['type'],
79
- properties: {
80
- type: { type: 'string' }
81
- }
79
+ required: ['type'],
80
+ properties: {
81
+ type: { type: 'string' }
82
82
  }
83
- },
84
- required: {
85
- type: 'array',
86
- items: { type: 'string' }
87
83
  }
84
+ },
85
+ required: {
86
+ type: 'array',
87
+ items: { type: 'string' }
88
88
  }
89
- };
90
-
91
- const valid = this.validator.validate(metaSchema, schema);
92
- if (!valid) {
93
- throw new Error(`Invalid schema structure: ${JSON.stringify(this.validator.errors)}`);
94
- }
95
-
96
- if (!schema.properties || typeof schema.properties !== 'object') {
97
- throw new Error('Schema must have properties in strict mode');
98
89
  }
90
+ };
99
91
 
100
- if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
101
- throw new Error('Schema must have required fields in strict mode');
102
- }
92
+ const valid = this.validator.validate(metaSchema, schema);
93
+ if (!valid) {
94
+ throw new Error(`Invalid schema structure: ${JSON.stringify(this.validator.errors)}`);
103
95
  }
104
96
 
105
- return new Promise((resolve, reject) => {
106
- try {
107
- const schemaString = JSON.stringify(schema);
108
- const schemaData = {
109
- schema: schemaString,
110
- timestamp: Date.now(),
111
- // Only set owner if there's an authenticated space
112
- ...(this.currentSpace && { owner: this.currentSpace.alias })
113
- };
97
+ if (!schema.properties || typeof schema.properties !== 'object') {
98
+ throw new Error('Schema must have properties in strict mode');
99
+ }
114
100
 
115
- this.gun.get(this.appname)
116
- .get(lens)
117
- .get('schema')
118
- .put(schemaData, ack => {
119
- if (ack.err) {
120
- reject(new Error(ack.err));
121
- } else {
122
- // Add small delay to ensure data is written
123
- setTimeout(() => resolve(true), 50);
124
- }
125
- });
126
- } catch (error) {
127
- reject(error);
128
- }
101
+ if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
102
+ throw new Error('Schema must have required fields in strict mode');
103
+ }
104
+
105
+ // Store schema in global table with lens as key
106
+ await this.putGlobal('schemas', {
107
+ id: lens,
108
+ schema: schema,
109
+ timestamp: Date.now()
129
110
  });
111
+
112
+ return true;
130
113
  }
131
114
 
132
115
  /**
@@ -139,39 +122,12 @@ class HoloSphere {
139
122
  throw new Error('getSchema: Missing lens parameter');
140
123
  }
141
124
 
142
- return new Promise((resolve) => {
143
- let timeout = setTimeout(() => {
144
- console.warn('getSchema: Operation timed out');
145
- resolve(null);
146
- }, 5000);
147
-
148
- this.gun.get(this.appname)
149
- .get(lens)
150
- .get('schema')
151
- .once(data => {
152
- clearTimeout(timeout);
153
- if (!data) {
154
- resolve(null);
155
- return;
156
- }
125
+ const schemaData = await this.getGlobal('schemas', lens);
126
+ if (!schemaData || !schemaData.schema) {
127
+ return null;
128
+ }
157
129
 
158
- try {
159
- // Handle both new format and legacy format
160
- if (data.schema) {
161
- // New format with timestamp
162
- resolve(JSON.parse(data.schema));
163
- } else {
164
- // Legacy format or direct string
165
- const schemaStr = typeof data === 'string' ? data :
166
- Object.values(data).find(v => typeof v === 'string' && v.includes('"type":'));
167
- resolve(schemaStr ? JSON.parse(schemaStr) : null);
168
- }
169
- } catch (error) {
170
- console.error('getSchema: Error parsing schema:', error);
171
- resolve(null);
172
- }
173
- });
174
- });
130
+ return schemaData.schema;
175
131
  }
176
132
 
177
133
  // ================================ CONTENT FUNCTIONS ================================
@@ -181,148 +137,142 @@ class HoloSphere {
181
137
  * @param {string} holon - The holon identifier.
182
138
  * @param {string} lens - The lens under which to store the content.
183
139
  * @param {object} data - The data to store.
140
+ * @param {string} [password] - Optional password for private space.
184
141
  * @returns {Promise<boolean>} - Returns true if successful, false if there was an error
185
142
  */
186
- async put(holon, lens, data) {
187
- // Check authentication for data operations
188
- if (!this.currentSpace) {
189
- throw new Error('Unauthorized to modify this data');
190
- }
191
- this._checkSession();
192
-
193
- // If updating existing data, check ownership
194
- if (data.id) {
195
- const existing = await this.get(holon, lens, data.id);
196
- if (existing && existing.owner &&
197
- existing.owner !== this.currentSpace.alias &&
198
- !existing.federation) { // Skip ownership check for federated data
199
- throw new Error('Unauthorized to modify this data');
200
- }
201
- }
202
-
203
- // Add owner and federation information to data
204
- const dataWithMeta = {
205
- ...data,
206
- owner: this.currentSpace.alias,
207
- federation: {
208
- origin: this.currentSpace.alias,
209
- timestamp: Date.now()
210
- }
211
- };
212
-
213
- if (!holon || !lens || !dataWithMeta) {
143
+ async put(holon, lens, data, password = null) {
144
+ if (!holon || !lens || !data) {
214
145
  throw new Error('put: Missing required parameters');
215
146
  }
216
147
 
217
- if (!dataWithMeta.id) {
218
- dataWithMeta.id = this.generateId();
148
+ if (!data.id) {
149
+ data.id = this.generateId();
219
150
  }
220
151
 
221
- // Get and validate schema first
222
- const schema = await this.getSchema(lens);
223
- if (schema) {
224
- // Deep clone data to avoid modifying the original
225
- 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));
226
159
  const valid = this.validator.validate(schema, dataToValidate);
227
160
 
228
161
  if (!valid) {
229
162
  const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
230
- // Always throw on schema validation failure, regardless of strict mode
231
163
  throw new Error(errorMsg);
232
164
  }
233
- } else if (this.strict) {
234
- throw new Error('Schema required in strict mode');
235
165
  }
236
166
 
237
- // Store data in current space
238
- const putResult = await new Promise((resolve, reject) => {
239
- try {
240
- const payload = JSON.stringify(dataWithMeta);
241
- this.gun.get(this.appname)
242
- .get(holon)
243
- .get(lens)
244
- .get(dataWithMeta.id)
245
- .put(payload, ack => {
246
- if (ack.err) {
247
- reject(new Error(ack.err));
248
- } else {
249
- // Notify subscribers after successful put
250
- this.notifySubscribers({
251
- holon,
252
- lens,
253
- ...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
+ }
254
211
  });
255
- 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');
256
231
  }
257
- });
258
- } catch (error) {
259
- reject(error);
260
- }
261
- });
262
-
263
- // If successful, propagate to federated spaces
264
- if (putResult) {
265
- await this._propagateToFederation(holon, lens, dataWithMeta);
266
- }
267
-
268
- return putResult;
269
- }
270
-
271
- /**
272
- * Propagates data to federated spaces
273
- * @private
274
- * @param {string} holon - The holon identifier
275
- * @param {string} lens - The lens identifier
276
- * @param {object} data - The data to propagate
277
- */
278
- async _propagateToFederation(holon, lens, data) {
279
- try {
280
- // Get federation info for current space
281
- const fedInfo = await this.getFederation(this.currentSpace.alias);
282
- if (!fedInfo || !fedInfo.notify || fedInfo.notify.length === 0) {
283
- return; // No federation to propagate to
232
+ }
233
+ }
284
234
  }
285
235
 
286
- // Propagate to each federated space
287
- const propagationPromises = fedInfo.notify.map(spaceId =>
288
- new Promise((resolve) => {
289
- // Store data in the federated space's lens
290
- this.gun.get(this.appname)
291
- .get(spaceId)
292
- .get(lens)
293
- .get(data.id)
294
- .put(JSON.stringify({
295
- ...data,
296
- federation: {
297
- ...data.federation,
298
- notified: Date.now()
299
- }
300
- }), 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 => {
301
243
  if (ack.err) {
302
- 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);
303
252
  }
304
- resolve();
305
253
  });
306
-
307
- // Also store in federation lens for notifications
308
- this.gun.get(this.appname)
309
- .get(spaceId)
310
- .get('federation')
311
- .get(data.id)
312
- .put(JSON.stringify({
313
- ...data,
314
- federation: {
315
- ...data.federation,
316
- 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);
317
266
  }
318
- }));
319
- })
320
- );
321
-
322
- await Promise.all(propagationPromises);
267
+ });
268
+ }
269
+ } catch (error) {
270
+ reject(error);
271
+ }
272
+ });
323
273
  } catch (error) {
324
- console.warn('Federation propagation error:', error);
325
- // Don't throw here to avoid failing the original put
274
+ console.error('Error in put:', error);
275
+ throw error;
326
276
  }
327
277
  }
328
278
 
@@ -330,151 +280,160 @@ class HoloSphere {
330
280
  * Retrieves content from the specified holon and lens.
331
281
  * @param {string} holon - The holon identifier.
332
282
  * @param {string} lens - The lens from which to retrieve content.
333
- * @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.
334
286
  */
335
- async getAll(holon, lens) {
336
- if (!holon || !lens) {
337
- throw new Error('getAll: Missing required parameters');
338
- }
339
-
340
- const schema = await this.getSchema(lens);
341
- if (!schema && this.strict) {
342
- throw new Error('getAll: Schema required in strict mode');
343
- }
344
-
345
- // Get local data
346
- const localData = await this._getAllLocal(holon, lens, schema);
347
-
348
- // If authenticated, get federated data
349
- let federatedData = [];
350
- if (this.currentSpace) {
351
- 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;
352
291
  }
353
292
 
354
- // Combine and deduplicate data based on ID
355
- const combined = new Map();
356
-
357
- // Add local data first
358
- localData.forEach(item => {
359
- if (item.id) {
360
- 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');
361
299
  }
362
- });
300
+ }
363
301
 
364
- // Add federated data, potentially overwriting local data if newer
365
- federatedData.forEach(item => {
366
- if (item.id) {
367
- const existing = combined.get(item.id);
368
- if (!existing ||
369
- (item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
370
- 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
+ });
371
326
  }
372
327
  }
373
- });
374
328
 
375
- return Array.from(combined.values());
376
- }
329
+ return new Promise((resolve) => {
330
+ const handleData = async (data) => {
331
+ if (!data) {
332
+ resolve(null);
333
+ return;
334
+ }
377
335
 
378
- /**
379
- * Gets data from federated spaces
380
- * @private
381
- * @param {string} holon - The holon identifier
382
- * @param {string} lens - The lens identifier
383
- * @param {string} key - The key to get
384
- * @returns {Promise<object|null>} - The federated data or null if not found
385
- */
386
- async _getFederatedData(holon, lens, key) {
387
- try {
388
- const fedInfo = await this.getFederation(this.currentSpace.alias);
389
- if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
390
- return null;
391
- }
336
+ try {
337
+ const parsed = await this.parse(data);
392
338
 
393
- // Try each federated space
394
- for (const spaceId of fedInfo.federation) {
395
- const result = await new Promise((resolve) => {
396
- this.gun.get(this.appname)
397
- .get(spaceId)
398
- .get(lens)
399
- .get(key)
400
- .once(async (data) => {
401
- if (!data) {
402
- resolve(null);
403
- return;
404
- }
405
- try {
406
- const parsed = await this.parse(data);
407
- resolve(parsed);
408
- } catch (error) {
409
- console.warn(`Error parsing federated data from ${spaceId}:`, error);
410
- 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
+ }
411
347
  }
412
- });
413
- });
348
+ }
414
349
 
415
- if (result) {
416
- 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);
417
363
  }
418
- }
364
+ });
419
365
  } catch (error) {
420
- console.warn('Federation get error:', error);
366
+ console.error('Error in get:', error);
367
+ return null;
421
368
  }
422
- return null;
423
369
  }
424
370
 
425
371
  /**
426
- * Gets all data from local space
427
- * @private
372
+ * Propagates data to federated spaces
428
373
  * @param {string} holon - The holon identifier
429
374
  * @param {string} lens - The lens identifier
430
- * @param {object} schema - The schema to validate against
431
- * @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
432
378
  */
433
- async _getAllLocal(holon, lens, schema) {
434
- return new Promise((resolve) => {
435
- const output = new Map();
436
- let isResolved = false;
437
- let listener = null;
438
-
439
- const hardTimeout = setTimeout(() => {
440
- cleanup();
441
- resolve(Array.from(output.values()));
442
- }, 5000);
443
-
444
- const cleanup = () => {
445
- if (listener) {
446
- listener.off();
447
- }
448
- clearTimeout(hardTimeout);
449
- isResolved = true;
450
- };
379
+ async propagateToFederation(holon, lens, data, options = {}) {
380
+ return Federation.propagateToFederation(this, holon, lens, data, options);
381
+ }
451
382
 
452
- const processData = async (data, key) => {
453
- 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
+ }
454
391
 
455
- try {
456
- const parsed = await this.parse(data);
457
- 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
+ }
403
+
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;
458
421
 
459
- if (schema) {
460
- const valid = this.validator.validate(schema, parsed);
461
- if (valid || !this.strict) {
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 {
462
428
  output.set(parsed.id, parsed);
463
429
  }
464
- } else {
465
- output.set(parsed.id, parsed);
430
+ } catch (error) {
431
+ console.error('Error processing data:', error);
466
432
  }
467
- } catch (error) {
468
- console.error('Error processing data:', error);
469
- }
470
- };
433
+ };
471
434
 
472
- this.gun.get(this.appname)
473
- .get(holon)
474
- .get(lens)
475
- .once(async (data) => {
435
+ const handleData = async (data) => {
476
436
  if (!data) {
477
- cleanup();
478
437
  resolve([]);
479
438
  return;
480
439
  }
@@ -488,75 +447,23 @@ class HoloSphere {
488
447
 
489
448
  try {
490
449
  await Promise.all(initialPromises);
491
- cleanup();
492
450
  resolve(Array.from(output.values()));
493
451
  } catch (error) {
494
- cleanup();
452
+ console.error('Error in getAll:', error);
495
453
  resolve([]);
496
454
  }
497
- });
498
- });
499
- }
500
-
501
- /**
502
- * Gets all data from federated spaces
503
- * @private
504
- * @param {string} holon - The holon identifier
505
- * @param {string} lens - The lens identifier
506
- * @param {object} schema - The schema to validate against
507
- * @returns {Promise<Array>} - Array of federated data
508
- */
509
- async _getAllFederated(holon, lens, schema) {
510
- try {
511
- const fedInfo = await this.getFederation(this.currentSpace.alias);
512
- if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
513
- return [];
514
- }
515
-
516
- const federatedData = new Map();
517
-
518
- // Get data from each federated space
519
- const fedPromises = fedInfo.federation.map(spaceId =>
520
- new Promise((resolve) => {
521
- this.gun.get(this.appname)
522
- .get(spaceId)
523
- .get(lens)
524
- .once(async (data) => {
525
- if (!data) {
526
- resolve();
527
- return;
528
- }
529
-
530
- const processPromises = Object.keys(data)
531
- .filter(key => key !== '_')
532
- .map(async key => {
533
- try {
534
- const parsed = await this.parse(data[key]);
535
- if (parsed && parsed.id) {
536
- if (schema) {
537
- const valid = this.validator.validate(schema, parsed);
538
- if (valid || !this.strict) {
539
- federatedData.set(parsed.id, parsed);
540
- }
541
- } else {
542
- federatedData.set(parsed.id, parsed);
543
- }
544
- }
545
- } catch (error) {
546
- console.warn(`Error processing federated data from ${spaceId}:`, error);
547
- }
548
- });
549
-
550
- await Promise.all(processPromises);
551
- resolve();
552
- });
553
- })
554
- );
455
+ };
555
456
 
556
- await Promise.all(fedPromises);
557
- 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
+ });
558
465
  } catch (error) {
559
- console.warn('Federation getAll error:', error);
466
+ console.error('Error in getAll:', error);
560
467
  return [];
561
468
  }
562
469
  }
@@ -567,11 +474,19 @@ class HoloSphere {
567
474
  * @returns {Promise<object>} - The parsed data.
568
475
  */
569
476
  async parse(rawData) {
477
+ let parsedData = {};
478
+
570
479
  if (!rawData) {
571
480
  throw new Error('parse: No data provided');
572
481
  }
573
482
 
574
483
  try {
484
+
485
+ if (typeof rawData === 'string') {
486
+ parsedData = await JSON.parse(rawData);
487
+ }
488
+
489
+
575
490
  if (rawData.soul) {
576
491
  const data = await this.getNodeRef(rawData.soul).once();
577
492
  if (!data) {
@@ -580,7 +495,7 @@ class HoloSphere {
580
495
  return JSON.parse(data);
581
496
  }
582
497
 
583
- let parsedData = {};
498
+
584
499
  if (typeof rawData === 'object' && rawData !== null) {
585
500
  if (rawData._ && rawData._["#"]) {
586
501
  const pathParts = rawData._['#'].split('/');
@@ -600,11 +515,10 @@ class HoloSphere {
600
515
  } else {
601
516
  parsedData = rawData;
602
517
  }
603
- } else {
604
- parsedData = JSON.parse(rawData);
605
518
  }
606
519
 
607
520
  return parsedData;
521
+
608
522
  } catch (error) {
609
523
  console.log("Parsing not a JSON, returning raw data", rawData);
610
524
  return rawData;
@@ -612,186 +526,116 @@ class HoloSphere {
612
526
  }
613
527
  }
614
528
 
615
- /**
616
- * Retrieves a specific key from the specified holon and lens.
617
- * @param {string} holon - The holon identifier.
618
- * @param {string} lens - The lens from which to retrieve the key.
619
- * @param {string} key - The specific key to retrieve.
620
- * @returns {Promise<object|null>} - The retrieved content or null if not found.
621
- */
622
- async get(holon, lens, key) {
623
- if (!holon || !lens || !key) {
624
- console.error('get: Missing required parameters:', { holon, lens, key });
625
- return null;
626
- }
627
-
628
- // Get schema for validation
629
- const schema = await this.getSchema(lens);
630
-
631
- // First try to get from current space
632
- const localResult = await new Promise((resolve) => {
633
- let timeout = setTimeout(() => {
634
- console.warn('get: Operation timed out');
635
- resolve(null);
636
- }, 5000);
637
-
638
- this.gun.get(this.appname)
639
- .get(holon)
640
- .get(lens)
641
- .get(key)
642
- .once(async (data) => {
643
- clearTimeout(timeout);
644
-
645
- if (!data) {
646
- resolve(null);
647
- return;
648
- }
649
-
650
- try {
651
- const parsed = await this.parse(data);
652
-
653
- // Validate against schema if one exists
654
- if (schema) {
655
- const valid = this.validator.validate(schema, parsed);
656
- if (!valid) {
657
- console.error('get: Invalid data according to schema:', this.validator.errors);
658
- if (this.strict) {
659
- resolve(null);
660
- return;
661
- }
662
- }
663
- }
664
-
665
- // Check if user has access - only allow if:
666
- // 1. No owner (public data)
667
- // 2. User is the owner
668
- // 3. User is in shared list
669
- // 4. Data is from federation
670
- if (parsed.owner &&
671
- this.currentSpace?.alias !== parsed.owner &&
672
- (!parsed.shared || !parsed.shared.includes(this.currentSpace?.alias)) &&
673
- (!parsed.federation || !parsed.federation.origin)) {
674
- resolve(null);
675
- return;
676
- }
677
-
678
- resolve(parsed);
679
- } catch (error) {
680
- console.error('Error parsing data:', error);
681
- resolve(null);
682
- }
683
- });
684
- });
685
-
686
- // If found locally, return it
687
- if (localResult) {
688
- return localResult;
689
- }
690
-
691
- // If not found locally and we're authenticated, try federated spaces
692
- if (this.currentSpace) {
693
- const fedResult = await this._getFederatedData(holon, lens, key);
694
- if (fedResult) {
695
- return fedResult;
696
- }
697
- }
698
-
699
- return null;
700
- }
701
-
702
529
  /**
703
530
  * Deletes a specific key from a given holon and lens.
704
531
  * @param {string} holon - The holon identifier.
705
532
  * @param {string} lens - The lens from which to delete the key.
706
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
707
536
  */
708
- async delete(holon, lens, key) {
537
+ async delete(holon, lens, key, password = null) {
709
538
  if (!holon || !lens || !key) {
710
539
  throw new Error('delete: Missing required parameters');
711
540
  }
712
541
 
713
- if (!this.currentSpace) {
714
- throw new Error('Unauthorized to delete this data');
715
- }
716
- this._checkSession();
717
-
718
- // Check ownership before delete
719
- const data = await this.get(holon, lens, key);
720
- if (!data) {
721
- return true; // Nothing to delete
722
- }
723
-
724
- if (data.owner && data.owner !== this.currentSpace.alias) {
725
- throw new Error('Unauthorized to delete this data');
726
- }
727
-
728
- return new Promise((resolve, reject) => {
729
- try {
730
- this.gun.get(this.appname)
731
- .get(holon)
732
- .get(lens)
733
- .get(key)
734
- .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 => {
735
551
  if (ack.err) {
736
552
  reject(new Error(ack.err));
737
553
  } else {
738
554
  resolve(true);
739
555
  }
740
556
  });
741
- } catch (error) {
742
- reject(error);
743
- }
744
- });
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
+ }
745
572
  }
746
573
 
747
574
  /**
748
575
  * Deletes all keys from a given holon and lens.
749
576
  * @param {string} holon - The holon identifier.
750
577
  * @param {string} lens - The lens from which to delete all keys.
751
- * @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
752
580
  */
753
- async deleteAll(holon, lens) {
581
+ async deleteAll(holon, lens, password = null) {
754
582
  if (!holon || !lens) {
755
583
  console.error('deleteAll: Missing holon or lens parameter');
756
584
  return false;
757
585
  }
758
586
 
759
- return new Promise((resolve) => {
760
- 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
+ }
761
604
 
762
- // First get all the data to find keys to delete
763
- this.gun.get(this.appname).get(holon).get(lens).once((data) => {
764
- if (!data) {
765
- resolve(true); // Nothing to delete
766
- return;
767
- }
605
+ // Get all keys except Gun's metadata key '_'
606
+ const keys = Object.keys(data).filter(key => key !== '_');
768
607
 
769
- // Get all keys except Gun's metadata key '_'
770
- 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);
771
615
 
772
- // Create deletion promises for each key
773
- keys.forEach(key => {
774
- deletionPromises.push(
775
- new Promise((resolveDelete) => {
776
- this.gun.get(this.appname).get(holon).get(lens).get(key).put(null, ack => {
777
- resolveDelete(!!ack.ok); // Convert to boolean
778
- });
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);
779
628
  })
780
- );
629
+ .catch(error => {
630
+ console.error('Error in deleteAll:', error);
631
+ resolve(false);
632
+ });
781
633
  });
782
-
783
- // Wait for all deletions to complete
784
- Promise.all(deletionPromises)
785
- .then(results => {
786
- const allSuccessful = results.every(result => result === true);
787
- resolve(allSuccessful);
788
- })
789
- .catch(error => {
790
- console.error('Error in deleteAll:', error);
791
- resolve(false);
792
- });
793
634
  });
794
- });
635
+ } catch (error) {
636
+ console.error('Error in deleteAll:', error);
637
+ return false;
638
+ }
795
639
  }
796
640
 
797
641
  // ================================ NODE FUNCTIONS ================================
@@ -908,243 +752,418 @@ class HoloSphere {
908
752
  * Stores data in a global (non-holon-specific) table.
909
753
  * @param {string} tableName - The table name to store data in.
910
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.
911
756
  * @returns {Promise<void>}
912
757
  */
913
- async putGlobal(tableName, data) {
914
- return new Promise((resolve, reject) => {
915
- try {
916
- if (!tableName || !data) {
917
- throw new Error('Table name and data are required');
918
- }
758
+ async putGlobal(tableName, data, password = null) {
759
+ try {
760
+ if (!tableName || !data) {
761
+ throw new Error('Table name and data are required');
762
+ }
919
763
 
920
- if (data.id) {
921
- this.gun.get(this.appname).get(tableName).get(data.id).put(JSON.stringify(data), ack => {
922
- if (ack.err) {
923
- reject(new Error(ack.err));
924
- } else {
925
- resolve();
926
- }
927
- });
928
- } else {
929
- this.gun.get(this.appname).get(tableName).put(JSON.stringify(data), ack => {
930
- if (ack.err) {
931
- reject(new Error(ack.err));
932
- } else {
933
- resolve();
934
- }
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
+ });
935
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
+ }
936
821
  }
937
- } catch (error) {
938
- reject(error);
939
822
  }
940
- });
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
+ }
941
875
  }
942
876
 
943
877
  /**
944
- * Retrieves a specific key from a global table.
945
- * @param {string} tableName - The table name to retrieve from.
946
- * @param {string} key - The key to retrieve.
947
- * @returns {Promise<object|null>} - The parsed data for the key or null if not found.
948
- */
949
- async getGlobal(tableName, key) {
950
- return new Promise((resolve) => {
951
- this.gun.get(this.appname).get(tableName).get(key).once((data) => {
952
- if (!data) {
953
- resolve(null);
954
- return;
955
- }
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) {
956
889
  try {
957
- const parsed = this.parse(data);
958
- resolve(parsed);
959
- } catch (e) {
960
- 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);
961
950
  }
962
951
  });
963
- });
952
+ } catch (error) {
953
+ console.error('Error in getGlobal:', error);
954
+ return null;
955
+ }
964
956
  }
965
957
 
966
-
967
-
968
958
  /**
969
959
  * Retrieves all data from a global table.
970
960
  * @param {string} tableName - The table name to retrieve data from.
971
- * @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.
972
963
  */
973
- async getAllGlobal(tableName) {
964
+ async getAllGlobal(tableName, password = null) {
974
965
  if (!tableName) {
975
966
  throw new Error('getAllGlobal: Missing table name parameter');
976
967
  }
977
968
 
978
- return new Promise((resolve) => {
979
- let output = [];
980
- let isResolved = false;
981
- let timeout = setTimeout(() => {
982
- if (!isResolved) {
983
- isResolved = true;
984
- resolve(output);
985
- }
986
- }, 5000);
969
+ try {
970
+ // Get the appropriate space
971
+ const user = this.gun.user();
987
972
 
988
- this.gun.get(this.appname).get(tableName).once(async (data) => {
989
- if (!data) {
990
- clearTimeout(timeout);
991
- isResolved = true;
992
- resolve([]);
993
- return;
994
- }
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);
995
982
 
996
- const keys = Object.keys(data).filter(key => key !== '_');
997
- const promises = keys.map(key =>
998
- new Promise(async (resolveItem) => {
999
- const itemData = await new Promise(resolveData => {
1000
- this.gun.get(this.appname).get(tableName).get(key).once(resolveData);
1001
- });
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
+ };
1002
1021
 
1003
- if (itemData) {
1004
- try {
1005
- const parsed = await this.parse(itemData);
1006
- if (parsed) output.push(parsed);
1007
- } catch (error) {
1008
- console.error('Error parsing data:', error);
1009
- }
1010
- }
1011
- resolveItem();
1012
- })
1013
- );
1014
-
1015
- await Promise.all(promises);
1016
- clearTimeout(timeout);
1017
- if (!isResolved) {
1018
- isResolved = true;
1019
- 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);
1020
1028
  }
1021
1029
  });
1022
- });
1030
+ } catch (error) {
1031
+ console.error('Error in getAllGlobal:', error);
1032
+ return [];
1033
+ }
1023
1034
  }
1035
+
1024
1036
  /**
1025
1037
  * Deletes a specific key from a global table.
1026
1038
  * @param {string} tableName - The table name to delete from.
1027
1039
  * @param {string} key - The key to delete.
1028
- * @returns {Promise<void>}
1040
+ * @param {string} [password] - Optional password for private space.
1041
+ * @returns {Promise<boolean>}
1029
1042
  */
1030
- async deleteGlobal(tableName, key) {
1043
+ async deleteGlobal(tableName, key, password = null) {
1031
1044
  if (!tableName || !key) {
1032
1045
  throw new Error('deleteGlobal: Missing required parameters');
1033
1046
  }
1034
1047
 
1035
- // Only check authentication for non-spaces tables
1036
- if (tableName !== 'spaces' && !this.currentSpace) {
1037
- throw new Error('Unauthorized to delete this data');
1038
- }
1039
-
1040
- // Skip session check for spaces table
1041
- if (tableName !== 'spaces') {
1042
- this._checkSession();
1043
- }
1048
+ try {
1049
+ // Get the appropriate space
1050
+ const user = this.gun.user();
1044
1051
 
1045
- return new Promise((resolve, reject) => {
1046
- try {
1047
- this.gun.get(this.appname)
1048
- .get(tableName)
1049
- .get(key)
1050
- .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 => {
1051
1056
  if (ack.err) {
1052
1057
  reject(new Error(ack.err));
1053
1058
  } else {
1054
1059
  resolve(true);
1055
1060
  }
1056
1061
  });
1057
- } catch (error) {
1058
- reject(error);
1059
- }
1060
- });
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
+ }
1061
1077
  }
1062
1078
 
1063
1079
  /**
1064
1080
  * Deletes an entire global table.
1065
1081
  * @param {string} tableName - The table name to delete.
1066
- * @returns {Promise<void>}
1082
+ * @param {string} [password] - Optional password for private space.
1083
+ * @returns {Promise<boolean>}
1067
1084
  */
1068
- async deleteAllGlobal(tableName) {
1085
+ async deleteAllGlobal(tableName, password = null) {
1069
1086
  if (!tableName) {
1070
1087
  throw new Error('deleteAllGlobal: Missing table name parameter');
1071
1088
  }
1072
1089
 
1073
- // Only check authentication for non-spaces and non-federation tables
1074
- if (!['spaces', 'federation'].includes(tableName) && !this.currentSpace) {
1075
- throw new Error('Unauthorized to delete this data');
1076
- }
1090
+ try {
1091
+ // Get the appropriate space
1092
+ const user = this.gun.user();
1077
1093
 
1078
- // Skip session check for spaces and federation tables
1079
- if (!['spaces', 'federation'].includes(tableName)) {
1080
- this._checkSession();
1081
- }
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);
1082
1102
 
1083
- return new Promise((resolve, reject) => {
1084
- try {
1085
- const deletions = new Set();
1086
- let timeout = setTimeout(() => {
1087
- if (deletions.size === 0) {
1088
- resolve(true); // No data to delete
1089
- }
1090
- }, 5000);
1103
+ const dataPath = password ?
1104
+ user.get('private').get(tableName) :
1105
+ this.gun.get(this.appname).get(tableName);
1091
1106
 
1092
- this.gun.get(this.appname).get(tableName).once(async (data) => {
1093
- if (!data) {
1094
- clearTimeout(timeout);
1095
- resolve(true);
1096
- return;
1097
- }
1107
+ dataPath.once(async (data) => {
1108
+ if (!data) {
1109
+ clearTimeout(timeout);
1110
+ resolve(true);
1111
+ return;
1112
+ }
1098
1113
 
1099
- const keys = Object.keys(data).filter(key => key !== '_');
1100
- const promises = keys.map(key =>
1101
- new Promise((resolveDelete) => {
1102
- this.gun.get(this.appname)
1103
- .get(tableName)
1104
- .get(key)
1105
- .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 => {
1106
1122
  if (ack.err) {
1107
1123
  console.error(`Failed to delete ${key}:`, ack.err);
1108
1124
  }
1109
1125
  resolveDelete();
1110
1126
  });
1111
- })
1112
- );
1113
-
1114
- try {
1115
- await Promise.all(promises);
1116
- // Finally delete the table itself
1117
- this.gun.get(this.appname).get(tableName).put(null);
1118
- clearTimeout(timeout);
1119
- resolve(true);
1120
- } catch (error) {
1121
- reject(error);
1122
- }
1123
- });
1124
- } catch (error) {
1125
- reject(error);
1126
- }
1127
- });
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
+ }
1128
1148
  }
1129
1149
 
1130
1150
  // ================================ COMPUTE FUNCTIONS ================================
1131
1151
  /**
1132
-
1133
- /**
1134
- * Computes operations across multiple layers up the hierarchy
1135
- * @param {string} holon - Starting holon identifier
1136
- * @param {string} lens - The lens to compute
1137
- * @param {object} options - Computation options
1138
- * @param {number} [maxLevels=15] - Maximum levels to compute up
1139
- */
1140
- 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) {
1141
1160
  let currentHolon = holon;
1142
1161
  let currentRes = h3.getResolution(currentHolon);
1143
1162
  const results = [];
1144
1163
 
1145
1164
  while (currentRes > 0 && maxLevels > 0) {
1146
1165
  try {
1147
- const result = await this.compute(currentHolon, lens, options);
1166
+ const result = await this.compute(currentHolon, lens, options, password);
1148
1167
  if (result) {
1149
1168
  results.push(result);
1150
1169
  }
@@ -1160,16 +1179,18 @@ class HoloSphere {
1160
1179
  return results;
1161
1180
  }
1162
1181
 
1163
- /* 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.
1164
1184
  * @param {string} holon - The holon identifier.
1165
1185
  * @param {string} lens - The lens to compute.
1166
1186
  * @param {object} options - Computation options
1167
1187
  * @param {string} options.operation - The operation to perform ('summarize', 'aggregate', 'concatenate')
1168
1188
  * @param {string[]} [options.fields] - Fields to perform operation on
1169
1189
  * @param {string} [options.targetField] - Field to store the result in
1190
+ * @param {string} [password] - Optional password for private spaces
1170
1191
  * @throws {Error} If parameters are invalid or missing
1171
1192
  */
1172
- async compute(holon, lens, options) {
1193
+ async compute(holon, lens, options, password = null) {
1173
1194
  // Validate required parameters
1174
1195
  if (!holon || !lens) {
1175
1196
  throw new Error('compute: Missing required parameters');
@@ -1224,7 +1245,7 @@ class HoloSphere {
1224
1245
 
1225
1246
  // Collect all content from siblings
1226
1247
  const contents = await Promise.all(
1227
- siblings.map(sibling => this.getAll(sibling, lens))
1248
+ siblings.map(sibling => this.getAll(sibling, lens, password))
1228
1249
  );
1229
1250
 
1230
1251
  const flatContents = contents.flat().filter(Boolean);
@@ -1286,7 +1307,7 @@ class HoloSphere {
1286
1307
  result.value = computed;
1287
1308
  }
1288
1309
 
1289
- await this.put(parent, lens, result);
1310
+ await this.put(parent, lens, result, password);
1290
1311
  return result;
1291
1312
  }
1292
1313
  } catch (error) {
@@ -1421,307 +1442,143 @@ class HoloSphere {
1421
1442
  * @param {string} holon - The holon identifier.
1422
1443
  * @param {string} lens - The lens to subscribe to.
1423
1444
  * @param {function} callback - The callback to execute on changes.
1445
+ * @returns {Promise<object>} - Subscription object with unsubscribe method
1424
1446
  */
1425
1447
  async subscribe(holon, lens, callback) {
1426
- const subscriptionId = this.generateSubscriptionId();
1427
- this.subscriptions[subscriptionId] = {
1428
- query: { holon, lens },
1429
- callback,
1430
- active: true
1431
- };
1432
-
1433
- // Add cleanup to ensure callback isn't called after unsubscribe
1434
- return {
1435
- unsubscribe: () => {
1436
- if (this.subscriptions[subscriptionId]) {
1437
- delete this.subscriptions[subscriptionId];
1438
- }
1439
- }
1440
- };
1441
- }
1442
-
1443
- notifySubscribers(data) {
1444
- Object.values(this.subscriptions).forEach(subscription => {
1445
- if (subscription.active && this.matchesQuery(data, subscription.query)) {
1446
- subscription.callback(data);
1447
- }
1448
- });
1449
- }
1450
-
1451
- // Add ID generation method
1452
- generateId() {
1453
- return Date.now().toString(10) + Math.random().toString(2);
1454
- }
1455
-
1456
- generateSubscriptionId() {
1457
- return Date.now().toString(10) + Math.random().toString(2);
1458
- }
1459
-
1460
- matchesQuery(data, query) {
1461
- return data && query &&
1462
- data.holon === query.holon &&
1463
- data.lens === query.lens;
1464
- }
1465
-
1466
- /**
1467
- * Creates a new space with the given credentials
1468
- * @param {string} spacename - The space identifier/username
1469
- * @param {string} password - The space password
1470
- * @returns {Promise<boolean>} - True if space was created successfully
1471
- */
1472
- async createSpace(spacename, password) {
1473
- if (!spacename || !password) {
1474
- throw new Error('Invalid credentials format');
1475
- }
1476
-
1477
- // Check if space already exists
1478
- const existingSpace = await this.getGlobal('spaces', spacename);
1479
- if (existingSpace) {
1480
- throw new Error('Space already exists');
1448
+ if (!holon || !lens || typeof callback !== 'function') {
1449
+ throw new Error('subscribe: Missing required parameters');
1481
1450
  }
1482
1451
 
1452
+ const subscriptionId = this.generateId();
1453
+
1483
1454
  try {
1484
- // Generate key pair
1485
- const pair = await Gun.SEA.pair();
1486
-
1487
- // Create auth record with SEA
1488
- const salt = await Gun.SEA.random(64).toString('base64');
1489
- const hash = await Gun.SEA.work(password, salt);
1490
- const auth = {
1491
- salt: salt,
1492
- hash: hash,
1493
- pub: pair.pub
1455
+ // Create the subscription
1456
+ const gunSubscription = this.gun.get(this.appname).get(holon).get(lens).map().on(async (data, key) => {
1457
+ if (data) {
1458
+ try {
1459
+ let parsed = await this.parse(data);
1460
+ callback(parsed, key);
1461
+ } catch (error) {
1462
+ console.error('Error in subscribe:', error);
1463
+ }
1464
+ }
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
1494
1474
  };
1495
-
1496
- // Create space record with encrypted data
1497
- const space = {
1498
- alias: spacename,
1499
- auth: auth,
1500
- epub: pair.epub,
1501
- pub: pair.pub,
1502
- 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
+ }
1503
1492
  };
1504
-
1505
- await this.putGlobal('spaces', {
1506
- ...space,
1507
- id: spacename
1508
- });
1509
-
1510
- return true;
1511
1493
  } catch (error) {
1512
- throw new Error(`Space creation failed: ${error.message}`);
1494
+ console.error('Error creating subscription:', error);
1495
+ throw error;
1513
1496
  }
1514
1497
  }
1515
1498
 
1499
+
1516
1500
  /**
1517
- * Logs in to a space with the given credentials
1518
- * @param {string} spacename - The space identifier/username
1519
- * @param {string} password - The space password
1520
- * @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
1521
1504
  */
1522
- async login(spacename, password) {
1523
- // Validate input
1524
- if (!spacename || !password ||
1525
- typeof spacename !== 'string' ||
1526
- typeof password !== 'string') {
1527
- throw new Error('Invalid credentials format');
1505
+ notifySubscribers(data) {
1506
+ if (!data || !data.holon || !data.lens) {
1507
+ return;
1528
1508
  }
1529
-
1509
+
1530
1510
  try {
1531
- // Get space record
1532
- const space = await this.getGlobal('spaces', spacename);
1533
- if (!space || !space.auth) {
1534
- throw new Error('Invalid spacename or password');
1535
- }
1536
-
1537
- // Verify password using SEA
1538
- const hash = await Gun.SEA.work(password, space.auth.salt);
1539
- if (hash !== space.auth.hash) {
1540
- throw new Error('Invalid spacename or password');
1541
- }
1542
-
1543
- // Set current space with expiration
1544
- this.currentSpace = {
1545
- ...space,
1546
- exp: Date.now() + (24 * 60 * 60 * 1000) // 24 hour expiration
1547
- };
1548
-
1549
- 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
+ });
1550
1524
  } catch (error) {
1551
- throw new Error('Authentication failed');
1525
+ console.warn('Error notifying subscribers:', error);
1552
1526
  }
1553
1527
  }
1554
1528
 
1555
- /**
1556
- * Logs out the current space
1557
- * @returns {Promise<void>}
1558
- */
1559
- async logout() {
1560
- this.currentSpace = null;
1529
+ // Add ID generation method
1530
+ generateId() {
1531
+ return Date.now().toString(10) + Math.random().toString(2);
1561
1532
  }
1562
1533
 
1563
- /**
1564
- * Checks if the current session is valid
1565
- * @private
1566
- */
1567
- _checkSession() {
1568
- if (!this.currentSpace) {
1569
- throw new Error('No active session');
1570
- }
1571
- if (this.currentSpace.exp < Date.now()) {
1572
- this.currentSpace = null;
1573
- throw new Error('Session expired');
1574
- }
1575
- return true;
1576
- }
1534
+ // ================================ FEDERATION FUNCTIONS ================================
1577
1535
 
1578
1536
  /**
1579
1537
  * Creates a federation relationship between two spaces
1580
1538
  * @param {string} spaceId1 - The first space ID
1581
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
1582
1542
  * @returns {Promise<boolean>} - True if federation was created successfully
1583
1543
  */
1584
- async federate(spaceId1, spaceId2) {
1585
- if (!spaceId1 || !spaceId2) {
1586
- throw new Error('federate: Missing required parameters');
1587
- }
1588
-
1589
- // Get existing federation info for both spaces
1590
- let fedInfo1 = await this.getGlobal('federation', spaceId1);
1591
- let fedInfo2 = await this.getGlobal('federation', spaceId2);
1592
-
1593
- // Check if federation already exists
1594
- if (fedInfo1 && fedInfo1.federation && fedInfo1.federation.includes(spaceId2)) {
1595
- throw new Error('Federation already exists');
1596
- }
1597
-
1598
- // Create or update federation info for first space
1599
- if (!fedInfo1) {
1600
- fedInfo1 = {
1601
- id: spaceId1,
1602
- name: spaceId1,
1603
- federation: [],
1604
- notify: []
1605
- };
1606
- }
1607
- if (!fedInfo1.federation) fedInfo1.federation = [];
1608
- fedInfo1.federation.push(spaceId2);
1609
-
1610
- // Create or update federation info for second space
1611
- if (!fedInfo2) {
1612
- fedInfo2 = {
1613
- id: spaceId2,
1614
- name: spaceId2,
1615
- federation: [],
1616
- notify: []
1617
- };
1618
- }
1619
- if (!fedInfo2.notify) fedInfo2.notify = [];
1620
- fedInfo2.notify.push(spaceId1);
1621
-
1622
- // Save both federation records
1623
- await this.putGlobal('federation', fedInfo1);
1624
- await this.putGlobal('federation', fedInfo2);
1625
-
1626
- return true;
1544
+ async federate(spaceId1, spaceId2, password1, password2 = null) {
1545
+ return Federation.federate(this, spaceId1, spaceId2, password1, password2);
1627
1546
  }
1628
1547
 
1629
1548
  /**
1630
1549
  * Subscribes to federation notifications for a space
1631
1550
  * @param {string} spaceId - The space ID to subscribe to
1551
+ * @param {string} password - Password for the space
1632
1552
  * @param {function} callback - The callback to execute on notifications
1633
- * @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
1634
1557
  */
1635
- async subscribeFederation(spaceId, callback) {
1636
- if (!spaceId || !callback) {
1637
- throw new Error('subscribeFederation: Missing required parameters');
1638
- }
1639
-
1640
- // Get federation info
1641
- const fedInfo = await this.getGlobal('federation', spaceId);
1642
- if (!fedInfo) {
1643
- throw new Error('No federation info found for space');
1644
- }
1645
-
1646
- // Create subscription for each federated space
1647
- const subscriptions = [];
1648
- if (fedInfo.federation && fedInfo.federation.length > 0) {
1649
- for (const federatedSpace of fedInfo.federation) {
1650
- // Subscribe to all lenses in the federated space
1651
- const sub = await this.subscribe(federatedSpace, '*', async (data) => {
1652
- try {
1653
- // Only notify if the data has federation info and is from the federated space
1654
- if (data && data.federation && data.federation.origin === federatedSpace) {
1655
- await callback(data);
1656
- }
1657
- } catch (error) {
1658
- console.warn('Federation notification error:', error);
1659
- }
1660
- });
1661
- subscriptions.push(sub);
1662
- }
1663
- }
1664
-
1665
- // Return combined subscription object
1666
- return {
1667
- off: () => {
1668
- subscriptions.forEach(sub => {
1669
- if (sub && typeof sub.off === 'function') {
1670
- sub.off();
1671
- }
1672
- });
1673
- }
1674
- };
1558
+ async subscribeFederation(spaceId, password, callback, options = {}) {
1559
+ return Federation.subscribeFederation(this, spaceId, password, callback, options);
1675
1560
  }
1676
1561
 
1677
1562
  /**
1678
1563
  * Gets federation info for a space
1679
1564
  * @param {string} spaceId - The space ID
1565
+ * @param {string} [password] - Optional password for the space
1680
1566
  * @returns {Promise<object|null>} - Federation info or null if not found
1681
1567
  */
1682
- async getFederation(spaceId) {
1683
- if (!spaceId) {
1684
- throw new Error('getFederationInfo: Missing space ID');
1685
- }
1686
- return await this.getGlobal('federation', spaceId);
1568
+ async getFederation(spaceId, password = null) {
1569
+ return Federation.getFederation(this, spaceId, password);
1687
1570
  }
1688
1571
 
1689
1572
  /**
1690
1573
  * Removes a federation relationship between spaces
1691
1574
  * @param {string} spaceId1 - The first space ID
1692
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
1693
1578
  * @returns {Promise<boolean>} - True if federation was removed successfully
1694
1579
  */
1695
- async unfederate(spaceId1, spaceId2) {
1696
- if (!spaceId1 || !spaceId2) {
1697
- throw new Error('unfederate: Missing required parameters');
1698
- }
1699
-
1700
- // Get federation info for both spaces
1701
- const fedInfo1 = await this.getGlobal('federation', spaceId1);
1702
- const fedInfo2 = await this.getGlobal('federation', spaceId2);
1703
-
1704
- if (fedInfo1) {
1705
- fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
1706
- await this.putGlobal('federation', fedInfo1);
1707
- }
1708
-
1709
- if (fedInfo2) {
1710
- fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
1711
- await this.putGlobal('federation', fedInfo2);
1712
- }
1713
-
1714
- return true;
1715
- }
1716
-
1717
- /**
1718
- * Gets the name of a chat/space
1719
- * @param {string} spaceId - The space ID
1720
- * @returns {Promise<string>} - The space name or the ID if not found
1721
- */
1722
- async getChatName(spaceId) {
1723
- const spaceInfo = await this.getGlobal('spaces', spaceId);
1724
- return spaceInfo?.name || spaceId;
1580
+ async unfederate(spaceId1, spaceId2, password1, password2 = null) {
1581
+ return Federation.unfederate(this, spaceId1, spaceId2, password1, password2);
1725
1582
  }
1726
1583
 
1727
1584
  /**
@@ -1729,135 +1586,109 @@ class HoloSphere {
1729
1586
  * @param {string} holon - The holon identifier
1730
1587
  * @param {string} lens - The lens identifier
1731
1588
  * @param {object} options - Options for data retrieval and aggregation
1732
- * @param {boolean} options.aggregate - Whether to aggregate items with matching IDs (default: false)
1733
- * @param {string} options.idField - Field to use as identifier for aggregation (default: 'id')
1734
- * @param {string[]} options.sumFields - Numeric fields to sum during aggregation (e.g., ['received', 'sent'])
1735
- * @param {string[]} options.concatArrays - Array fields to concatenate during aggregation (e.g., ['wants', 'offers'])
1736
- * @param {boolean} options.removeDuplicates - Whether to remove duplicates when not aggregating (default: true)
1737
- * @param {function} options.mergeStrategy - Custom function to merge items during aggregation
1589
+ * @param {string} [password] - Optional password for accessing private data
1738
1590
  * @returns {Promise<Array>} - Combined array of local and federated data
1739
1591
  */
1740
- async getFederated(holon, lens, options = {}) {
1741
- // Validate required parameters
1742
- if (!holon || !lens) {
1743
- throw new Error('getFederated: Missing required parameters');
1744
- }
1745
-
1746
- const {
1747
- aggregate = false,
1748
- idField = 'id',
1749
- sumFields = [],
1750
- concatArrays = [],
1751
- removeDuplicates = true,
1752
- mergeStrategy = null
1753
- } = options;
1754
-
1755
- // Get federation info for current space
1756
- const fedInfo = await this.getFederation(this.currentSpace?.alias);
1757
-
1758
- // Get local data
1759
- const localData = await this.getAll(holon, lens);
1760
-
1761
- // If no federation or not authenticated, return local data only
1762
- if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
1763
- return localData;
1764
- }
1765
-
1766
- // Get data from each federated space
1767
- const federatedData = await Promise.all(
1768
- fedInfo.federation.map(async (federatedSpace) => {
1769
- try {
1770
- const data = await this.getAll(federatedSpace, lens);
1771
- return data || [];
1772
- } catch (error) {
1773
- console.warn(`Error getting data from federated space ${federatedSpace}:`, error);
1774
- return [];
1775
- }
1776
- })
1777
- );
1778
-
1779
- // Combine all data
1780
- const allData = [...localData, ...federatedData.flat()];
1781
-
1782
- // If aggregating, use enhanced aggregation logic
1783
- if (aggregate) {
1784
- const aggregated = new Map();
1785
-
1786
- for (const item of allData) {
1787
- const itemId = item[idField];
1788
- if (!itemId) continue;
1789
-
1790
- const existing = aggregated.get(itemId);
1791
- if (!existing) {
1792
- aggregated.set(itemId, { ...item });
1793
- } else {
1794
- // If custom merge strategy is provided, use it
1795
- if (mergeStrategy && typeof mergeStrategy === 'function') {
1796
- aggregated.set(itemId, mergeStrategy(existing, item));
1797
- continue;
1798
- }
1799
-
1800
- // Enhanced default merge strategy
1801
- const merged = { ...existing };
1592
+ async getFederated(holon, lens, options = {}, password = null) {
1593
+ return Federation.getFederated(this, holon, lens, options, password);
1594
+ }
1802
1595
 
1803
- // Sum numeric fields
1804
- for (const field of sumFields) {
1805
- if (typeof item[field] === 'number') {
1806
- 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;
1807
1617
  }
1618
+ } catch (error) {
1619
+ console.warn(`Error cleaning up subscription ${id}:`, error);
1808
1620
  }
1621
+ }
1622
+
1623
+ // Clear subscriptions
1624
+ this.subscriptions = {};
1809
1625
 
1810
- // Concatenate and deduplicate array fields
1811
- for (const field of concatArrays) {
1812
- if (Array.isArray(item[field])) {
1813
- const combinedArray = [
1814
- ...(merged[field] || []),
1815
- ...(item[field] || [])
1816
- ];
1817
- // Remove duplicates if elements are primitive
1818
- 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
+ }
1819
1649
  }
1650
+ } catch (error) {
1651
+ console.warn('Error accessing Gun mesh:', error);
1820
1652
  }
1653
+ }
1821
1654
 
1822
- // Update federation metadata
1823
- merged.federation = {
1824
- ...merged.federation,
1825
- timestamp: Math.max(
1826
- merged.federation?.timestamp || 0,
1827
- item.federation?.timestamp || 0
1828
- ),
1829
- origins: Array.from(new Set([
1830
- ...(merged.federation?.origins || [merged.federation?.origin]),
1831
- ...(item.federation?.origins || [item.federation?.origin])
1832
- ]).filter(Boolean))
1833
- };
1834
-
1835
- // Update the aggregated item
1836
- 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);
1837
1660
  }
1661
+
1662
+ // Wait a moment for cleanup to complete
1663
+ await new Promise(resolve => setTimeout(resolve, 100));
1838
1664
  }
1839
-
1840
- return Array.from(aggregated.values());
1841
- }
1842
-
1843
- // If not aggregating, optionally remove duplicates based on idField
1844
- if (!removeDuplicates) {
1845
- return allData;
1665
+
1666
+ console.log('HoloSphere instance closed successfully');
1667
+ } catch (error) {
1668
+ console.error('Error closing HoloSphere instance:', error);
1846
1669
  }
1670
+ }
1847
1671
 
1848
- // Remove duplicates keeping the most recent version
1849
- const uniqueMap = new Map();
1850
- allData.forEach(item => {
1851
- const id = item[idField];
1852
- 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
+ }
1853
1682
 
1854
- const existing = uniqueMap.get(id);
1855
- if (!existing ||
1856
- (item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
1857
- uniqueMap.set(id, item);
1858
- }
1859
- });
1860
- 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}`;
1861
1692
  }
1862
1693
  }
1863
1694