holosphere 1.1.3 → 1.1.5
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 +236 -257
- package/package.json +9 -3
- package/services/environmentalApi.js +253 -0
- package/services/environmentalApi.test.js +164 -0
- package/test/ai.test.js +233 -0
- package/test/federation.test.js +2 -57
- package/test/holosphere.test.js +68 -107
- package/test/jest.setup.js +5 -0
- package/test/spacesauth.test.js +0 -2
package/holosphere.js
CHANGED
|
@@ -1,7 +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'
|
|
4
|
+
import SEA from 'gun/sea.js'
|
|
5
5
|
import Ajv2019 from 'ajv/dist/2019.js'
|
|
6
6
|
|
|
7
7
|
|
|
@@ -14,6 +14,7 @@ class HoloSphere {
|
|
|
14
14
|
* @param {Gun|null} gunInstance - The Gun instance to use.
|
|
15
15
|
*/
|
|
16
16
|
constructor(appname, strict = false, openaikey = null, gunInstance = null) {
|
|
17
|
+
console.log('HoloSphere v1.1.8');
|
|
17
18
|
this.appname = appname
|
|
18
19
|
this.strict = strict;
|
|
19
20
|
this.validator = new Ajv2019({
|
|
@@ -21,10 +22,10 @@ class HoloSphere {
|
|
|
21
22
|
strict: false, // Keep this false to avoid Ajv strict mode issues
|
|
22
23
|
validateSchema: true // Always validate schemas
|
|
23
24
|
});
|
|
24
|
-
|
|
25
|
+
|
|
25
26
|
// Use provided Gun instance or create new one
|
|
26
27
|
this.gun = gunInstance || Gun({
|
|
27
|
-
peers: ['https://gun.holons.io/gun'
|
|
28
|
+
peers: ['https://gun.holons.io/gun'],
|
|
28
29
|
axe: false,
|
|
29
30
|
// uuid: (content) => { // generate a unique id for each node
|
|
30
31
|
// console.log('uuid', content);
|
|
@@ -32,7 +33,7 @@ class HoloSphere {
|
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
// Initialize SEA
|
|
35
|
-
this.sea =
|
|
36
|
+
this.sea = SEA;
|
|
36
37
|
|
|
37
38
|
if (openaikey != null) {
|
|
38
39
|
this.openai = new OpenAI({
|
|
@@ -42,6 +43,9 @@ class HoloSphere {
|
|
|
42
43
|
|
|
43
44
|
// Add currentSpace property to track logged in space
|
|
44
45
|
this.currentSpace = null;
|
|
46
|
+
|
|
47
|
+
// Initialize subscriptions
|
|
48
|
+
this.subscriptions = {};
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
// ================================ SCHEMA FUNCTIONS ================================
|
|
@@ -62,68 +66,50 @@ class HoloSphere {
|
|
|
62
66
|
throw new Error('setSchema: Schema must have a type field');
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const metaSchema = {
|
|
70
|
+
type: 'object',
|
|
71
|
+
required: ['type', 'properties'],
|
|
72
|
+
properties: {
|
|
73
|
+
type: { type: 'string' },
|
|
69
74
|
properties: {
|
|
70
|
-
type:
|
|
71
|
-
|
|
75
|
+
type: 'object',
|
|
76
|
+
additionalProperties: {
|
|
72
77
|
type: 'object',
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
properties: {
|
|
77
|
-
type: { type: 'string' }
|
|
78
|
-
}
|
|
78
|
+
required: ['type'],
|
|
79
|
+
properties: {
|
|
80
|
+
type: { type: 'string' }
|
|
79
81
|
}
|
|
80
|
-
},
|
|
81
|
-
required: {
|
|
82
|
-
type: 'array',
|
|
83
|
-
items: { type: 'string' }
|
|
84
82
|
}
|
|
83
|
+
},
|
|
84
|
+
required: {
|
|
85
|
+
type: 'array',
|
|
86
|
+
items: { type: 'string' }
|
|
85
87
|
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const valid = this.validator.validate(metaSchema, schema);
|
|
89
|
-
if (!valid) {
|
|
90
|
-
throw new Error(`Invalid schema structure: ${JSON.stringify(this.validator.errors)}`);
|
|
91
88
|
}
|
|
89
|
+
};
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
91
|
+
const valid = this.validator.validate(metaSchema, schema);
|
|
92
|
+
if (!valid) {
|
|
93
|
+
throw new Error(`Invalid schema structure: ${JSON.stringify(this.validator.errors)}`);
|
|
94
|
+
}
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
96
|
+
if (!schema.properties || typeof schema.properties !== 'object') {
|
|
97
|
+
throw new Error('Schema must have properties in strict mode');
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.gun.get(this.appname)
|
|
113
|
-
.get(lens)
|
|
114
|
-
.get('schema')
|
|
115
|
-
.put(schemaData, ack => {
|
|
116
|
-
if (ack.err) {
|
|
117
|
-
reject(new Error(ack.err));
|
|
118
|
-
} else {
|
|
119
|
-
// Add small delay to ensure data is written
|
|
120
|
-
setTimeout(() => resolve(true), 50);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
} catch (error) {
|
|
124
|
-
reject(error);
|
|
125
|
-
}
|
|
100
|
+
if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
|
|
101
|
+
throw new Error('Schema must have required fields in strict mode');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Store schema in global table with lens as key
|
|
105
|
+
await this.putGlobal('schemas', {
|
|
106
|
+
id: lens,
|
|
107
|
+
schema: schema,
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
owner: this.currentSpace?.alias
|
|
126
110
|
});
|
|
111
|
+
|
|
112
|
+
return true;
|
|
127
113
|
}
|
|
128
114
|
|
|
129
115
|
/**
|
|
@@ -136,39 +122,12 @@ class HoloSphere {
|
|
|
136
122
|
throw new Error('getSchema: Missing lens parameter');
|
|
137
123
|
}
|
|
138
124
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}, 5000);
|
|
144
|
-
|
|
145
|
-
this.gun.get(this.appname)
|
|
146
|
-
.get(lens)
|
|
147
|
-
.get('schema')
|
|
148
|
-
.once(data => {
|
|
149
|
-
clearTimeout(timeout);
|
|
150
|
-
if (!data) {
|
|
151
|
-
resolve(null);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
125
|
+
const schemaData = await this.getGlobal('schemas', lens);
|
|
126
|
+
if (!schemaData || !schemaData.schema) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
154
129
|
|
|
155
|
-
|
|
156
|
-
// Handle both new format and legacy format
|
|
157
|
-
if (data.schema) {
|
|
158
|
-
// New format with timestamp
|
|
159
|
-
resolve(JSON.parse(data.schema));
|
|
160
|
-
} else {
|
|
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);
|
|
165
|
-
}
|
|
166
|
-
} catch (error) {
|
|
167
|
-
console.error('getSchema: Error parsing schema:', error);
|
|
168
|
-
resolve(null);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
});
|
|
130
|
+
return schemaData.schema;
|
|
172
131
|
}
|
|
173
132
|
|
|
174
133
|
// ================================ CONTENT FUNCTIONS ================================
|
|
@@ -190,8 +149,8 @@ class HoloSphere {
|
|
|
190
149
|
// If updating existing data, check ownership
|
|
191
150
|
if (data.id) {
|
|
192
151
|
const existing = await this.get(holon, lens, data.id);
|
|
193
|
-
if (existing && existing.owner &&
|
|
194
|
-
existing.owner !== this.currentSpace.alias &&
|
|
152
|
+
if (existing && existing.owner &&
|
|
153
|
+
existing.owner !== this.currentSpace.alias &&
|
|
195
154
|
!existing.federation) { // Skip ownership check for federated data
|
|
196
155
|
throw new Error('Unauthorized to modify this data');
|
|
197
156
|
}
|
|
@@ -221,7 +180,7 @@ class HoloSphere {
|
|
|
221
180
|
// Deep clone data to avoid modifying the original
|
|
222
181
|
const dataToValidate = JSON.parse(JSON.stringify(dataWithMeta));
|
|
223
182
|
const valid = this.validator.validate(schema, dataToValidate);
|
|
224
|
-
|
|
183
|
+
|
|
225
184
|
if (!valid) {
|
|
226
185
|
const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
|
|
227
186
|
// Always throw on schema validation failure, regardless of strict mode
|
|
@@ -243,6 +202,12 @@ class HoloSphere {
|
|
|
243
202
|
if (ack.err) {
|
|
244
203
|
reject(new Error(ack.err));
|
|
245
204
|
} else {
|
|
205
|
+
// Notify subscribers after successful put
|
|
206
|
+
this.notifySubscribers({
|
|
207
|
+
holon,
|
|
208
|
+
lens,
|
|
209
|
+
...dataWithMeta
|
|
210
|
+
});
|
|
246
211
|
resolve(true);
|
|
247
212
|
}
|
|
248
213
|
});
|
|
@@ -275,7 +240,7 @@ class HoloSphere {
|
|
|
275
240
|
}
|
|
276
241
|
|
|
277
242
|
// Propagate to each federated space
|
|
278
|
-
const propagationPromises = fedInfo.notify.map(spaceId =>
|
|
243
|
+
const propagationPromises = fedInfo.notify.map(spaceId =>
|
|
279
244
|
new Promise((resolve) => {
|
|
280
245
|
// Store data in the federated space's lens
|
|
281
246
|
this.gun.get(this.appname)
|
|
@@ -335,7 +300,7 @@ class HoloSphere {
|
|
|
335
300
|
|
|
336
301
|
// Get local data
|
|
337
302
|
const localData = await this._getAllLocal(holon, lens, schema);
|
|
338
|
-
|
|
303
|
+
|
|
339
304
|
// If authenticated, get federated data
|
|
340
305
|
let federatedData = [];
|
|
341
306
|
if (this.currentSpace) {
|
|
@@ -344,7 +309,7 @@ class HoloSphere {
|
|
|
344
309
|
|
|
345
310
|
// Combine and deduplicate data based on ID
|
|
346
311
|
const combined = new Map();
|
|
347
|
-
|
|
312
|
+
|
|
348
313
|
// Add local data first
|
|
349
314
|
localData.forEach(item => {
|
|
350
315
|
if (item.id) {
|
|
@@ -356,7 +321,7 @@ class HoloSphere {
|
|
|
356
321
|
federatedData.forEach(item => {
|
|
357
322
|
if (item.id) {
|
|
358
323
|
const existing = combined.get(item.id);
|
|
359
|
-
if (!existing ||
|
|
324
|
+
if (!existing ||
|
|
360
325
|
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
361
326
|
combined.set(item.id, item);
|
|
362
327
|
}
|
|
@@ -426,7 +391,7 @@ class HoloSphere {
|
|
|
426
391
|
const output = new Map();
|
|
427
392
|
let isResolved = false;
|
|
428
393
|
let listener = null;
|
|
429
|
-
|
|
394
|
+
|
|
430
395
|
const hardTimeout = setTimeout(() => {
|
|
431
396
|
cleanup();
|
|
432
397
|
resolve(Array.from(output.values()));
|
|
@@ -505,7 +470,7 @@ class HoloSphere {
|
|
|
505
470
|
}
|
|
506
471
|
|
|
507
472
|
const federatedData = new Map();
|
|
508
|
-
|
|
473
|
+
|
|
509
474
|
// Get data from each federated space
|
|
510
475
|
const fedPromises = fedInfo.federation.map(spaceId =>
|
|
511
476
|
new Promise((resolve) => {
|
|
@@ -558,11 +523,19 @@ class HoloSphere {
|
|
|
558
523
|
* @returns {Promise<object>} - The parsed data.
|
|
559
524
|
*/
|
|
560
525
|
async parse(rawData) {
|
|
526
|
+
let parsedData = {};
|
|
527
|
+
|
|
561
528
|
if (!rawData) {
|
|
562
529
|
throw new Error('parse: No data provided');
|
|
563
530
|
}
|
|
564
531
|
|
|
565
532
|
try {
|
|
533
|
+
|
|
534
|
+
if (typeof rawData === 'string') {
|
|
535
|
+
parsedData = await JSON.parse(rawData);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
566
539
|
if (rawData.soul) {
|
|
567
540
|
const data = await this.getNodeRef(rawData.soul).once();
|
|
568
541
|
if (!data) {
|
|
@@ -571,7 +544,7 @@ class HoloSphere {
|
|
|
571
544
|
return JSON.parse(data);
|
|
572
545
|
}
|
|
573
546
|
|
|
574
|
-
|
|
547
|
+
|
|
575
548
|
if (typeof rawData === 'object' && rawData !== null) {
|
|
576
549
|
if (rawData._ && rawData._["#"]) {
|
|
577
550
|
const pathParts = rawData._['#'].split('/');
|
|
@@ -591,11 +564,10 @@ class HoloSphere {
|
|
|
591
564
|
} else {
|
|
592
565
|
parsedData = rawData;
|
|
593
566
|
}
|
|
594
|
-
} else {
|
|
595
|
-
parsedData = JSON.parse(rawData);
|
|
596
567
|
}
|
|
597
568
|
|
|
598
569
|
return parsedData;
|
|
570
|
+
|
|
599
571
|
} catch (error) {
|
|
600
572
|
console.log("Parsing not a JSON, returning raw data", rawData);
|
|
601
573
|
return rawData;
|
|
@@ -632,7 +604,7 @@ class HoloSphere {
|
|
|
632
604
|
.get(key)
|
|
633
605
|
.once(async (data) => {
|
|
634
606
|
clearTimeout(timeout);
|
|
635
|
-
|
|
607
|
+
|
|
636
608
|
if (!data) {
|
|
637
609
|
resolve(null);
|
|
638
610
|
return;
|
|
@@ -658,7 +630,7 @@ class HoloSphere {
|
|
|
658
630
|
// 2. User is the owner
|
|
659
631
|
// 3. User is in shared list
|
|
660
632
|
// 4. Data is from federation
|
|
661
|
-
if (parsed.owner &&
|
|
633
|
+
if (parsed.owner &&
|
|
662
634
|
this.currentSpace?.alias !== parsed.owner &&
|
|
663
635
|
(!parsed.shared || !parsed.shared.includes(this.currentSpace?.alias)) &&
|
|
664
636
|
(!parsed.federation || !parsed.federation.origin)) {
|
|
@@ -711,7 +683,7 @@ class HoloSphere {
|
|
|
711
683
|
if (!data) {
|
|
712
684
|
return true; // Nothing to delete
|
|
713
685
|
}
|
|
714
|
-
|
|
686
|
+
|
|
715
687
|
if (data.owner && data.owner !== this.currentSpace.alias) {
|
|
716
688
|
throw new Error('Unauthorized to delete this data');
|
|
717
689
|
}
|
|
@@ -780,7 +752,7 @@ class HoloSphere {
|
|
|
780
752
|
.catch(error => {
|
|
781
753
|
console.error('Error in deleteAll:', error);
|
|
782
754
|
resolve(false);
|
|
783
|
-
|
|
755
|
+
});
|
|
784
756
|
});
|
|
785
757
|
});
|
|
786
758
|
}
|
|
@@ -985,12 +957,12 @@ class HoloSphere {
|
|
|
985
957
|
}
|
|
986
958
|
|
|
987
959
|
const keys = Object.keys(data).filter(key => key !== '_');
|
|
988
|
-
const promises = keys.map(key =>
|
|
960
|
+
const promises = keys.map(key =>
|
|
989
961
|
new Promise(async (resolveItem) => {
|
|
990
962
|
const itemData = await new Promise(resolveData => {
|
|
991
963
|
this.gun.get(this.appname).get(tableName).get(key).once(resolveData);
|
|
992
964
|
});
|
|
993
|
-
|
|
965
|
+
|
|
994
966
|
if (itemData) {
|
|
995
967
|
try {
|
|
996
968
|
const parsed = await this.parse(itemData);
|
|
@@ -1088,7 +1060,7 @@ class HoloSphere {
|
|
|
1088
1060
|
}
|
|
1089
1061
|
|
|
1090
1062
|
const keys = Object.keys(data).filter(key => key !== '_');
|
|
1091
|
-
const promises = keys.map(key =>
|
|
1063
|
+
const promises = keys.map(key =>
|
|
1092
1064
|
new Promise((resolveDelete) => {
|
|
1093
1065
|
this.gun.get(this.appname)
|
|
1094
1066
|
.get(tableName)
|
|
@@ -1120,21 +1092,62 @@ class HoloSphere {
|
|
|
1120
1092
|
|
|
1121
1093
|
// ================================ COMPUTE FUNCTIONS ================================
|
|
1122
1094
|
/**
|
|
1123
|
-
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Computes operations across multiple layers up the hierarchy
|
|
1098
|
+
* @param {string} holon - Starting holon identifier
|
|
1099
|
+
* @param {string} lens - The lens to compute
|
|
1100
|
+
* @param {object} options - Computation options
|
|
1101
|
+
* @param {number} [maxLevels=15] - Maximum levels to compute up
|
|
1102
|
+
*/
|
|
1103
|
+
async computeHierarchy(holon, lens, options, maxLevels = 15) {
|
|
1104
|
+
let currentHolon = holon;
|
|
1105
|
+
let currentRes = h3.getResolution(currentHolon);
|
|
1106
|
+
const results = [];
|
|
1107
|
+
|
|
1108
|
+
while (currentRes > 0 && maxLevels > 0) {
|
|
1109
|
+
try {
|
|
1110
|
+
const result = await this.compute(currentHolon, lens, options);
|
|
1111
|
+
if (result) {
|
|
1112
|
+
results.push(result);
|
|
1113
|
+
}
|
|
1114
|
+
currentHolon = h3.cellToParent(currentHolon, currentRes - 1);
|
|
1115
|
+
currentRes--;
|
|
1116
|
+
maxLevels--;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
console.error('Error in compute hierarchy:', error);
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return results;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/* Computes operations on content within a holon and lens for one layer up.
|
|
1124
1127
|
* @param {string} holon - The holon identifier.
|
|
1125
1128
|
* @param {string} lens - The lens to compute.
|
|
1126
|
-
* @param {
|
|
1127
|
-
* @param {
|
|
1128
|
-
* @param {
|
|
1129
|
+
* @param {object} options - Computation options
|
|
1130
|
+
* @param {string} options.operation - The operation to perform ('summarize', 'aggregate', 'concatenate')
|
|
1131
|
+
* @param {string[]} [options.fields] - Fields to perform operation on
|
|
1132
|
+
* @param {string} [options.targetField] - Field to store the result in
|
|
1129
1133
|
* @throws {Error} If parameters are invalid or missing
|
|
1130
1134
|
*/
|
|
1131
|
-
async compute(holon, lens,
|
|
1135
|
+
async compute(holon, lens, options) {
|
|
1132
1136
|
// Validate required parameters
|
|
1133
|
-
if (!holon || !lens
|
|
1137
|
+
if (!holon || !lens) {
|
|
1138
|
+
throw new Error('compute: Missing required parameters');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Convert string operation to options object
|
|
1142
|
+
if (typeof options === 'string') {
|
|
1143
|
+
options = { operation: options };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (!options?.operation) {
|
|
1134
1147
|
throw new Error('compute: Missing required parameters');
|
|
1135
1148
|
}
|
|
1136
1149
|
|
|
1137
|
-
// Validate holon format and resolution
|
|
1150
|
+
// Validate holon format and resolution first
|
|
1138
1151
|
let res;
|
|
1139
1152
|
try {
|
|
1140
1153
|
res = h3.getResolution(holon);
|
|
@@ -1146,141 +1159,108 @@ class HoloSphere {
|
|
|
1146
1159
|
throw new Error('compute: Invalid holon resolution (must be between 1 and 15)');
|
|
1147
1160
|
}
|
|
1148
1161
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1162
|
+
const {
|
|
1163
|
+
operation,
|
|
1164
|
+
fields = [],
|
|
1165
|
+
targetField,
|
|
1166
|
+
depth,
|
|
1167
|
+
maxDepth
|
|
1168
|
+
} = options;
|
|
1169
|
+
|
|
1170
|
+
// Validate depth parameters if provided
|
|
1171
|
+
if (depth !== undefined && depth < 0) {
|
|
1151
1172
|
throw new Error('compute: Invalid depth parameter');
|
|
1152
1173
|
}
|
|
1153
1174
|
|
|
1154
|
-
if (
|
|
1175
|
+
if (maxDepth !== undefined && (maxDepth < 1 || maxDepth > 15)) {
|
|
1155
1176
|
throw new Error('compute: Invalid maxDepth parameter (must be between 1 and 15)');
|
|
1156
1177
|
}
|
|
1157
1178
|
|
|
1158
|
-
if (depth >= maxDepth) {
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
1179
|
// Validate operation
|
|
1163
|
-
|
|
1164
|
-
|
|
1180
|
+
const validOperations = ['summarize', 'aggregate', 'concatenate'];
|
|
1181
|
+
if (!validOperations.includes(operation)) {
|
|
1182
|
+
throw new Error(`compute: Invalid operation (must be one of ${validOperations.join(', ')})`);
|
|
1165
1183
|
}
|
|
1166
1184
|
|
|
1167
1185
|
const parent = h3.cellToParent(holon, res - 1);
|
|
1168
1186
|
const siblings = h3.cellToChildren(parent, res);
|
|
1169
1187
|
|
|
1170
|
-
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
const timeout = setTimeout(() => {
|
|
1174
|
-
console.warn(`Timeout for sibling ${sibling}`);
|
|
1175
|
-
resolve();
|
|
1176
|
-
}, 10000);
|
|
1177
|
-
|
|
1178
|
-
this.gun.get(this.appname)
|
|
1179
|
-
.get(sibling)
|
|
1180
|
-
.get(lens)
|
|
1181
|
-
.map()
|
|
1182
|
-
.once((data) => {
|
|
1183
|
-
clearTimeout(timeout);
|
|
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);
|
|
1197
|
-
}
|
|
1198
|
-
resolve();
|
|
1199
|
-
});
|
|
1200
|
-
})
|
|
1188
|
+
// Collect all content from siblings
|
|
1189
|
+
const contents = await Promise.all(
|
|
1190
|
+
siblings.map(sibling => this.getAll(sibling, lens))
|
|
1201
1191
|
);
|
|
1202
1192
|
|
|
1203
|
-
|
|
1193
|
+
const flatContents = contents.flat().filter(Boolean);
|
|
1204
1194
|
|
|
1205
|
-
if (
|
|
1195
|
+
if (flatContents.length > 0) {
|
|
1206
1196
|
try {
|
|
1207
|
-
|
|
1197
|
+
let computed;
|
|
1198
|
+
switch (operation) {
|
|
1199
|
+
case 'summarize':
|
|
1200
|
+
// For summarize, concatenate specified fields or use entire content
|
|
1201
|
+
const textToSummarize = fields.length > 0
|
|
1202
|
+
? flatContents.map(item => fields.map(field => item[field]).filter(Boolean).join('\n')).join('\n')
|
|
1203
|
+
: JSON.stringify(flatContents);
|
|
1204
|
+
computed = await this.summarize(textToSummarize);
|
|
1205
|
+
break;
|
|
1206
|
+
|
|
1207
|
+
case 'aggregate':
|
|
1208
|
+
// For aggregate, sum numeric fields
|
|
1209
|
+
computed = fields.reduce((acc, field) => {
|
|
1210
|
+
acc[field] = flatContents.reduce((sum, item) => {
|
|
1211
|
+
return sum + (Number(item[field]) || 0);
|
|
1212
|
+
}, 0);
|
|
1213
|
+
return acc;
|
|
1214
|
+
}, {});
|
|
1215
|
+
break;
|
|
1216
|
+
|
|
1217
|
+
case 'concatenate':
|
|
1218
|
+
// For concatenate, combine arrays or strings
|
|
1219
|
+
computed = fields.reduce((acc, field) => {
|
|
1220
|
+
acc[field] = flatContents.reduce((combined, item) => {
|
|
1221
|
+
const value = item[field];
|
|
1222
|
+
if (Array.isArray(value)) {
|
|
1223
|
+
return [...combined, ...value];
|
|
1224
|
+
} else if (value) {
|
|
1225
|
+
return [...combined, value];
|
|
1226
|
+
}
|
|
1227
|
+
return combined;
|
|
1228
|
+
}, []);
|
|
1229
|
+
// Remove duplicates if array
|
|
1230
|
+
acc[field] = Array.from(new Set(acc[field]));
|
|
1231
|
+
return acc;
|
|
1232
|
+
}, {});
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1208
1236
|
if (computed) {
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
id:
|
|
1212
|
-
content: computed,
|
|
1237
|
+
const resultId = `${parent}_${operation}`;
|
|
1238
|
+
const result = {
|
|
1239
|
+
id: resultId,
|
|
1213
1240
|
timestamp: Date.now()
|
|
1214
|
-
}
|
|
1241
|
+
};
|
|
1215
1242
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1243
|
+
// Store result in targetField if specified, otherwise at root level
|
|
1244
|
+
if (targetField) {
|
|
1245
|
+
result[targetField] = computed;
|
|
1246
|
+
} else if (typeof computed === 'object') {
|
|
1247
|
+
Object.assign(result, computed);
|
|
1248
|
+
} else {
|
|
1249
|
+
result.value = computed;
|
|
1218
1250
|
}
|
|
1251
|
+
|
|
1252
|
+
await this.put(parent, lens, result);
|
|
1253
|
+
return result;
|
|
1219
1254
|
}
|
|
1220
1255
|
} catch (error) {
|
|
1221
1256
|
console.warn('Error in compute operation:', error);
|
|
1222
|
-
|
|
1257
|
+
throw error;
|
|
1223
1258
|
}
|
|
1224
1259
|
}
|
|
1225
1260
|
|
|
1226
|
-
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
/**
|
|
1231
|
-
* Clears all entities under a specific holon and lens.
|
|
1232
|
-
* @param {string} holon - The holon identifier.
|
|
1233
|
-
* @param {string} lens - The lens to clear.
|
|
1234
|
-
*/
|
|
1235
|
-
async clearlens(holon, lens) {
|
|
1236
|
-
if (!holon || !lens) {
|
|
1237
|
-
throw new Error('clearlens: Missing required parameters');
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
return new Promise((resolve, reject) => {
|
|
1241
|
-
try {
|
|
1242
|
-
const deletions = new Set();
|
|
1243
|
-
const timeout = setTimeout(() => {
|
|
1244
|
-
if (deletions.size === 0) {
|
|
1245
|
-
resolve(); // No data to delete
|
|
1246
|
-
}
|
|
1247
|
-
}, 1000);
|
|
1248
|
-
|
|
1249
|
-
this.gun.get(this.appname)
|
|
1250
|
-
.get(holon)
|
|
1251
|
-
.get(lens)
|
|
1252
|
-
.map()
|
|
1253
|
-
.once((data, key) => {
|
|
1254
|
-
if (data) {
|
|
1255
|
-
const deletion = new Promise((resolveDelete) => {
|
|
1256
|
-
this.gun.get(this.appname)
|
|
1257
|
-
.get(holon)
|
|
1258
|
-
.get(lens)
|
|
1259
|
-
.get(key)
|
|
1260
|
-
.put(null, ack => {
|
|
1261
|
-
if (ack.err) {
|
|
1262
|
-
console.error(`Failed to delete ${key}:`, ack.err);
|
|
1263
|
-
}
|
|
1264
|
-
resolveDelete();
|
|
1265
|
-
});
|
|
1266
|
-
});
|
|
1267
|
-
deletions.add(deletion);
|
|
1268
|
-
deletion.finally(() => {
|
|
1269
|
-
deletions.delete(deletion);
|
|
1270
|
-
if (deletions.size === 0) {
|
|
1271
|
-
clearTimeout(timeout);
|
|
1272
|
-
resolve();
|
|
1273
|
-
}
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
});
|
|
1277
|
-
} catch (error) {
|
|
1278
|
-
reject(error);
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1261
|
+
return null;
|
|
1281
1262
|
}
|
|
1282
1263
|
|
|
1283
|
-
|
|
1284
1264
|
/**
|
|
1285
1265
|
* Summarizes provided history text using OpenAI.
|
|
1286
1266
|
* @param {string} history - The history text to summarize.
|
|
@@ -1406,40 +1386,33 @@ class HoloSphere {
|
|
|
1406
1386
|
* @param {function} callback - The callback to execute on changes.
|
|
1407
1387
|
*/
|
|
1408
1388
|
async subscribe(holon, lens, callback) {
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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);
|
|
1389
|
+
const subscriptionId = this.generateId();
|
|
1390
|
+
this.subscriptions[subscriptionId] =
|
|
1391
|
+
this.gun.get(this.appname).get(holon).get(lens).map().on( async (data, key) => {
|
|
1392
|
+
if (data) {
|
|
1393
|
+
try {
|
|
1394
|
+
let parsed = await this.parse(data)
|
|
1395
|
+
callback(parsed, key)
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
console.error('Error in subscribe:', error);
|
|
1398
|
+
}
|
|
1425
1399
|
}
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1400
|
+
})
|
|
1401
|
+
return {
|
|
1402
|
+
unsubscribe: () => {
|
|
1403
|
+
this.gun.get(this.appname).get(holon).get(lens).map().off()
|
|
1404
|
+
delete this.subscriptions[subscriptionId];
|
|
1428
1405
|
}
|
|
1429
|
-
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1430
1408
|
|
|
1431
|
-
// Subscribe using Gun's map() and on()
|
|
1432
|
-
const chain = ref.map();
|
|
1433
|
-
chain.on(handler);
|
|
1434
1409
|
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
chain.off();
|
|
1440
|
-
}
|
|
1410
|
+
notifySubscribers(data) {
|
|
1411
|
+
Object.values(this.subscriptions).forEach(subscription => {
|
|
1412
|
+
if (subscription.active && this.matchesQuery(data, subscription.query)) {
|
|
1413
|
+
subscription.callback(data);
|
|
1441
1414
|
}
|
|
1442
|
-
};
|
|
1415
|
+
});
|
|
1443
1416
|
}
|
|
1444
1417
|
|
|
1445
1418
|
// Add ID generation method
|
|
@@ -1447,6 +1420,12 @@ class HoloSphere {
|
|
|
1447
1420
|
return Date.now().toString(10) + Math.random().toString(2);
|
|
1448
1421
|
}
|
|
1449
1422
|
|
|
1423
|
+
matchesQuery(data, query) {
|
|
1424
|
+
return data && query &&
|
|
1425
|
+
data.holon === query.holon &&
|
|
1426
|
+
data.lens === query.lens;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1450
1429
|
/**
|
|
1451
1430
|
* Creates a new space with the given credentials
|
|
1452
1431
|
* @param {string} spacename - The space identifier/username
|
|
@@ -1467,7 +1446,7 @@ class HoloSphere {
|
|
|
1467
1446
|
try {
|
|
1468
1447
|
// Generate key pair
|
|
1469
1448
|
const pair = await Gun.SEA.pair();
|
|
1470
|
-
|
|
1449
|
+
|
|
1471
1450
|
// Create auth record with SEA
|
|
1472
1451
|
const salt = await Gun.SEA.random(64).toString('base64');
|
|
1473
1452
|
const hash = await Gun.SEA.work(password, salt);
|
|
@@ -1505,8 +1484,8 @@ class HoloSphere {
|
|
|
1505
1484
|
*/
|
|
1506
1485
|
async login(spacename, password) {
|
|
1507
1486
|
// Validate input
|
|
1508
|
-
if (!spacename || !password ||
|
|
1509
|
-
typeof spacename !== 'string' ||
|
|
1487
|
+
if (!spacename || !password ||
|
|
1488
|
+
typeof spacename !== 'string' ||
|
|
1510
1489
|
typeof password !== 'string') {
|
|
1511
1490
|
throw new Error('Invalid credentials format');
|
|
1512
1491
|
}
|
|
@@ -1738,10 +1717,10 @@ class HoloSphere {
|
|
|
1738
1717
|
|
|
1739
1718
|
// Get federation info for current space
|
|
1740
1719
|
const fedInfo = await this.getFederation(this.currentSpace?.alias);
|
|
1741
|
-
|
|
1720
|
+
|
|
1742
1721
|
// Get local data
|
|
1743
1722
|
const localData = await this.getAll(holon, lens);
|
|
1744
|
-
|
|
1723
|
+
|
|
1745
1724
|
// If no federation or not authenticated, return local data only
|
|
1746
1725
|
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
1747
1726
|
return localData;
|
|
@@ -1828,15 +1807,15 @@ class HoloSphere {
|
|
|
1828
1807
|
if (!removeDuplicates) {
|
|
1829
1808
|
return allData;
|
|
1830
1809
|
}
|
|
1831
|
-
|
|
1810
|
+
|
|
1832
1811
|
// Remove duplicates keeping the most recent version
|
|
1833
1812
|
const uniqueMap = new Map();
|
|
1834
1813
|
allData.forEach(item => {
|
|
1835
1814
|
const id = item[idField];
|
|
1836
1815
|
if (!id) return;
|
|
1837
|
-
|
|
1816
|
+
|
|
1838
1817
|
const existing = uniqueMap.get(id);
|
|
1839
|
-
if (!existing ||
|
|
1818
|
+
if (!existing ||
|
|
1840
1819
|
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
1841
1820
|
uniqueMap.set(id, item);
|
|
1842
1821
|
}
|