holosphere 1.1.2 → 1.1.3
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 +1054 -208
- package/package.json +2 -2
- package/test/federation.test.js +418 -0
- package/test/holosphere.test.js +686 -133
- package/test/spacesauth.test.js +337 -0
package/holosphere.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as h3 from 'h3-js';
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
3
|
import Gun from 'gun'
|
|
4
|
+
import 'gun/sea' // Import SEA module
|
|
4
5
|
import Ajv2019 from 'ajv/dist/2019.js'
|
|
5
6
|
|
|
6
7
|
|
|
@@ -10,8 +11,9 @@ class HoloSphere {
|
|
|
10
11
|
* @param {string} appname - The name of the application.
|
|
11
12
|
* @param {boolean} strict - Whether to enforce strict schema validation.
|
|
12
13
|
* @param {string|null} openaikey - The OpenAI API key.
|
|
14
|
+
* @param {Gun|null} gunInstance - The Gun instance to use.
|
|
13
15
|
*/
|
|
14
|
-
constructor(appname, strict = false, openaikey = null) {
|
|
16
|
+
constructor(appname, strict = false, openaikey = null, gunInstance = null) {
|
|
15
17
|
this.appname = appname
|
|
16
18
|
this.strict = strict;
|
|
17
19
|
this.validator = new Ajv2019({
|
|
@@ -19,7 +21,9 @@ class HoloSphere {
|
|
|
19
21
|
strict: false, // Keep this false to avoid Ajv strict mode issues
|
|
20
22
|
validateSchema: true // Always validate schemas
|
|
21
23
|
});
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
// Use provided Gun instance or create new one
|
|
26
|
+
this.gun = gunInstance || Gun({
|
|
23
27
|
peers: ['https://gun.holons.io/gun', 'https://59.src.eco/gun'],
|
|
24
28
|
axe: false,
|
|
25
29
|
// uuid: (content) => { // generate a unique id for each node
|
|
@@ -27,13 +31,17 @@ class HoloSphere {
|
|
|
27
31
|
// return content;}
|
|
28
32
|
});
|
|
29
33
|
|
|
34
|
+
// Initialize SEA
|
|
35
|
+
this.sea = Gun.SEA;
|
|
36
|
+
|
|
30
37
|
if (openaikey != null) {
|
|
31
38
|
this.openai = new OpenAI({
|
|
32
39
|
apiKey: openaikey,
|
|
33
40
|
});
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
// Add currentSpace property to track logged in space
|
|
44
|
+
this.currentSpace = null;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
// ================================ SCHEMA FUNCTIONS ================================
|
|
@@ -94,14 +102,22 @@ class HoloSphere {
|
|
|
94
102
|
return new Promise((resolve, reject) => {
|
|
95
103
|
try {
|
|
96
104
|
const schemaString = JSON.stringify(schema);
|
|
105
|
+
const schemaData = {
|
|
106
|
+
schema: schemaString,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
// Only set owner if there's an authenticated space
|
|
109
|
+
...(this.currentSpace && { owner: this.currentSpace.alias })
|
|
110
|
+
};
|
|
111
|
+
|
|
97
112
|
this.gun.get(this.appname)
|
|
98
113
|
.get(lens)
|
|
99
114
|
.get('schema')
|
|
100
|
-
.put(
|
|
115
|
+
.put(schemaData, ack => {
|
|
101
116
|
if (ack.err) {
|
|
102
117
|
reject(new Error(ack.err));
|
|
103
118
|
} else {
|
|
104
|
-
|
|
119
|
+
// Add small delay to ensure data is written
|
|
120
|
+
setTimeout(() => resolve(true), 50);
|
|
105
121
|
}
|
|
106
122
|
});
|
|
107
123
|
} catch (error) {
|
|
@@ -121,27 +137,31 @@ class HoloSphere {
|
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
return new Promise((resolve) => {
|
|
140
|
+
let timeout = setTimeout(() => {
|
|
141
|
+
console.warn('getSchema: Operation timed out');
|
|
142
|
+
resolve(null);
|
|
143
|
+
}, 5000);
|
|
144
|
+
|
|
124
145
|
this.gun.get(this.appname)
|
|
125
146
|
.get(lens)
|
|
126
147
|
.get('schema')
|
|
127
148
|
.once(data => {
|
|
149
|
+
clearTimeout(timeout);
|
|
128
150
|
if (!data) {
|
|
129
151
|
resolve(null);
|
|
130
152
|
return;
|
|
131
153
|
}
|
|
132
154
|
|
|
133
155
|
try {
|
|
134
|
-
// Handle both
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
typeof v === 'string' && v.includes('"type":'));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (schemaStr) {
|
|
142
|
-
resolve(JSON.parse(schemaStr));
|
|
156
|
+
// Handle both new format and legacy format
|
|
157
|
+
if (data.schema) {
|
|
158
|
+
// New format with timestamp
|
|
159
|
+
resolve(JSON.parse(data.schema));
|
|
143
160
|
} else {
|
|
144
|
-
|
|
161
|
+
// Legacy format or direct string
|
|
162
|
+
const schemaStr = typeof data === 'string' ? data :
|
|
163
|
+
Object.values(data).find(v => typeof v === 'string' && v.includes('"type":'));
|
|
164
|
+
resolve(schemaStr ? JSON.parse(schemaStr) : null);
|
|
145
165
|
}
|
|
146
166
|
} catch (error) {
|
|
147
167
|
console.error('getSchema: Error parsing schema:', error);
|
|
@@ -161,35 +181,64 @@ class HoloSphere {
|
|
|
161
181
|
* @returns {Promise<boolean>} - Returns true if successful, false if there was an error
|
|
162
182
|
*/
|
|
163
183
|
async put(holon, lens, data) {
|
|
164
|
-
|
|
184
|
+
// Check authentication for data operations
|
|
185
|
+
if (!this.currentSpace) {
|
|
186
|
+
throw new Error('Unauthorized to modify this data');
|
|
187
|
+
}
|
|
188
|
+
this._checkSession();
|
|
189
|
+
|
|
190
|
+
// If updating existing data, check ownership
|
|
191
|
+
if (data.id) {
|
|
192
|
+
const existing = await this.get(holon, lens, data.id);
|
|
193
|
+
if (existing && existing.owner &&
|
|
194
|
+
existing.owner !== this.currentSpace.alias &&
|
|
195
|
+
!existing.federation) { // Skip ownership check for federated data
|
|
196
|
+
throw new Error('Unauthorized to modify this data');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Add owner and federation information to data
|
|
201
|
+
const dataWithMeta = {
|
|
202
|
+
...data,
|
|
203
|
+
owner: this.currentSpace.alias,
|
|
204
|
+
federation: {
|
|
205
|
+
origin: this.currentSpace.alias,
|
|
206
|
+
timestamp: Date.now()
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (!holon || !lens || !dataWithMeta) {
|
|
165
211
|
throw new Error('put: Missing required parameters');
|
|
166
212
|
}
|
|
167
213
|
|
|
168
|
-
if (!
|
|
169
|
-
|
|
214
|
+
if (!dataWithMeta.id) {
|
|
215
|
+
dataWithMeta.id = this.generateId();
|
|
170
216
|
}
|
|
171
217
|
|
|
218
|
+
// Get and validate schema first
|
|
172
219
|
const schema = await this.getSchema(lens);
|
|
173
220
|
if (schema) {
|
|
174
|
-
//
|
|
175
|
-
const dataToValidate = JSON.parse(JSON.stringify(
|
|
176
|
-
|
|
177
|
-
// Validate against schema
|
|
221
|
+
// Deep clone data to avoid modifying the original
|
|
222
|
+
const dataToValidate = JSON.parse(JSON.stringify(dataWithMeta));
|
|
178
223
|
const valid = this.validator.validate(schema, dataToValidate);
|
|
224
|
+
|
|
179
225
|
if (!valid) {
|
|
180
|
-
|
|
226
|
+
const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
|
|
227
|
+
// Always throw on schema validation failure, regardless of strict mode
|
|
228
|
+
throw new Error(errorMsg);
|
|
181
229
|
}
|
|
182
230
|
} else if (this.strict) {
|
|
183
231
|
throw new Error('Schema required in strict mode');
|
|
184
232
|
}
|
|
185
233
|
|
|
186
|
-
|
|
234
|
+
// Store data in current space
|
|
235
|
+
const putResult = await new Promise((resolve, reject) => {
|
|
187
236
|
try {
|
|
188
|
-
const payload = JSON.stringify(
|
|
237
|
+
const payload = JSON.stringify(dataWithMeta);
|
|
189
238
|
this.gun.get(this.appname)
|
|
190
239
|
.get(holon)
|
|
191
240
|
.get(lens)
|
|
192
|
-
.get(
|
|
241
|
+
.get(dataWithMeta.id)
|
|
193
242
|
.put(payload, ack => {
|
|
194
243
|
if (ack.err) {
|
|
195
244
|
reject(new Error(ack.err));
|
|
@@ -201,6 +250,71 @@ class HoloSphere {
|
|
|
201
250
|
reject(error);
|
|
202
251
|
}
|
|
203
252
|
});
|
|
253
|
+
|
|
254
|
+
// If successful, propagate to federated spaces
|
|
255
|
+
if (putResult) {
|
|
256
|
+
await this._propagateToFederation(holon, lens, dataWithMeta);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return putResult;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Propagates data to federated spaces
|
|
264
|
+
* @private
|
|
265
|
+
* @param {string} holon - The holon identifier
|
|
266
|
+
* @param {string} lens - The lens identifier
|
|
267
|
+
* @param {object} data - The data to propagate
|
|
268
|
+
*/
|
|
269
|
+
async _propagateToFederation(holon, lens, data) {
|
|
270
|
+
try {
|
|
271
|
+
// Get federation info for current space
|
|
272
|
+
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
273
|
+
if (!fedInfo || !fedInfo.notify || fedInfo.notify.length === 0) {
|
|
274
|
+
return; // No federation to propagate to
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Propagate to each federated space
|
|
278
|
+
const propagationPromises = fedInfo.notify.map(spaceId =>
|
|
279
|
+
new Promise((resolve) => {
|
|
280
|
+
// Store data in the federated space's lens
|
|
281
|
+
this.gun.get(this.appname)
|
|
282
|
+
.get(spaceId)
|
|
283
|
+
.get(lens)
|
|
284
|
+
.get(data.id)
|
|
285
|
+
.put(JSON.stringify({
|
|
286
|
+
...data,
|
|
287
|
+
federation: {
|
|
288
|
+
...data.federation,
|
|
289
|
+
notified: Date.now()
|
|
290
|
+
}
|
|
291
|
+
}), ack => {
|
|
292
|
+
if (ack.err) {
|
|
293
|
+
console.warn(`Failed to propagate to space ${spaceId}:`, ack.err);
|
|
294
|
+
}
|
|
295
|
+
resolve();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Also store in federation lens for notifications
|
|
299
|
+
this.gun.get(this.appname)
|
|
300
|
+
.get(spaceId)
|
|
301
|
+
.get('federation')
|
|
302
|
+
.get(data.id)
|
|
303
|
+
.put(JSON.stringify({
|
|
304
|
+
...data,
|
|
305
|
+
federation: {
|
|
306
|
+
...data.federation,
|
|
307
|
+
notified: Date.now()
|
|
308
|
+
}
|
|
309
|
+
}));
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
await Promise.all(propagationPromises);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.warn('Federation propagation error:', error);
|
|
316
|
+
// Don't throw here to avoid failing the original put
|
|
317
|
+
}
|
|
204
318
|
}
|
|
205
319
|
|
|
206
320
|
/**
|
|
@@ -219,53 +333,225 @@ class HoloSphere {
|
|
|
219
333
|
throw new Error('getAll: Schema required in strict mode');
|
|
220
334
|
}
|
|
221
335
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
336
|
+
// Get local data
|
|
337
|
+
const localData = await this._getAllLocal(holon, lens, schema);
|
|
338
|
+
|
|
339
|
+
// If authenticated, get federated data
|
|
340
|
+
let federatedData = [];
|
|
341
|
+
if (this.currentSpace) {
|
|
342
|
+
federatedData = await this._getAllFederated(holon, lens, schema);
|
|
343
|
+
}
|
|
226
344
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
345
|
+
// Combine and deduplicate data based on ID
|
|
346
|
+
const combined = new Map();
|
|
347
|
+
|
|
348
|
+
// Add local data first
|
|
349
|
+
localData.forEach(item => {
|
|
350
|
+
if (item.id) {
|
|
351
|
+
combined.set(item.id, item);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Add federated data, potentially overwriting local data if newer
|
|
356
|
+
federatedData.forEach(item => {
|
|
357
|
+
if (item.id) {
|
|
358
|
+
const existing = combined.get(item.id);
|
|
359
|
+
if (!existing ||
|
|
360
|
+
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
361
|
+
combined.set(item.id, item);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
return Array.from(combined.values());
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Gets data from federated spaces
|
|
371
|
+
* @private
|
|
372
|
+
* @param {string} holon - The holon identifier
|
|
373
|
+
* @param {string} lens - The lens identifier
|
|
374
|
+
* @param {string} key - The key to get
|
|
375
|
+
* @returns {Promise<object|null>} - The federated data or null if not found
|
|
376
|
+
*/
|
|
377
|
+
async _getFederatedData(holon, lens, key) {
|
|
378
|
+
try {
|
|
379
|
+
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
380
|
+
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Try each federated space
|
|
385
|
+
for (const spaceId of fedInfo.federation) {
|
|
386
|
+
const result = await new Promise((resolve) => {
|
|
387
|
+
this.gun.get(this.appname)
|
|
388
|
+
.get(spaceId)
|
|
389
|
+
.get(lens)
|
|
390
|
+
.get(key)
|
|
391
|
+
.once(async (data) => {
|
|
392
|
+
if (!data) {
|
|
393
|
+
resolve(null);
|
|
394
|
+
return;
|
|
237
395
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
396
|
+
try {
|
|
397
|
+
const parsed = await this.parse(data);
|
|
398
|
+
resolve(parsed);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.warn(`Error parsing federated data from ${spaceId}:`, error);
|
|
401
|
+
resolve(null);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
if (result) {
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.warn('Federation get error:', error);
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Gets all data from local space
|
|
418
|
+
* @private
|
|
419
|
+
* @param {string} holon - The holon identifier
|
|
420
|
+
* @param {string} lens - The lens identifier
|
|
421
|
+
* @param {object} schema - The schema to validate against
|
|
422
|
+
* @returns {Promise<Array>} - Array of local data
|
|
423
|
+
*/
|
|
424
|
+
async _getAllLocal(holon, lens, schema) {
|
|
425
|
+
return new Promise((resolve) => {
|
|
426
|
+
const output = new Map();
|
|
427
|
+
let isResolved = false;
|
|
428
|
+
let listener = null;
|
|
429
|
+
|
|
430
|
+
const hardTimeout = setTimeout(() => {
|
|
431
|
+
cleanup();
|
|
432
|
+
resolve(Array.from(output.values()));
|
|
433
|
+
}, 5000);
|
|
434
|
+
|
|
435
|
+
const cleanup = () => {
|
|
436
|
+
if (listener) {
|
|
437
|
+
listener.off();
|
|
438
|
+
}
|
|
439
|
+
clearTimeout(hardTimeout);
|
|
440
|
+
isResolved = true;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const processData = async (data, key) => {
|
|
444
|
+
if (!data || key === '_') return;
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const parsed = await this.parse(data);
|
|
448
|
+
if (!parsed || !parsed.id) return;
|
|
449
|
+
|
|
450
|
+
if (schema) {
|
|
451
|
+
const valid = this.validator.validate(schema, parsed);
|
|
452
|
+
if (valid || !this.strict) {
|
|
453
|
+
output.set(parsed.id, parsed);
|
|
245
454
|
}
|
|
455
|
+
} else {
|
|
456
|
+
output.set(parsed.id, parsed);
|
|
246
457
|
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error('Error processing data:', error);
|
|
247
460
|
}
|
|
248
461
|
};
|
|
249
462
|
|
|
250
|
-
|
|
463
|
+
this.gun.get(this.appname)
|
|
251
464
|
.get(holon)
|
|
252
465
|
.get(lens)
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
466
|
+
.once(async (data) => {
|
|
467
|
+
if (!data) {
|
|
468
|
+
cleanup();
|
|
469
|
+
resolve([]);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
258
472
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
473
|
+
const initialPromises = [];
|
|
474
|
+
Object.keys(data)
|
|
475
|
+
.filter(key => key !== '_')
|
|
476
|
+
.forEach(key => {
|
|
477
|
+
initialPromises.push(processData(data[key], key));
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
await Promise.all(initialPromises);
|
|
482
|
+
cleanup();
|
|
483
|
+
resolve(Array.from(output.values()));
|
|
484
|
+
} catch (error) {
|
|
485
|
+
cleanup();
|
|
486
|
+
resolve([]);
|
|
487
|
+
}
|
|
265
488
|
});
|
|
266
489
|
});
|
|
267
490
|
}
|
|
268
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Gets all data from federated spaces
|
|
494
|
+
* @private
|
|
495
|
+
* @param {string} holon - The holon identifier
|
|
496
|
+
* @param {string} lens - The lens identifier
|
|
497
|
+
* @param {object} schema - The schema to validate against
|
|
498
|
+
* @returns {Promise<Array>} - Array of federated data
|
|
499
|
+
*/
|
|
500
|
+
async _getAllFederated(holon, lens, schema) {
|
|
501
|
+
try {
|
|
502
|
+
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
503
|
+
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const federatedData = new Map();
|
|
508
|
+
|
|
509
|
+
// Get data from each federated space
|
|
510
|
+
const fedPromises = fedInfo.federation.map(spaceId =>
|
|
511
|
+
new Promise((resolve) => {
|
|
512
|
+
this.gun.get(this.appname)
|
|
513
|
+
.get(spaceId)
|
|
514
|
+
.get(lens)
|
|
515
|
+
.once(async (data) => {
|
|
516
|
+
if (!data) {
|
|
517
|
+
resolve();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const processPromises = Object.keys(data)
|
|
522
|
+
.filter(key => key !== '_')
|
|
523
|
+
.map(async key => {
|
|
524
|
+
try {
|
|
525
|
+
const parsed = await this.parse(data[key]);
|
|
526
|
+
if (parsed && parsed.id) {
|
|
527
|
+
if (schema) {
|
|
528
|
+
const valid = this.validator.validate(schema, parsed);
|
|
529
|
+
if (valid || !this.strict) {
|
|
530
|
+
federatedData.set(parsed.id, parsed);
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
federatedData.set(parsed.id, parsed);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.warn(`Error processing federated data from ${spaceId}:`, error);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
await Promise.all(processPromises);
|
|
542
|
+
resolve();
|
|
543
|
+
});
|
|
544
|
+
})
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
await Promise.all(fedPromises);
|
|
548
|
+
return Array.from(federatedData.values());
|
|
549
|
+
} catch (error) {
|
|
550
|
+
console.warn('Federation getAll error:', error);
|
|
551
|
+
return [];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
269
555
|
/**
|
|
270
556
|
* Parses data from GunDB, handling various data formats and references.
|
|
271
557
|
* @param {*} data - The data to parse, could be a string, object, or GunDB reference.
|
|
@@ -311,7 +597,9 @@ class HoloSphere {
|
|
|
311
597
|
|
|
312
598
|
return parsedData;
|
|
313
599
|
} catch (error) {
|
|
314
|
-
|
|
600
|
+
console.log("Parsing not a JSON, returning raw data", rawData);
|
|
601
|
+
return rawData;
|
|
602
|
+
//throw new Error(`Parse error: ${error.message}`);
|
|
315
603
|
}
|
|
316
604
|
}
|
|
317
605
|
|
|
@@ -331,17 +619,18 @@ class HoloSphere {
|
|
|
331
619
|
// Get schema for validation
|
|
332
620
|
const schema = await this.getSchema(lens);
|
|
333
621
|
|
|
334
|
-
|
|
622
|
+
// First try to get from current space
|
|
623
|
+
const localResult = await new Promise((resolve) => {
|
|
335
624
|
let timeout = setTimeout(() => {
|
|
336
625
|
console.warn('get: Operation timed out');
|
|
337
626
|
resolve(null);
|
|
338
|
-
}, 5000);
|
|
627
|
+
}, 5000);
|
|
339
628
|
|
|
340
629
|
this.gun.get(this.appname)
|
|
341
630
|
.get(holon)
|
|
342
631
|
.get(lens)
|
|
343
632
|
.get(key)
|
|
344
|
-
.once((data
|
|
633
|
+
.once(async (data) => {
|
|
345
634
|
clearTimeout(timeout);
|
|
346
635
|
|
|
347
636
|
if (!data) {
|
|
@@ -350,7 +639,7 @@ class HoloSphere {
|
|
|
350
639
|
}
|
|
351
640
|
|
|
352
641
|
try {
|
|
353
|
-
const parsed = this.parse(data);
|
|
642
|
+
const parsed = await this.parse(data);
|
|
354
643
|
|
|
355
644
|
// Validate against schema if one exists
|
|
356
645
|
if (schema) {
|
|
@@ -363,6 +652,20 @@ class HoloSphere {
|
|
|
363
652
|
}
|
|
364
653
|
}
|
|
365
654
|
}
|
|
655
|
+
|
|
656
|
+
// Check if user has access - only allow if:
|
|
657
|
+
// 1. No owner (public data)
|
|
658
|
+
// 2. User is the owner
|
|
659
|
+
// 3. User is in shared list
|
|
660
|
+
// 4. Data is from federation
|
|
661
|
+
if (parsed.owner &&
|
|
662
|
+
this.currentSpace?.alias !== parsed.owner &&
|
|
663
|
+
(!parsed.shared || !parsed.shared.includes(this.currentSpace?.alias)) &&
|
|
664
|
+
(!parsed.federation || !parsed.federation.origin)) {
|
|
665
|
+
resolve(null);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
366
669
|
resolve(parsed);
|
|
367
670
|
} catch (error) {
|
|
368
671
|
console.error('Error parsing data:', error);
|
|
@@ -370,6 +673,21 @@ class HoloSphere {
|
|
|
370
673
|
}
|
|
371
674
|
});
|
|
372
675
|
});
|
|
676
|
+
|
|
677
|
+
// If found locally, return it
|
|
678
|
+
if (localResult) {
|
|
679
|
+
return localResult;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// If not found locally and we're authenticated, try federated spaces
|
|
683
|
+
if (this.currentSpace) {
|
|
684
|
+
const fedResult = await this._getFederatedData(holon, lens, key);
|
|
685
|
+
if (fedResult) {
|
|
686
|
+
return fedResult;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return null;
|
|
373
691
|
}
|
|
374
692
|
|
|
375
693
|
/**
|
|
@@ -383,6 +701,21 @@ class HoloSphere {
|
|
|
383
701
|
throw new Error('delete: Missing required parameters');
|
|
384
702
|
}
|
|
385
703
|
|
|
704
|
+
if (!this.currentSpace) {
|
|
705
|
+
throw new Error('Unauthorized to delete this data');
|
|
706
|
+
}
|
|
707
|
+
this._checkSession();
|
|
708
|
+
|
|
709
|
+
// Check ownership before delete
|
|
710
|
+
const data = await this.get(holon, lens, key);
|
|
711
|
+
if (!data) {
|
|
712
|
+
return true; // Nothing to delete
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (data.owner && data.owner !== this.currentSpace.alias) {
|
|
716
|
+
throw new Error('Unauthorized to delete this data');
|
|
717
|
+
}
|
|
718
|
+
|
|
386
719
|
return new Promise((resolve, reject) => {
|
|
387
720
|
try {
|
|
388
721
|
this.gun.get(this.appname)
|
|
@@ -459,10 +792,10 @@ class HoloSphere {
|
|
|
459
792
|
* Stores a specific gun node in a given holon and lens.
|
|
460
793
|
* @param {string} holon - The holon identifier.
|
|
461
794
|
* @param {string} lens - The lens under which to store the node.
|
|
462
|
-
* @param {object}
|
|
795
|
+
* @param {object} data - The node to store.
|
|
463
796
|
*/
|
|
464
|
-
async putNode(holon, lens,
|
|
465
|
-
if (!holon || !lens || !
|
|
797
|
+
async putNode(holon, lens, data) {
|
|
798
|
+
if (!holon || !lens || !data) {
|
|
466
799
|
throw new Error('putNode: Missing required parameters');
|
|
467
800
|
}
|
|
468
801
|
|
|
@@ -471,7 +804,8 @@ class HoloSphere {
|
|
|
471
804
|
this.gun.get(this.appname)
|
|
472
805
|
.get(holon)
|
|
473
806
|
.get(lens)
|
|
474
|
-
.
|
|
807
|
+
.get('value') // Store at 'value' key
|
|
808
|
+
.put(data.value, ack => { // Store the value directly
|
|
475
809
|
if (ack.err) {
|
|
476
810
|
reject(new Error(ack.err));
|
|
477
811
|
} else {
|
|
@@ -489,19 +823,26 @@ class HoloSphere {
|
|
|
489
823
|
* @param {string} holon - The holon identifier.
|
|
490
824
|
* @param {string} lens - The lens identifier.
|
|
491
825
|
* @param {string} key - The specific key to retrieve.
|
|
492
|
-
|
|
826
|
+
* @returns {Promise<any>} - The retrieved node or null if not found.
|
|
493
827
|
*/
|
|
494
|
-
getNode(holon, lens, key) {
|
|
828
|
+
async getNode(holon, lens, key) {
|
|
495
829
|
if (!holon || !lens || !key) {
|
|
496
|
-
|
|
497
|
-
return null;
|
|
830
|
+
throw new Error('getNode: Missing required parameters');
|
|
498
831
|
}
|
|
499
832
|
|
|
500
|
-
return
|
|
833
|
+
return new Promise((resolve) => {
|
|
834
|
+
this.gun.get(this.appname)
|
|
501
835
|
.get(holon)
|
|
502
836
|
.get(lens)
|
|
503
837
|
.get(key)
|
|
504
|
-
|
|
838
|
+
.once((data) => {
|
|
839
|
+
if (!data) {
|
|
840
|
+
resolve(null);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
resolve(data); // Return the data directly
|
|
844
|
+
});
|
|
845
|
+
});
|
|
505
846
|
}
|
|
506
847
|
|
|
507
848
|
getNodeRef(soul) {
|
|
@@ -620,83 +961,54 @@ class HoloSphere {
|
|
|
620
961
|
* @param {string} tableName - The table name to retrieve data from.
|
|
621
962
|
* @returns {Promise<object|null>} - The parsed data from the table or null if not found.
|
|
622
963
|
*/
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
// let counter = 0
|
|
627
|
-
// this.gun.get(this.appname).get(tableName.toString()).once((data, key) => {
|
|
628
|
-
|
|
629
|
-
// counter += 1
|
|
630
|
-
// if (itemdata) {
|
|
631
|
-
// let parsed = await this.parse(itemdata)
|
|
632
|
-
// output.push(parsed);
|
|
633
|
-
// console.log('getAllGlobal: parsed: ', parsed)
|
|
634
|
-
// }
|
|
635
|
-
|
|
636
|
-
// if (counter == maplenght) {
|
|
637
|
-
// resolve(output);
|
|
638
|
-
|
|
639
|
-
// }
|
|
640
|
-
// }
|
|
641
|
-
// );
|
|
642
|
-
// }
|
|
643
|
-
// )
|
|
644
|
-
// }
|
|
645
|
-
async getAllGlobal(lens) {
|
|
646
|
-
if ( !lens) {
|
|
647
|
-
console.error('getAll: Missing required parameters:', { lens });
|
|
648
|
-
return [];
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const schema = await this.getSchema(lens);
|
|
652
|
-
if (!schema && this.strict) {
|
|
653
|
-
console.error('getAll: Schema required in strict mode for lens:', lens);
|
|
654
|
-
return [];
|
|
964
|
+
async getAllGlobal(tableName) {
|
|
965
|
+
if (!tableName) {
|
|
966
|
+
throw new Error('getAllGlobal: Missing table name parameter');
|
|
655
967
|
}
|
|
656
968
|
|
|
657
969
|
return new Promise((resolve) => {
|
|
658
970
|
let output = [];
|
|
659
|
-
let
|
|
971
|
+
let isResolved = false;
|
|
972
|
+
let timeout = setTimeout(() => {
|
|
973
|
+
if (!isResolved) {
|
|
974
|
+
isResolved = true;
|
|
975
|
+
resolve(output);
|
|
976
|
+
}
|
|
977
|
+
}, 5000);
|
|
660
978
|
|
|
661
|
-
this.gun.get(this.appname).get(
|
|
979
|
+
this.gun.get(this.appname).get(tableName).once(async (data) => {
|
|
662
980
|
if (!data) {
|
|
663
|
-
|
|
981
|
+
clearTimeout(timeout);
|
|
982
|
+
isResolved = true;
|
|
983
|
+
resolve([]);
|
|
664
984
|
return;
|
|
665
985
|
}
|
|
666
986
|
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
await this.delete(holon, lens, key);
|
|
681
|
-
} else {
|
|
682
|
-
console.warn('Invalid data found:', key, this.validator.errors);
|
|
683
|
-
output.push(parsed);
|
|
684
|
-
}
|
|
685
|
-
} else {
|
|
686
|
-
output.push(parsed);
|
|
687
|
-
}
|
|
688
|
-
} catch (error) {
|
|
689
|
-
console.error('Error parsing data:', error);
|
|
690
|
-
if (this.strict) {
|
|
691
|
-
await this.delete(holon, lens, key);
|
|
987
|
+
const keys = Object.keys(data).filter(key => key !== '_');
|
|
988
|
+
const promises = keys.map(key =>
|
|
989
|
+
new Promise(async (resolveItem) => {
|
|
990
|
+
const itemData = await new Promise(resolveData => {
|
|
991
|
+
this.gun.get(this.appname).get(tableName).get(key).once(resolveData);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
if (itemData) {
|
|
995
|
+
try {
|
|
996
|
+
const parsed = await this.parse(itemData);
|
|
997
|
+
if (parsed) output.push(parsed);
|
|
998
|
+
} catch (error) {
|
|
999
|
+
console.error('Error parsing data:', error);
|
|
692
1000
|
}
|
|
693
1001
|
}
|
|
694
|
-
|
|
1002
|
+
resolveItem();
|
|
1003
|
+
})
|
|
1004
|
+
);
|
|
695
1005
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1006
|
+
await Promise.all(promises);
|
|
1007
|
+
clearTimeout(timeout);
|
|
1008
|
+
if (!isResolved) {
|
|
1009
|
+
isResolved = true;
|
|
1010
|
+
resolve(output);
|
|
1011
|
+
}
|
|
700
1012
|
});
|
|
701
1013
|
});
|
|
702
1014
|
}
|
|
@@ -707,7 +1019,36 @@ class HoloSphere {
|
|
|
707
1019
|
* @returns {Promise<void>}
|
|
708
1020
|
*/
|
|
709
1021
|
async deleteGlobal(tableName, key) {
|
|
710
|
-
|
|
1022
|
+
if (!tableName || !key) {
|
|
1023
|
+
throw new Error('deleteGlobal: Missing required parameters');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Only check authentication for non-spaces tables
|
|
1027
|
+
if (tableName !== 'spaces' && !this.currentSpace) {
|
|
1028
|
+
throw new Error('Unauthorized to delete this data');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Skip session check for spaces table
|
|
1032
|
+
if (tableName !== 'spaces') {
|
|
1033
|
+
this._checkSession();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return new Promise((resolve, reject) => {
|
|
1037
|
+
try {
|
|
1038
|
+
this.gun.get(this.appname)
|
|
1039
|
+
.get(tableName)
|
|
1040
|
+
.get(key)
|
|
1041
|
+
.put(null, ack => {
|
|
1042
|
+
if (ack.err) {
|
|
1043
|
+
reject(new Error(ack.err));
|
|
1044
|
+
} else {
|
|
1045
|
+
resolve(true);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
reject(error);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
711
1052
|
}
|
|
712
1053
|
|
|
713
1054
|
/**
|
|
@@ -716,16 +1057,65 @@ class HoloSphere {
|
|
|
716
1057
|
* @returns {Promise<void>}
|
|
717
1058
|
*/
|
|
718
1059
|
async deleteAllGlobal(tableName) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
//
|
|
1060
|
+
if (!tableName) {
|
|
1061
|
+
throw new Error('deleteAllGlobal: Missing table name parameter');
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Only check authentication for non-spaces and non-federation tables
|
|
1065
|
+
if (!['spaces', 'federation'].includes(tableName) && !this.currentSpace) {
|
|
1066
|
+
throw new Error('Unauthorized to delete this data');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Skip session check for spaces and federation tables
|
|
1070
|
+
if (!['spaces', 'federation'].includes(tableName)) {
|
|
1071
|
+
this._checkSession();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return new Promise((resolve, reject) => {
|
|
1075
|
+
try {
|
|
1076
|
+
const deletions = new Set();
|
|
1077
|
+
let timeout = setTimeout(() => {
|
|
1078
|
+
if (deletions.size === 0) {
|
|
1079
|
+
resolve(true); // No data to delete
|
|
1080
|
+
}
|
|
1081
|
+
}, 5000);
|
|
1082
|
+
|
|
1083
|
+
this.gun.get(this.appname).get(tableName).once(async (data) => {
|
|
1084
|
+
if (!data) {
|
|
1085
|
+
clearTimeout(timeout);
|
|
1086
|
+
resolve(true);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const keys = Object.keys(data).filter(key => key !== '_');
|
|
1091
|
+
const promises = keys.map(key =>
|
|
1092
|
+
new Promise((resolveDelete) => {
|
|
1093
|
+
this.gun.get(this.appname)
|
|
1094
|
+
.get(tableName)
|
|
1095
|
+
.get(key)
|
|
1096
|
+
.put(null, ack => {
|
|
1097
|
+
if (ack.err) {
|
|
1098
|
+
console.error(`Failed to delete ${key}:`, ack.err);
|
|
1099
|
+
}
|
|
1100
|
+
resolveDelete();
|
|
1101
|
+
});
|
|
1102
|
+
})
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1105
|
+
try {
|
|
1106
|
+
await Promise.all(promises);
|
|
1107
|
+
// Finally delete the table itself
|
|
1108
|
+
this.gun.get(this.appname).get(tableName).put(null);
|
|
1109
|
+
clearTimeout(timeout);
|
|
1110
|
+
resolve(true);
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
reject(error);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
reject(error);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
729
1119
|
}
|
|
730
1120
|
|
|
731
1121
|
// ================================ COMPUTE FUNCTIONS ================================
|
|
@@ -734,17 +1124,44 @@ class HoloSphere {
|
|
|
734
1124
|
* @param {string} holon - The holon identifier.
|
|
735
1125
|
* @param {string} lens - The lens to compute.
|
|
736
1126
|
* @param {string} operation - The operation to perform.
|
|
1127
|
+
* @param {number} [depth=0] - Current recursion depth.
|
|
1128
|
+
* @param {number} [maxDepth=15] - Maximum recursion depth.
|
|
1129
|
+
* @throws {Error} If parameters are invalid or missing
|
|
737
1130
|
*/
|
|
738
1131
|
async compute(holon, lens, operation, depth = 0, maxDepth = 15) {
|
|
739
|
-
|
|
1132
|
+
// Validate required parameters
|
|
1133
|
+
if (!holon || !lens || !operation) {
|
|
740
1134
|
throw new Error('compute: Missing required parameters');
|
|
741
1135
|
}
|
|
742
1136
|
|
|
743
|
-
|
|
1137
|
+
// Validate holon format and resolution
|
|
1138
|
+
let res;
|
|
1139
|
+
try {
|
|
1140
|
+
res = h3.getResolution(holon);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
throw new Error('compute: Invalid holon format');
|
|
1143
|
+
}
|
|
744
1144
|
|
|
745
|
-
const res = h3.getResolution(holon);
|
|
746
1145
|
if (res < 1 || res > 15) {
|
|
747
|
-
throw new Error('compute: Invalid holon resolution');
|
|
1146
|
+
throw new Error('compute: Invalid holon resolution (must be between 1 and 15)');
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Validate depth parameters
|
|
1150
|
+
if (typeof depth !== 'number' || depth < 0) {
|
|
1151
|
+
throw new Error('compute: Invalid depth parameter');
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (typeof maxDepth !== 'number' || maxDepth < 1 || maxDepth > 15) {
|
|
1155
|
+
throw new Error('compute: Invalid maxDepth parameter (must be between 1 and 15)');
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (depth >= maxDepth) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Validate operation
|
|
1163
|
+
if (typeof operation !== 'string' || !['summarize'].includes(operation)) {
|
|
1164
|
+
throw new Error('compute: Invalid operation (must be "summarize")');
|
|
748
1165
|
}
|
|
749
1166
|
|
|
750
1167
|
const parent = h3.cellToParent(holon, res - 1);
|
|
@@ -756,7 +1173,7 @@ class HoloSphere {
|
|
|
756
1173
|
const timeout = setTimeout(() => {
|
|
757
1174
|
console.warn(`Timeout for sibling ${sibling}`);
|
|
758
1175
|
resolve();
|
|
759
|
-
},
|
|
1176
|
+
}, 10000);
|
|
760
1177
|
|
|
761
1178
|
this.gun.get(this.appname)
|
|
762
1179
|
.get(sibling)
|
|
@@ -764,8 +1181,19 @@ class HoloSphere {
|
|
|
764
1181
|
.map()
|
|
765
1182
|
.once((data) => {
|
|
766
1183
|
clearTimeout(timeout);
|
|
767
|
-
if (data
|
|
768
|
-
|
|
1184
|
+
if (!data) {
|
|
1185
|
+
resolve();
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
// Parse the data if it's a string
|
|
1191
|
+
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
1192
|
+
if (parsed && parsed.content) {
|
|
1193
|
+
content.push(parsed.content);
|
|
1194
|
+
}
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
console.warn('Error parsing data:', error);
|
|
769
1197
|
}
|
|
770
1198
|
resolve();
|
|
771
1199
|
});
|
|
@@ -775,18 +1203,28 @@ class HoloSphere {
|
|
|
775
1203
|
await Promise.all(promises);
|
|
776
1204
|
|
|
777
1205
|
if (content.length > 0) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1206
|
+
try {
|
|
1207
|
+
const computed = await this.summarize(content.join('\n'));
|
|
1208
|
+
if (computed) {
|
|
1209
|
+
const summaryId = `${parent}_summary`;
|
|
1210
|
+
await this.put(parent, lens, {
|
|
1211
|
+
id: summaryId,
|
|
1212
|
+
content: computed,
|
|
1213
|
+
timestamp: Date.now()
|
|
1214
|
+
});
|
|
785
1215
|
|
|
786
|
-
|
|
787
|
-
|
|
1216
|
+
if (res > 1) { // Only recurse if not at top level
|
|
1217
|
+
await this.compute(parent, lens, operation, depth + 1, maxDepth);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
console.warn('Error in compute operation:', error);
|
|
1222
|
+
// Don't throw here to maintain graceful handling of compute errors
|
|
788
1223
|
}
|
|
789
1224
|
}
|
|
1225
|
+
|
|
1226
|
+
// Return successfully even if no content was found or processed
|
|
1227
|
+
return;
|
|
790
1228
|
}
|
|
791
1229
|
|
|
792
1230
|
/**
|
|
@@ -852,32 +1290,29 @@ class HoloSphere {
|
|
|
852
1290
|
if (!this.openai) {
|
|
853
1291
|
return 'OpenAI not initialized, please specify the API key in the constructor.'
|
|
854
1292
|
}
|
|
855
|
-
//const run = await this.openai.beta.threads.runs.retrieve(thread.id,run.id)
|
|
856
|
-
const assistant = await this.openai.beta.assistants.retrieve("asst_qhk79F8wV9BDNuwfOI80TqzC")
|
|
857
|
-
const thread = await this.openai.beta.threads.create()
|
|
858
|
-
const message = await this.openai.beta.threads.messages.create(thread.id, {
|
|
859
|
-
role: "user",
|
|
860
|
-
content: history
|
|
861
|
-
})
|
|
862
|
-
const run = await this.openai.beta.threads.runs.create(thread.id, {
|
|
863
|
-
assistant_id: assistant.id //,
|
|
864
|
-
//instructions: "What is the meaning of life?",
|
|
865
|
-
});
|
|
866
1293
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1294
|
+
try {
|
|
1295
|
+
const response = await this.openai.chat.completions.create({
|
|
1296
|
+
model: "gpt-4",
|
|
1297
|
+
messages: [
|
|
1298
|
+
{
|
|
1299
|
+
role: "system",
|
|
1300
|
+
content: "You are a helpful assistant that summarizes text concisely while preserving key information. Keep summaries clear and focused."
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
role: "user",
|
|
1304
|
+
content: history
|
|
1305
|
+
}
|
|
1306
|
+
],
|
|
1307
|
+
temperature: 0.7,
|
|
1308
|
+
max_tokens: 500
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
return response.choices[0].message.content.trim();
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
console.error('Error in summarize:', error);
|
|
1314
|
+
throw new Error('Failed to generate summary');
|
|
876
1315
|
}
|
|
877
|
-
// Get the latest messages from the thread
|
|
878
|
-
const messages = await this.openai.beta.threads.messages.list(thread.id)
|
|
879
|
-
const summary = messages.data[0].content[0].text.value.replace(/\`\`\`json\n/, '').replace(/\`\`\`/, '').trim()
|
|
880
|
-
return summary
|
|
881
1316
|
}
|
|
882
1317
|
|
|
883
1318
|
/**
|
|
@@ -903,7 +1338,7 @@ class HoloSphere {
|
|
|
903
1338
|
|
|
904
1339
|
|
|
905
1340
|
/**
|
|
906
|
-
* Updates the parent
|
|
1341
|
+
* Updates the parent holon with a new report.
|
|
907
1342
|
* @param {string} id - The child holon identifier.
|
|
908
1343
|
* @param {string} report - The report to update.
|
|
909
1344
|
* @returns {Promise<object>} - The updated parent information.
|
|
@@ -971,31 +1406,442 @@ class HoloSphere {
|
|
|
971
1406
|
* @param {function} callback - The callback to execute on changes.
|
|
972
1407
|
*/
|
|
973
1408
|
async subscribe(holon, lens, callback) {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
});
|
|
1409
|
+
if (!holon || !lens || !callback) {
|
|
1410
|
+
throw new Error('subscribe: Missing required parameters');
|
|
1411
|
+
}
|
|
978
1412
|
|
|
979
|
-
this.
|
|
1413
|
+
const ref = this.gun.get(this.appname)
|
|
1414
|
+
.get(holon)
|
|
1415
|
+
.get(lens);
|
|
980
1416
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1417
|
+
// Create a more robust handler
|
|
1418
|
+
const handler = async (data, key) => {
|
|
1419
|
+
if (!data || key === '_') return; // Skip empty data or Gun metadata
|
|
1420
|
+
|
|
1421
|
+
try {
|
|
1422
|
+
const parsed = typeof data === 'string' ? await this.parse(data) : data;
|
|
1423
|
+
if (parsed) {
|
|
1424
|
+
await callback(parsed);
|
|
1425
|
+
}
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
console.warn('Subscription handler error:', error);
|
|
1428
|
+
}
|
|
987
1429
|
};
|
|
988
|
-
}
|
|
989
1430
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1431
|
+
// Subscribe using Gun's map() and on()
|
|
1432
|
+
const chain = ref.map();
|
|
1433
|
+
chain.on(handler);
|
|
1434
|
+
|
|
1435
|
+
// Return subscription object
|
|
1436
|
+
return {
|
|
1437
|
+
off: () => {
|
|
1438
|
+
if (chain) {
|
|
1439
|
+
chain.off();
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
994
1443
|
}
|
|
995
1444
|
|
|
996
1445
|
// Add ID generation method
|
|
997
1446
|
generateId() {
|
|
998
|
-
return Date.now().toString(
|
|
1447
|
+
return Date.now().toString(10) + Math.random().toString(2);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Creates a new space with the given credentials
|
|
1452
|
+
* @param {string} spacename - The space identifier/username
|
|
1453
|
+
* @param {string} password - The space password
|
|
1454
|
+
* @returns {Promise<boolean>} - True if space was created successfully
|
|
1455
|
+
*/
|
|
1456
|
+
async createSpace(spacename, password) {
|
|
1457
|
+
if (!spacename || !password) {
|
|
1458
|
+
throw new Error('Invalid credentials format');
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Check if space already exists
|
|
1462
|
+
const existingSpace = await this.getGlobal('spaces', spacename);
|
|
1463
|
+
if (existingSpace) {
|
|
1464
|
+
throw new Error('Space already exists');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
try {
|
|
1468
|
+
// Generate key pair
|
|
1469
|
+
const pair = await Gun.SEA.pair();
|
|
1470
|
+
|
|
1471
|
+
// Create auth record with SEA
|
|
1472
|
+
const salt = await Gun.SEA.random(64).toString('base64');
|
|
1473
|
+
const hash = await Gun.SEA.work(password, salt);
|
|
1474
|
+
const auth = {
|
|
1475
|
+
salt: salt,
|
|
1476
|
+
hash: hash,
|
|
1477
|
+
pub: pair.pub
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
// Create space record with encrypted data
|
|
1481
|
+
const space = {
|
|
1482
|
+
alias: spacename,
|
|
1483
|
+
auth: auth,
|
|
1484
|
+
epub: pair.epub,
|
|
1485
|
+
pub: pair.pub,
|
|
1486
|
+
created: Date.now()
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
await this.putGlobal('spaces', {
|
|
1490
|
+
...space,
|
|
1491
|
+
id: spacename
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
return true;
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
throw new Error(`Space creation failed: ${error.message}`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Logs in to a space with the given credentials
|
|
1502
|
+
* @param {string} spacename - The space identifier/username
|
|
1503
|
+
* @param {string} password - The space password
|
|
1504
|
+
* @returns {Promise<boolean>} - True if login was successful
|
|
1505
|
+
*/
|
|
1506
|
+
async login(spacename, password) {
|
|
1507
|
+
// Validate input
|
|
1508
|
+
if (!spacename || !password ||
|
|
1509
|
+
typeof spacename !== 'string' ||
|
|
1510
|
+
typeof password !== 'string') {
|
|
1511
|
+
throw new Error('Invalid credentials format');
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
try {
|
|
1515
|
+
// Get space record
|
|
1516
|
+
const space = await this.getGlobal('spaces', spacename);
|
|
1517
|
+
if (!space || !space.auth) {
|
|
1518
|
+
throw new Error('Invalid spacename or password');
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Verify password using SEA
|
|
1522
|
+
const hash = await Gun.SEA.work(password, space.auth.salt);
|
|
1523
|
+
if (hash !== space.auth.hash) {
|
|
1524
|
+
throw new Error('Invalid spacename or password');
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Set current space with expiration
|
|
1528
|
+
this.currentSpace = {
|
|
1529
|
+
...space,
|
|
1530
|
+
exp: Date.now() + (24 * 60 * 60 * 1000) // 24 hour expiration
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
return true;
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
throw new Error('Authentication failed');
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Logs out the current space
|
|
1541
|
+
* @returns {Promise<void>}
|
|
1542
|
+
*/
|
|
1543
|
+
async logout() {
|
|
1544
|
+
this.currentSpace = null;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Checks if the current session is valid
|
|
1549
|
+
* @private
|
|
1550
|
+
*/
|
|
1551
|
+
_checkSession() {
|
|
1552
|
+
if (!this.currentSpace) {
|
|
1553
|
+
throw new Error('No active session');
|
|
1554
|
+
}
|
|
1555
|
+
if (this.currentSpace.exp < Date.now()) {
|
|
1556
|
+
this.currentSpace = null;
|
|
1557
|
+
throw new Error('Session expired');
|
|
1558
|
+
}
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Creates a federation relationship between two spaces
|
|
1564
|
+
* @param {string} spaceId1 - The first space ID
|
|
1565
|
+
* @param {string} spaceId2 - The second space ID
|
|
1566
|
+
* @returns {Promise<boolean>} - True if federation was created successfully
|
|
1567
|
+
*/
|
|
1568
|
+
async federate(spaceId1, spaceId2) {
|
|
1569
|
+
if (!spaceId1 || !spaceId2) {
|
|
1570
|
+
throw new Error('federate: Missing required parameters');
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Get existing federation info for both spaces
|
|
1574
|
+
let fedInfo1 = await this.getGlobal('federation', spaceId1);
|
|
1575
|
+
let fedInfo2 = await this.getGlobal('federation', spaceId2);
|
|
1576
|
+
|
|
1577
|
+
// Check if federation already exists
|
|
1578
|
+
if (fedInfo1 && fedInfo1.federation && fedInfo1.federation.includes(spaceId2)) {
|
|
1579
|
+
throw new Error('Federation already exists');
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Create or update federation info for first space
|
|
1583
|
+
if (!fedInfo1) {
|
|
1584
|
+
fedInfo1 = {
|
|
1585
|
+
id: spaceId1,
|
|
1586
|
+
name: spaceId1,
|
|
1587
|
+
federation: [],
|
|
1588
|
+
notify: []
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
if (!fedInfo1.federation) fedInfo1.federation = [];
|
|
1592
|
+
fedInfo1.federation.push(spaceId2);
|
|
1593
|
+
|
|
1594
|
+
// Create or update federation info for second space
|
|
1595
|
+
if (!fedInfo2) {
|
|
1596
|
+
fedInfo2 = {
|
|
1597
|
+
id: spaceId2,
|
|
1598
|
+
name: spaceId2,
|
|
1599
|
+
federation: [],
|
|
1600
|
+
notify: []
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
if (!fedInfo2.notify) fedInfo2.notify = [];
|
|
1604
|
+
fedInfo2.notify.push(spaceId1);
|
|
1605
|
+
|
|
1606
|
+
// Save both federation records
|
|
1607
|
+
await this.putGlobal('federation', fedInfo1);
|
|
1608
|
+
await this.putGlobal('federation', fedInfo2);
|
|
1609
|
+
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Subscribes to federation notifications for a space
|
|
1615
|
+
* @param {string} spaceId - The space ID to subscribe to
|
|
1616
|
+
* @param {function} callback - The callback to execute on notifications
|
|
1617
|
+
* @returns {Promise<object>} - Subscription object with off() method
|
|
1618
|
+
*/
|
|
1619
|
+
async subscribeFederation(spaceId, callback) {
|
|
1620
|
+
if (!spaceId || !callback) {
|
|
1621
|
+
throw new Error('subscribeFederation: Missing required parameters');
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Get federation info
|
|
1625
|
+
const fedInfo = await this.getGlobal('federation', spaceId);
|
|
1626
|
+
if (!fedInfo) {
|
|
1627
|
+
throw new Error('No federation info found for space');
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Create subscription for each federated space
|
|
1631
|
+
const subscriptions = [];
|
|
1632
|
+
if (fedInfo.federation && fedInfo.federation.length > 0) {
|
|
1633
|
+
for (const federatedSpace of fedInfo.federation) {
|
|
1634
|
+
// Subscribe to all lenses in the federated space
|
|
1635
|
+
const sub = await this.subscribe(federatedSpace, '*', async (data) => {
|
|
1636
|
+
try {
|
|
1637
|
+
// Only notify if the data has federation info and is from the federated space
|
|
1638
|
+
if (data && data.federation && data.federation.origin === federatedSpace) {
|
|
1639
|
+
await callback(data);
|
|
1640
|
+
}
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
console.warn('Federation notification error:', error);
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
subscriptions.push(sub);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// Return combined subscription object
|
|
1650
|
+
return {
|
|
1651
|
+
off: () => {
|
|
1652
|
+
subscriptions.forEach(sub => {
|
|
1653
|
+
if (sub && typeof sub.off === 'function') {
|
|
1654
|
+
sub.off();
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Gets federation info for a space
|
|
1663
|
+
* @param {string} spaceId - The space ID
|
|
1664
|
+
* @returns {Promise<object|null>} - Federation info or null if not found
|
|
1665
|
+
*/
|
|
1666
|
+
async getFederation(spaceId) {
|
|
1667
|
+
if (!spaceId) {
|
|
1668
|
+
throw new Error('getFederationInfo: Missing space ID');
|
|
1669
|
+
}
|
|
1670
|
+
return await this.getGlobal('federation', spaceId);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* Removes a federation relationship between spaces
|
|
1675
|
+
* @param {string} spaceId1 - The first space ID
|
|
1676
|
+
* @param {string} spaceId2 - The second space ID
|
|
1677
|
+
* @returns {Promise<boolean>} - True if federation was removed successfully
|
|
1678
|
+
*/
|
|
1679
|
+
async unfederate(spaceId1, spaceId2) {
|
|
1680
|
+
if (!spaceId1 || !spaceId2) {
|
|
1681
|
+
throw new Error('unfederate: Missing required parameters');
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Get federation info for both spaces
|
|
1685
|
+
const fedInfo1 = await this.getGlobal('federation', spaceId1);
|
|
1686
|
+
const fedInfo2 = await this.getGlobal('federation', spaceId2);
|
|
1687
|
+
|
|
1688
|
+
if (fedInfo1) {
|
|
1689
|
+
fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
|
|
1690
|
+
await this.putGlobal('federation', fedInfo1);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (fedInfo2) {
|
|
1694
|
+
fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
|
|
1695
|
+
await this.putGlobal('federation', fedInfo2);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
return true;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* Gets the name of a chat/space
|
|
1703
|
+
* @param {string} spaceId - The space ID
|
|
1704
|
+
* @returns {Promise<string>} - The space name or the ID if not found
|
|
1705
|
+
*/
|
|
1706
|
+
async getChatName(spaceId) {
|
|
1707
|
+
const spaceInfo = await this.getGlobal('spaces', spaceId);
|
|
1708
|
+
return spaceInfo?.name || spaceId;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Gets data from a holon and lens, including data from federated spaces with optional aggregation
|
|
1713
|
+
* @param {string} holon - The holon identifier
|
|
1714
|
+
* @param {string} lens - The lens identifier
|
|
1715
|
+
* @param {object} options - Options for data retrieval and aggregation
|
|
1716
|
+
* @param {boolean} options.aggregate - Whether to aggregate items with matching IDs (default: false)
|
|
1717
|
+
* @param {string} options.idField - Field to use as identifier for aggregation (default: 'id')
|
|
1718
|
+
* @param {string[]} options.sumFields - Numeric fields to sum during aggregation (e.g., ['received', 'sent'])
|
|
1719
|
+
* @param {string[]} options.concatArrays - Array fields to concatenate during aggregation (e.g., ['wants', 'offers'])
|
|
1720
|
+
* @param {boolean} options.removeDuplicates - Whether to remove duplicates when not aggregating (default: true)
|
|
1721
|
+
* @param {function} options.mergeStrategy - Custom function to merge items during aggregation
|
|
1722
|
+
* @returns {Promise<Array>} - Combined array of local and federated data
|
|
1723
|
+
*/
|
|
1724
|
+
async getFederated(holon, lens, options = {}) {
|
|
1725
|
+
// Validate required parameters
|
|
1726
|
+
if (!holon || !lens) {
|
|
1727
|
+
throw new Error('getFederated: Missing required parameters');
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const {
|
|
1731
|
+
aggregate = false,
|
|
1732
|
+
idField = 'id',
|
|
1733
|
+
sumFields = [],
|
|
1734
|
+
concatArrays = [],
|
|
1735
|
+
removeDuplicates = true,
|
|
1736
|
+
mergeStrategy = null
|
|
1737
|
+
} = options;
|
|
1738
|
+
|
|
1739
|
+
// Get federation info for current space
|
|
1740
|
+
const fedInfo = await this.getFederation(this.currentSpace?.alias);
|
|
1741
|
+
|
|
1742
|
+
// Get local data
|
|
1743
|
+
const localData = await this.getAll(holon, lens);
|
|
1744
|
+
|
|
1745
|
+
// If no federation or not authenticated, return local data only
|
|
1746
|
+
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
1747
|
+
return localData;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Get data from each federated space
|
|
1751
|
+
const federatedData = await Promise.all(
|
|
1752
|
+
fedInfo.federation.map(async (federatedSpace) => {
|
|
1753
|
+
try {
|
|
1754
|
+
const data = await this.getAll(federatedSpace, lens);
|
|
1755
|
+
return data || [];
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
console.warn(`Error getting data from federated space ${federatedSpace}:`, error);
|
|
1758
|
+
return [];
|
|
1759
|
+
}
|
|
1760
|
+
})
|
|
1761
|
+
);
|
|
1762
|
+
|
|
1763
|
+
// Combine all data
|
|
1764
|
+
const allData = [...localData, ...federatedData.flat()];
|
|
1765
|
+
|
|
1766
|
+
// If aggregating, use enhanced aggregation logic
|
|
1767
|
+
if (aggregate) {
|
|
1768
|
+
const aggregated = new Map();
|
|
1769
|
+
|
|
1770
|
+
for (const item of allData) {
|
|
1771
|
+
const itemId = item[idField];
|
|
1772
|
+
if (!itemId) continue;
|
|
1773
|
+
|
|
1774
|
+
const existing = aggregated.get(itemId);
|
|
1775
|
+
if (!existing) {
|
|
1776
|
+
aggregated.set(itemId, { ...item });
|
|
1777
|
+
} else {
|
|
1778
|
+
// If custom merge strategy is provided, use it
|
|
1779
|
+
if (mergeStrategy && typeof mergeStrategy === 'function') {
|
|
1780
|
+
aggregated.set(itemId, mergeStrategy(existing, item));
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Enhanced default merge strategy
|
|
1785
|
+
const merged = { ...existing };
|
|
1786
|
+
|
|
1787
|
+
// Sum numeric fields
|
|
1788
|
+
for (const field of sumFields) {
|
|
1789
|
+
if (typeof item[field] === 'number') {
|
|
1790
|
+
merged[field] = (merged[field] || 0) + (item[field] || 0);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Concatenate and deduplicate array fields
|
|
1795
|
+
for (const field of concatArrays) {
|
|
1796
|
+
if (Array.isArray(item[field])) {
|
|
1797
|
+
const combinedArray = [
|
|
1798
|
+
...(merged[field] || []),
|
|
1799
|
+
...(item[field] || [])
|
|
1800
|
+
];
|
|
1801
|
+
// Remove duplicates if elements are primitive
|
|
1802
|
+
merged[field] = Array.from(new Set(combinedArray));
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Update federation metadata
|
|
1807
|
+
merged.federation = {
|
|
1808
|
+
...merged.federation,
|
|
1809
|
+
timestamp: Math.max(
|
|
1810
|
+
merged.federation?.timestamp || 0,
|
|
1811
|
+
item.federation?.timestamp || 0
|
|
1812
|
+
),
|
|
1813
|
+
origins: Array.from(new Set([
|
|
1814
|
+
...(merged.federation?.origins || [merged.federation?.origin]),
|
|
1815
|
+
...(item.federation?.origins || [item.federation?.origin])
|
|
1816
|
+
]).filter(Boolean))
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// Update the aggregated item
|
|
1820
|
+
aggregated.set(itemId, merged);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
return Array.from(aggregated.values());
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// If not aggregating, optionally remove duplicates based on idField
|
|
1828
|
+
if (!removeDuplicates) {
|
|
1829
|
+
return allData;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// Remove duplicates keeping the most recent version
|
|
1833
|
+
const uniqueMap = new Map();
|
|
1834
|
+
allData.forEach(item => {
|
|
1835
|
+
const id = item[idField];
|
|
1836
|
+
if (!id) return;
|
|
1837
|
+
|
|
1838
|
+
const existing = uniqueMap.get(id);
|
|
1839
|
+
if (!existing ||
|
|
1840
|
+
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
1841
|
+
uniqueMap.set(id, item);
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
return Array.from(uniqueMap.values());
|
|
999
1845
|
}
|
|
1000
1846
|
}
|
|
1001
1847
|
|