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