holosphere 1.1.3 → 1.1.4
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 +187 -171
- package/package.json +2 -1
- package/test/ai.test.js +233 -0
- package/test/federation.test.js +2 -57
- package/test/holosphere.test.js +43 -48
- 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
|
|
|
@@ -21,7 +21,7 @@ class HoloSphere {
|
|
|
21
21
|
strict: false, // Keep this false to avoid Ajv strict mode issues
|
|
22
22
|
validateSchema: true // Always validate schemas
|
|
23
23
|
});
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
// Use provided Gun instance or create new one
|
|
26
26
|
this.gun = gunInstance || Gun({
|
|
27
27
|
peers: ['https://gun.holons.io/gun', 'https://59.src.eco/gun'],
|
|
@@ -32,7 +32,7 @@ class HoloSphere {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
// Initialize SEA
|
|
35
|
-
this.sea =
|
|
35
|
+
this.sea = SEA;
|
|
36
36
|
|
|
37
37
|
if (openaikey != null) {
|
|
38
38
|
this.openai = new OpenAI({
|
|
@@ -42,6 +42,9 @@ class HoloSphere {
|
|
|
42
42
|
|
|
43
43
|
// Add currentSpace property to track logged in space
|
|
44
44
|
this.currentSpace = null;
|
|
45
|
+
|
|
46
|
+
// Initialize subscriptions
|
|
47
|
+
this.subscriptions = {};
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
// ================================ SCHEMA FUNCTIONS ================================
|
|
@@ -108,7 +111,7 @@ class HoloSphere {
|
|
|
108
111
|
// Only set owner if there's an authenticated space
|
|
109
112
|
...(this.currentSpace && { owner: this.currentSpace.alias })
|
|
110
113
|
};
|
|
111
|
-
|
|
114
|
+
|
|
112
115
|
this.gun.get(this.appname)
|
|
113
116
|
.get(lens)
|
|
114
117
|
.get('schema')
|
|
@@ -159,7 +162,7 @@ class HoloSphere {
|
|
|
159
162
|
resolve(JSON.parse(data.schema));
|
|
160
163
|
} else {
|
|
161
164
|
// Legacy format or direct string
|
|
162
|
-
const schemaStr = typeof data === 'string' ? data :
|
|
165
|
+
const schemaStr = typeof data === 'string' ? data :
|
|
163
166
|
Object.values(data).find(v => typeof v === 'string' && v.includes('"type":'));
|
|
164
167
|
resolve(schemaStr ? JSON.parse(schemaStr) : null);
|
|
165
168
|
}
|
|
@@ -190,8 +193,8 @@ class HoloSphere {
|
|
|
190
193
|
// If updating existing data, check ownership
|
|
191
194
|
if (data.id) {
|
|
192
195
|
const existing = await this.get(holon, lens, data.id);
|
|
193
|
-
if (existing && existing.owner &&
|
|
194
|
-
existing.owner !== this.currentSpace.alias &&
|
|
196
|
+
if (existing && existing.owner &&
|
|
197
|
+
existing.owner !== this.currentSpace.alias &&
|
|
195
198
|
!existing.federation) { // Skip ownership check for federated data
|
|
196
199
|
throw new Error('Unauthorized to modify this data');
|
|
197
200
|
}
|
|
@@ -221,7 +224,7 @@ class HoloSphere {
|
|
|
221
224
|
// Deep clone data to avoid modifying the original
|
|
222
225
|
const dataToValidate = JSON.parse(JSON.stringify(dataWithMeta));
|
|
223
226
|
const valid = this.validator.validate(schema, dataToValidate);
|
|
224
|
-
|
|
227
|
+
|
|
225
228
|
if (!valid) {
|
|
226
229
|
const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
|
|
227
230
|
// Always throw on schema validation failure, regardless of strict mode
|
|
@@ -243,6 +246,12 @@ class HoloSphere {
|
|
|
243
246
|
if (ack.err) {
|
|
244
247
|
reject(new Error(ack.err));
|
|
245
248
|
} else {
|
|
249
|
+
// Notify subscribers after successful put
|
|
250
|
+
this.notifySubscribers({
|
|
251
|
+
holon,
|
|
252
|
+
lens,
|
|
253
|
+
...dataWithMeta
|
|
254
|
+
});
|
|
246
255
|
resolve(true);
|
|
247
256
|
}
|
|
248
257
|
});
|
|
@@ -275,7 +284,7 @@ class HoloSphere {
|
|
|
275
284
|
}
|
|
276
285
|
|
|
277
286
|
// Propagate to each federated space
|
|
278
|
-
const propagationPromises = fedInfo.notify.map(spaceId =>
|
|
287
|
+
const propagationPromises = fedInfo.notify.map(spaceId =>
|
|
279
288
|
new Promise((resolve) => {
|
|
280
289
|
// Store data in the federated space's lens
|
|
281
290
|
this.gun.get(this.appname)
|
|
@@ -335,7 +344,7 @@ class HoloSphere {
|
|
|
335
344
|
|
|
336
345
|
// Get local data
|
|
337
346
|
const localData = await this._getAllLocal(holon, lens, schema);
|
|
338
|
-
|
|
347
|
+
|
|
339
348
|
// If authenticated, get federated data
|
|
340
349
|
let federatedData = [];
|
|
341
350
|
if (this.currentSpace) {
|
|
@@ -344,7 +353,7 @@ class HoloSphere {
|
|
|
344
353
|
|
|
345
354
|
// Combine and deduplicate data based on ID
|
|
346
355
|
const combined = new Map();
|
|
347
|
-
|
|
356
|
+
|
|
348
357
|
// Add local data first
|
|
349
358
|
localData.forEach(item => {
|
|
350
359
|
if (item.id) {
|
|
@@ -356,7 +365,7 @@ class HoloSphere {
|
|
|
356
365
|
federatedData.forEach(item => {
|
|
357
366
|
if (item.id) {
|
|
358
367
|
const existing = combined.get(item.id);
|
|
359
|
-
if (!existing ||
|
|
368
|
+
if (!existing ||
|
|
360
369
|
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
361
370
|
combined.set(item.id, item);
|
|
362
371
|
}
|
|
@@ -426,7 +435,7 @@ class HoloSphere {
|
|
|
426
435
|
const output = new Map();
|
|
427
436
|
let isResolved = false;
|
|
428
437
|
let listener = null;
|
|
429
|
-
|
|
438
|
+
|
|
430
439
|
const hardTimeout = setTimeout(() => {
|
|
431
440
|
cleanup();
|
|
432
441
|
resolve(Array.from(output.values()));
|
|
@@ -505,7 +514,7 @@ class HoloSphere {
|
|
|
505
514
|
}
|
|
506
515
|
|
|
507
516
|
const federatedData = new Map();
|
|
508
|
-
|
|
517
|
+
|
|
509
518
|
// Get data from each federated space
|
|
510
519
|
const fedPromises = fedInfo.federation.map(spaceId =>
|
|
511
520
|
new Promise((resolve) => {
|
|
@@ -632,7 +641,7 @@ class HoloSphere {
|
|
|
632
641
|
.get(key)
|
|
633
642
|
.once(async (data) => {
|
|
634
643
|
clearTimeout(timeout);
|
|
635
|
-
|
|
644
|
+
|
|
636
645
|
if (!data) {
|
|
637
646
|
resolve(null);
|
|
638
647
|
return;
|
|
@@ -658,7 +667,7 @@ class HoloSphere {
|
|
|
658
667
|
// 2. User is the owner
|
|
659
668
|
// 3. User is in shared list
|
|
660
669
|
// 4. Data is from federation
|
|
661
|
-
if (parsed.owner &&
|
|
670
|
+
if (parsed.owner &&
|
|
662
671
|
this.currentSpace?.alias !== parsed.owner &&
|
|
663
672
|
(!parsed.shared || !parsed.shared.includes(this.currentSpace?.alias)) &&
|
|
664
673
|
(!parsed.federation || !parsed.federation.origin)) {
|
|
@@ -711,7 +720,7 @@ class HoloSphere {
|
|
|
711
720
|
if (!data) {
|
|
712
721
|
return true; // Nothing to delete
|
|
713
722
|
}
|
|
714
|
-
|
|
723
|
+
|
|
715
724
|
if (data.owner && data.owner !== this.currentSpace.alias) {
|
|
716
725
|
throw new Error('Unauthorized to delete this data');
|
|
717
726
|
}
|
|
@@ -780,7 +789,7 @@ class HoloSphere {
|
|
|
780
789
|
.catch(error => {
|
|
781
790
|
console.error('Error in deleteAll:', error);
|
|
782
791
|
resolve(false);
|
|
783
|
-
|
|
792
|
+
});
|
|
784
793
|
});
|
|
785
794
|
});
|
|
786
795
|
}
|
|
@@ -985,12 +994,12 @@ class HoloSphere {
|
|
|
985
994
|
}
|
|
986
995
|
|
|
987
996
|
const keys = Object.keys(data).filter(key => key !== '_');
|
|
988
|
-
const promises = keys.map(key =>
|
|
997
|
+
const promises = keys.map(key =>
|
|
989
998
|
new Promise(async (resolveItem) => {
|
|
990
999
|
const itemData = await new Promise(resolveData => {
|
|
991
1000
|
this.gun.get(this.appname).get(tableName).get(key).once(resolveData);
|
|
992
1001
|
});
|
|
993
|
-
|
|
1002
|
+
|
|
994
1003
|
if (itemData) {
|
|
995
1004
|
try {
|
|
996
1005
|
const parsed = await this.parse(itemData);
|
|
@@ -1088,7 +1097,7 @@ class HoloSphere {
|
|
|
1088
1097
|
}
|
|
1089
1098
|
|
|
1090
1099
|
const keys = Object.keys(data).filter(key => key !== '_');
|
|
1091
|
-
const promises = keys.map(key =>
|
|
1100
|
+
const promises = keys.map(key =>
|
|
1092
1101
|
new Promise((resolveDelete) => {
|
|
1093
1102
|
this.gun.get(this.appname)
|
|
1094
1103
|
.get(tableName)
|
|
@@ -1120,21 +1129,62 @@ class HoloSphere {
|
|
|
1120
1129
|
|
|
1121
1130
|
// ================================ COMPUTE FUNCTIONS ================================
|
|
1122
1131
|
/**
|
|
1123
|
-
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Computes operations across multiple layers up the hierarchy
|
|
1135
|
+
* @param {string} holon - Starting holon identifier
|
|
1136
|
+
* @param {string} lens - The lens to compute
|
|
1137
|
+
* @param {object} options - Computation options
|
|
1138
|
+
* @param {number} [maxLevels=15] - Maximum levels to compute up
|
|
1139
|
+
*/
|
|
1140
|
+
async computeHierarchy(holon, lens, options, maxLevels = 15) {
|
|
1141
|
+
let currentHolon = holon;
|
|
1142
|
+
let currentRes = h3.getResolution(currentHolon);
|
|
1143
|
+
const results = [];
|
|
1144
|
+
|
|
1145
|
+
while (currentRes > 0 && maxLevels > 0) {
|
|
1146
|
+
try {
|
|
1147
|
+
const result = await this.compute(currentHolon, lens, options);
|
|
1148
|
+
if (result) {
|
|
1149
|
+
results.push(result);
|
|
1150
|
+
}
|
|
1151
|
+
currentHolon = h3.cellToParent(currentHolon, currentRes - 1);
|
|
1152
|
+
currentRes--;
|
|
1153
|
+
maxLevels--;
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
console.error('Error in compute hierarchy:', error);
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return results;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/* Computes operations on content within a holon and lens for one layer up.
|
|
1124
1164
|
* @param {string} holon - The holon identifier.
|
|
1125
1165
|
* @param {string} lens - The lens to compute.
|
|
1126
|
-
* @param {
|
|
1127
|
-
* @param {
|
|
1128
|
-
* @param {
|
|
1166
|
+
* @param {object} options - Computation options
|
|
1167
|
+
* @param {string} options.operation - The operation to perform ('summarize', 'aggregate', 'concatenate')
|
|
1168
|
+
* @param {string[]} [options.fields] - Fields to perform operation on
|
|
1169
|
+
* @param {string} [options.targetField] - Field to store the result in
|
|
1129
1170
|
* @throws {Error} If parameters are invalid or missing
|
|
1130
1171
|
*/
|
|
1131
|
-
async compute(holon, lens,
|
|
1172
|
+
async compute(holon, lens, options) {
|
|
1132
1173
|
// Validate required parameters
|
|
1133
|
-
if (!holon || !lens
|
|
1174
|
+
if (!holon || !lens) {
|
|
1175
|
+
throw new Error('compute: Missing required parameters');
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Convert string operation to options object
|
|
1179
|
+
if (typeof options === 'string') {
|
|
1180
|
+
options = { operation: options };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (!options?.operation) {
|
|
1134
1184
|
throw new Error('compute: Missing required parameters');
|
|
1135
1185
|
}
|
|
1136
1186
|
|
|
1137
|
-
// Validate holon format and resolution
|
|
1187
|
+
// Validate holon format and resolution first
|
|
1138
1188
|
let res;
|
|
1139
1189
|
try {
|
|
1140
1190
|
res = h3.getResolution(holon);
|
|
@@ -1146,141 +1196,108 @@ class HoloSphere {
|
|
|
1146
1196
|
throw new Error('compute: Invalid holon resolution (must be between 1 and 15)');
|
|
1147
1197
|
}
|
|
1148
1198
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1199
|
+
const {
|
|
1200
|
+
operation,
|
|
1201
|
+
fields = [],
|
|
1202
|
+
targetField,
|
|
1203
|
+
depth,
|
|
1204
|
+
maxDepth
|
|
1205
|
+
} = options;
|
|
1206
|
+
|
|
1207
|
+
// Validate depth parameters if provided
|
|
1208
|
+
if (depth !== undefined && depth < 0) {
|
|
1151
1209
|
throw new Error('compute: Invalid depth parameter');
|
|
1152
1210
|
}
|
|
1153
1211
|
|
|
1154
|
-
if (
|
|
1212
|
+
if (maxDepth !== undefined && (maxDepth < 1 || maxDepth > 15)) {
|
|
1155
1213
|
throw new Error('compute: Invalid maxDepth parameter (must be between 1 and 15)');
|
|
1156
1214
|
}
|
|
1157
1215
|
|
|
1158
|
-
if (depth >= maxDepth) {
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
1216
|
// Validate operation
|
|
1163
|
-
|
|
1164
|
-
|
|
1217
|
+
const validOperations = ['summarize', 'aggregate', 'concatenate'];
|
|
1218
|
+
if (!validOperations.includes(operation)) {
|
|
1219
|
+
throw new Error(`compute: Invalid operation (must be one of ${validOperations.join(', ')})`);
|
|
1165
1220
|
}
|
|
1166
1221
|
|
|
1167
1222
|
const parent = h3.cellToParent(holon, res - 1);
|
|
1168
1223
|
const siblings = h3.cellToChildren(parent, res);
|
|
1169
1224
|
|
|
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
|
-
})
|
|
1225
|
+
// Collect all content from siblings
|
|
1226
|
+
const contents = await Promise.all(
|
|
1227
|
+
siblings.map(sibling => this.getAll(sibling, lens))
|
|
1201
1228
|
);
|
|
1202
1229
|
|
|
1203
|
-
|
|
1230
|
+
const flatContents = contents.flat().filter(Boolean);
|
|
1204
1231
|
|
|
1205
|
-
if (
|
|
1232
|
+
if (flatContents.length > 0) {
|
|
1206
1233
|
try {
|
|
1207
|
-
|
|
1234
|
+
let computed;
|
|
1235
|
+
switch (operation) {
|
|
1236
|
+
case 'summarize':
|
|
1237
|
+
// For summarize, concatenate specified fields or use entire content
|
|
1238
|
+
const textToSummarize = fields.length > 0
|
|
1239
|
+
? flatContents.map(item => fields.map(field => item[field]).filter(Boolean).join('\n')).join('\n')
|
|
1240
|
+
: JSON.stringify(flatContents);
|
|
1241
|
+
computed = await this.summarize(textToSummarize);
|
|
1242
|
+
break;
|
|
1243
|
+
|
|
1244
|
+
case 'aggregate':
|
|
1245
|
+
// For aggregate, sum numeric fields
|
|
1246
|
+
computed = fields.reduce((acc, field) => {
|
|
1247
|
+
acc[field] = flatContents.reduce((sum, item) => {
|
|
1248
|
+
return sum + (Number(item[field]) || 0);
|
|
1249
|
+
}, 0);
|
|
1250
|
+
return acc;
|
|
1251
|
+
}, {});
|
|
1252
|
+
break;
|
|
1253
|
+
|
|
1254
|
+
case 'concatenate':
|
|
1255
|
+
// For concatenate, combine arrays or strings
|
|
1256
|
+
computed = fields.reduce((acc, field) => {
|
|
1257
|
+
acc[field] = flatContents.reduce((combined, item) => {
|
|
1258
|
+
const value = item[field];
|
|
1259
|
+
if (Array.isArray(value)) {
|
|
1260
|
+
return [...combined, ...value];
|
|
1261
|
+
} else if (value) {
|
|
1262
|
+
return [...combined, value];
|
|
1263
|
+
}
|
|
1264
|
+
return combined;
|
|
1265
|
+
}, []);
|
|
1266
|
+
// Remove duplicates if array
|
|
1267
|
+
acc[field] = Array.from(new Set(acc[field]));
|
|
1268
|
+
return acc;
|
|
1269
|
+
}, {});
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1208
1273
|
if (computed) {
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
id:
|
|
1212
|
-
content: computed,
|
|
1274
|
+
const resultId = `${parent}_${operation}`;
|
|
1275
|
+
const result = {
|
|
1276
|
+
id: resultId,
|
|
1213
1277
|
timestamp: Date.now()
|
|
1214
|
-
}
|
|
1278
|
+
};
|
|
1215
1279
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1280
|
+
// Store result in targetField if specified, otherwise at root level
|
|
1281
|
+
if (targetField) {
|
|
1282
|
+
result[targetField] = computed;
|
|
1283
|
+
} else if (typeof computed === 'object') {
|
|
1284
|
+
Object.assign(result, computed);
|
|
1285
|
+
} else {
|
|
1286
|
+
result.value = computed;
|
|
1218
1287
|
}
|
|
1288
|
+
|
|
1289
|
+
await this.put(parent, lens, result);
|
|
1290
|
+
return result;
|
|
1219
1291
|
}
|
|
1220
1292
|
} catch (error) {
|
|
1221
1293
|
console.warn('Error in compute operation:', error);
|
|
1222
|
-
|
|
1294
|
+
throw error;
|
|
1223
1295
|
}
|
|
1224
1296
|
}
|
|
1225
1297
|
|
|
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
|
-
});
|
|
1298
|
+
return null;
|
|
1281
1299
|
}
|
|
1282
1300
|
|
|
1283
|
-
|
|
1284
1301
|
/**
|
|
1285
1302
|
* Summarizes provided history text using OpenAI.
|
|
1286
1303
|
* @param {string} history - The history text to summarize.
|
|
@@ -1406,47 +1423,46 @@ class HoloSphere {
|
|
|
1406
1423
|
* @param {function} callback - The callback to execute on changes.
|
|
1407
1424
|
*/
|
|
1408
1425
|
async subscribe(holon, lens, callback) {
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
.get(holon)
|
|
1415
|
-
.get(lens);
|
|
1416
|
-
|
|
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
|
-
}
|
|
1426
|
+
const subscriptionId = this.generateSubscriptionId();
|
|
1427
|
+
this.subscriptions[subscriptionId] = {
|
|
1428
|
+
query: { holon, lens },
|
|
1429
|
+
callback,
|
|
1430
|
+
active: true
|
|
1429
1431
|
};
|
|
1430
1432
|
|
|
1431
|
-
//
|
|
1432
|
-
const chain = ref.map();
|
|
1433
|
-
chain.on(handler);
|
|
1434
|
-
|
|
1435
|
-
// Return subscription object
|
|
1433
|
+
// Add cleanup to ensure callback isn't called after unsubscribe
|
|
1436
1434
|
return {
|
|
1437
|
-
|
|
1438
|
-
if (
|
|
1439
|
-
|
|
1435
|
+
unsubscribe: () => {
|
|
1436
|
+
if (this.subscriptions[subscriptionId]) {
|
|
1437
|
+
delete this.subscriptions[subscriptionId];
|
|
1440
1438
|
}
|
|
1441
1439
|
}
|
|
1442
1440
|
};
|
|
1443
1441
|
}
|
|
1444
1442
|
|
|
1443
|
+
notifySubscribers(data) {
|
|
1444
|
+
Object.values(this.subscriptions).forEach(subscription => {
|
|
1445
|
+
if (subscription.active && this.matchesQuery(data, subscription.query)) {
|
|
1446
|
+
subscription.callback(data);
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1445
1451
|
// Add ID generation method
|
|
1446
1452
|
generateId() {
|
|
1447
1453
|
return Date.now().toString(10) + Math.random().toString(2);
|
|
1448
1454
|
}
|
|
1449
1455
|
|
|
1456
|
+
generateSubscriptionId() {
|
|
1457
|
+
return Date.now().toString(10) + Math.random().toString(2);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
matchesQuery(data, query) {
|
|
1461
|
+
return data && query &&
|
|
1462
|
+
data.holon === query.holon &&
|
|
1463
|
+
data.lens === query.lens;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1450
1466
|
/**
|
|
1451
1467
|
* Creates a new space with the given credentials
|
|
1452
1468
|
* @param {string} spacename - The space identifier/username
|
|
@@ -1467,7 +1483,7 @@ class HoloSphere {
|
|
|
1467
1483
|
try {
|
|
1468
1484
|
// Generate key pair
|
|
1469
1485
|
const pair = await Gun.SEA.pair();
|
|
1470
|
-
|
|
1486
|
+
|
|
1471
1487
|
// Create auth record with SEA
|
|
1472
1488
|
const salt = await Gun.SEA.random(64).toString('base64');
|
|
1473
1489
|
const hash = await Gun.SEA.work(password, salt);
|
|
@@ -1505,8 +1521,8 @@ class HoloSphere {
|
|
|
1505
1521
|
*/
|
|
1506
1522
|
async login(spacename, password) {
|
|
1507
1523
|
// Validate input
|
|
1508
|
-
if (!spacename || !password ||
|
|
1509
|
-
typeof spacename !== 'string' ||
|
|
1524
|
+
if (!spacename || !password ||
|
|
1525
|
+
typeof spacename !== 'string' ||
|
|
1510
1526
|
typeof password !== 'string') {
|
|
1511
1527
|
throw new Error('Invalid credentials format');
|
|
1512
1528
|
}
|
|
@@ -1738,10 +1754,10 @@ class HoloSphere {
|
|
|
1738
1754
|
|
|
1739
1755
|
// Get federation info for current space
|
|
1740
1756
|
const fedInfo = await this.getFederation(this.currentSpace?.alias);
|
|
1741
|
-
|
|
1757
|
+
|
|
1742
1758
|
// Get local data
|
|
1743
1759
|
const localData = await this.getAll(holon, lens);
|
|
1744
|
-
|
|
1760
|
+
|
|
1745
1761
|
// If no federation or not authenticated, return local data only
|
|
1746
1762
|
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
1747
1763
|
return localData;
|
|
@@ -1828,15 +1844,15 @@ class HoloSphere {
|
|
|
1828
1844
|
if (!removeDuplicates) {
|
|
1829
1845
|
return allData;
|
|
1830
1846
|
}
|
|
1831
|
-
|
|
1847
|
+
|
|
1832
1848
|
// Remove duplicates keeping the most recent version
|
|
1833
1849
|
const uniqueMap = new Map();
|
|
1834
1850
|
allData.forEach(item => {
|
|
1835
1851
|
const id = item[idField];
|
|
1836
1852
|
if (!id) return;
|
|
1837
|
-
|
|
1853
|
+
|
|
1838
1854
|
const existing = uniqueMap.get(id);
|
|
1839
|
-
if (!existing ||
|
|
1855
|
+
if (!existing ||
|
|
1840
1856
|
(item.federation?.timestamp > (existing.federation?.timestamp || 0))) {
|
|
1841
1857
|
uniqueMap.set(id, item);
|
|
1842
1858
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "holosphere",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Holonic Geospatial Communication Infrastructure",
|
|
5
5
|
"main": "holosphere.js",
|
|
6
6
|
"types": "holosphere.d.ts",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"license": "GPL-3.0-or-later",
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"ajv": "^8.12.0",
|
|
17
|
+
"dotenv": "^16.4.7",
|
|
17
18
|
"gun": "^0.2020.1240",
|
|
18
19
|
"h3-js": "^4.1.0",
|
|
19
20
|
"openai": "^4.85.1"
|
package/test/ai.test.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import HoloSphere from '../holosphere.js';
|
|
2
|
+
import * as h3 from 'h3-js';
|
|
3
|
+
import { jest } from '@jest/globals';
|
|
4
|
+
import 'dotenv/config';
|
|
5
|
+
|
|
6
|
+
// Set global timeout for all tests
|
|
7
|
+
jest.setTimeout(120000);
|
|
8
|
+
|
|
9
|
+
describe('AI Operations', () => {
|
|
10
|
+
let holoSphere;
|
|
11
|
+
const testAppName = 'test-ai-app';
|
|
12
|
+
const testHolon = h3.latLngToCell(40.7128, -74.0060, 7);
|
|
13
|
+
const testLens = 'aiTestLens';
|
|
14
|
+
const testCredentials = {
|
|
15
|
+
spacename: 'aitest@example.com',
|
|
16
|
+
password: 'AiTest123!'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
holoSphere = new HoloSphere(testAppName, false, process.env.OPENAI_API_KEY);
|
|
21
|
+
|
|
22
|
+
// Clean up any existing test space and data
|
|
23
|
+
try {
|
|
24
|
+
await holoSphere.deleteAllGlobal('federation');
|
|
25
|
+
await holoSphere.deleteGlobal('spaces', testCredentials.spacename);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.log('Cleanup error (can be ignored):', error);
|
|
28
|
+
}
|
|
29
|
+
// Create and login to test space
|
|
30
|
+
await holoSphere.createSpace(testCredentials.spacename, testCredentials.password);
|
|
31
|
+
await holoSphere.login(testCredentials.spacename, testCredentials.password);
|
|
32
|
+
// Set up base schema for compute tests
|
|
33
|
+
const baseSchema = {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
id: { type: 'string' },
|
|
37
|
+
content: { type: 'string' },
|
|
38
|
+
value: { type: 'number' },
|
|
39
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
40
|
+
timestamp: { type: 'number' }
|
|
41
|
+
},
|
|
42
|
+
required: ['id']
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
await holoSphere.setSchema(testLens, baseSchema);
|
|
46
|
+
}, 30000);
|
|
47
|
+
|
|
48
|
+
describe('Summarize Operations', () => {
|
|
49
|
+
test('should generate summary from text content', async () => {
|
|
50
|
+
const testContent = `
|
|
51
|
+
The HoloSphere project is a decentralized data management system.
|
|
52
|
+
It uses Gun.js for peer-to-peer data synchronization and SEA for encryption.
|
|
53
|
+
The system supports federation between spaces and implements schema validation.
|
|
54
|
+
Data can be organized in holons and viewed through different lenses.
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const summary = await holoSphere.summarize(testContent);
|
|
58
|
+
console.log("summary",summary);
|
|
59
|
+
expect(summary).toBeDefined();
|
|
60
|
+
expect(typeof summary).toBe('string');
|
|
61
|
+
expect(summary.length).toBeGreaterThan(0);
|
|
62
|
+
}, 15000);
|
|
63
|
+
|
|
64
|
+
test('should handle empty content gracefully', async () => {
|
|
65
|
+
const summary = await holoSphere.summarize('');
|
|
66
|
+
expect(summary).toBeDefined();
|
|
67
|
+
expect(typeof summary).toBe('string');
|
|
68
|
+
}, 10000);
|
|
69
|
+
|
|
70
|
+
test('should handle long content', async () => {
|
|
71
|
+
const longContent = Array(10).fill(
|
|
72
|
+
'This is a long paragraph of text that needs to be summarized. ' +
|
|
73
|
+
'It contains multiple sentences with various information. ' +
|
|
74
|
+
'The summary should capture the key points while remaining concise.'
|
|
75
|
+
).join('\n');
|
|
76
|
+
|
|
77
|
+
const summary = await holoSphere.summarize(longContent);
|
|
78
|
+
expect(summary).toBeDefined();
|
|
79
|
+
expect(typeof summary).toBe('string');
|
|
80
|
+
expect(summary.length).toBeLessThan(longContent.length);
|
|
81
|
+
}, 20000);
|
|
82
|
+
|
|
83
|
+
test('should fail gracefully without API key', async () => {
|
|
84
|
+
const noKeyHoloSphere = new HoloSphere(testAppName, false);
|
|
85
|
+
const result = await noKeyHoloSphere.summarize('Test content');
|
|
86
|
+
expect(result).toBe('OpenAI not initialized, please specify the API key in the constructor.');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('Compute Operations', () => {
|
|
91
|
+
beforeEach(async () => {
|
|
92
|
+
// Ensure we're logged in
|
|
93
|
+
if (!holoSphere.currentSpace) {
|
|
94
|
+
await holoSphere.login(testCredentials.spacename, testCredentials.password);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Clean up any existing test data
|
|
98
|
+
await holoSphere.deleteAll(testHolon, testLens);
|
|
99
|
+
}, 15000);
|
|
100
|
+
|
|
101
|
+
test('should compute summaries for nested holons', async () => {
|
|
102
|
+
const childHolon = h3.cellToChildren(testHolon, 8)[0];
|
|
103
|
+
const testData = {
|
|
104
|
+
id: 'test1',
|
|
105
|
+
content: 'This is test content for the child holon that should be summarized.',
|
|
106
|
+
timestamp: Date.now()
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Put data in child holon
|
|
110
|
+
await holoSphere.put(childHolon, testLens, testData);
|
|
111
|
+
|
|
112
|
+
// Compute summaries
|
|
113
|
+
const result = await holoSphere.compute(childHolon, testLens, {
|
|
114
|
+
operation: 'summarize',
|
|
115
|
+
fields: ['content'],
|
|
116
|
+
targetField: 'summary'
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result).toBeDefined();
|
|
120
|
+
expect(result.id).toMatch(/_summarize$/);
|
|
121
|
+
expect(result.summary).toBeDefined();
|
|
122
|
+
expect(typeof result.summary).toBe('string');
|
|
123
|
+
}, 60000);
|
|
124
|
+
|
|
125
|
+
test('should compute aggregations for numeric fields', async () => {
|
|
126
|
+
const childHolon = h3.cellToChildren(testHolon, 8)[0];
|
|
127
|
+
const testData = [
|
|
128
|
+
{ id: 'test1', value: 10, timestamp: Date.now() },
|
|
129
|
+
{ id: 'test2', value: 20, timestamp: Date.now() }
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
// Put test data
|
|
133
|
+
await Promise.all(testData.map(data => holoSphere.put(childHolon, testLens, data)));
|
|
134
|
+
|
|
135
|
+
// Compute aggregation
|
|
136
|
+
const result = await holoSphere.compute(childHolon, testLens, {
|
|
137
|
+
operation: 'aggregate',
|
|
138
|
+
fields: ['value']
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result).toBeDefined();
|
|
142
|
+
expect(result.id).toMatch(/_aggregate$/);
|
|
143
|
+
expect(result.value).toBe(30);
|
|
144
|
+
}, 30000);
|
|
145
|
+
|
|
146
|
+
test('should compute concatenations for array fields', async () => {
|
|
147
|
+
const childHolon = h3.cellToChildren(testHolon, 8)[0];
|
|
148
|
+
const testData = [
|
|
149
|
+
{ id: 'test1', tags: ['tag1', 'tag2'], timestamp: Date.now() },
|
|
150
|
+
{ id: 'test2', tags: ['tag2', 'tag3'], timestamp: Date.now() }
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// Put test data
|
|
154
|
+
await Promise.all(testData.map(data => holoSphere.put(childHolon, testLens, data)));
|
|
155
|
+
|
|
156
|
+
// Compute concatenation
|
|
157
|
+
const result = await holoSphere.compute(childHolon, testLens, {
|
|
158
|
+
operation: 'concatenate',
|
|
159
|
+
fields: ['tags']
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result).toBeDefined();
|
|
163
|
+
expect(result.id).toMatch(/_concatenate$/);
|
|
164
|
+
expect(result.tags).toEqual(['tag1', 'tag2', 'tag3']);
|
|
165
|
+
}, 30000);
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
test('should handle empty holons', async () => {
|
|
169
|
+
// Clean up any existing data first
|
|
170
|
+
await holoSphere.deleteAll(testHolon, testLens);
|
|
171
|
+
|
|
172
|
+
// Try to compute on empty holon
|
|
173
|
+
const result = await holoSphere.compute(testHolon, testLens, {
|
|
174
|
+
operation: 'summarize',
|
|
175
|
+
fields: ['content']
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(result).toBeNull();
|
|
179
|
+
}, 30000);
|
|
180
|
+
|
|
181
|
+
test('should compute hierarchy across multiple levels', async () => {
|
|
182
|
+
const childHolon = h3.cellToChildren(testHolon, 9)[0];
|
|
183
|
+
const testData = {
|
|
184
|
+
id: 'test-hierarchy',
|
|
185
|
+
content: 'Content for testing hierarchy computation',
|
|
186
|
+
value: 42,
|
|
187
|
+
tags: ['test', 'hierarchy'],
|
|
188
|
+
timestamp: Date.now()
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Put test data
|
|
192
|
+
await holoSphere.put(childHolon, testLens, testData);
|
|
193
|
+
|
|
194
|
+
// Compute hierarchy
|
|
195
|
+
const results = await holoSphere.computeHierarchy(childHolon, testLens, {
|
|
196
|
+
operation: 'summarize',
|
|
197
|
+
fields: ['content'],
|
|
198
|
+
targetField: 'summary'
|
|
199
|
+
}, 3);
|
|
200
|
+
|
|
201
|
+
expect(Array.isArray(results)).toBe(true);
|
|
202
|
+
expect(results.length).toBeGreaterThan(0);
|
|
203
|
+
results.forEach(result => {
|
|
204
|
+
expect(result.id).toMatch(/_summarize$/);
|
|
205
|
+
expect(result.summary).toBeDefined();
|
|
206
|
+
expect(typeof result.summary).toBe('string');
|
|
207
|
+
});
|
|
208
|
+
}, 60000);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
afterEach(async () => {
|
|
212
|
+
// Clean up test data
|
|
213
|
+
if (holoSphere.currentSpace) {
|
|
214
|
+
await holoSphere.deleteAll(testHolon, testLens);
|
|
215
|
+
}
|
|
216
|
+
}, 15000);
|
|
217
|
+
|
|
218
|
+
afterAll(async () => {
|
|
219
|
+
// Clean up test space and data
|
|
220
|
+
try {
|
|
221
|
+
await holoSphere.deleteAll(testHolon, testLens);
|
|
222
|
+
await holoSphere.deleteGlobal('spaces', testCredentials.spacename);
|
|
223
|
+
await holoSphere.deleteAllGlobal('federation');
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.log('Cleanup error (can be ignored):', error);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Clean up Gun instance
|
|
229
|
+
if (holoSphere.gun) {
|
|
230
|
+
holoSphere.gun.off();
|
|
231
|
+
}
|
|
232
|
+
}, 30000);
|
|
233
|
+
});
|
package/test/federation.test.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import HoloSphere from '../holosphere.js';
|
|
2
|
-
import * as h3 from 'h3-js';
|
|
3
2
|
import { jest } from '@jest/globals';
|
|
4
3
|
|
|
5
4
|
// Set global timeout for all tests
|
|
6
|
-
jest.setTimeout(
|
|
5
|
+
jest.setTimeout(3000);
|
|
7
6
|
|
|
8
7
|
describe('Federation Operations', () => {
|
|
9
8
|
const testAppName = 'test-app';
|
|
@@ -28,8 +27,6 @@ describe('Federation Operations', () => {
|
|
|
28
27
|
await holoSphere.deleteAllGlobal('federation');
|
|
29
28
|
await holoSphere.deleteGlobal('spaces', space1.spacename);
|
|
30
29
|
await holoSphere.deleteGlobal('spaces', space2.spacename);
|
|
31
|
-
// Wait for cleanup
|
|
32
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
33
30
|
} catch (error) {
|
|
34
31
|
// Ignore errors during cleanup
|
|
35
32
|
console.log('Cleanup error (expected):', error.message);
|
|
@@ -44,12 +41,9 @@ describe('Federation Operations', () => {
|
|
|
44
41
|
} catch (error) {
|
|
45
42
|
console.log('Space creation attempt', i + 1, 'failed:', error.message);
|
|
46
43
|
if (i === 2) throw error;
|
|
47
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
// Wait for space creation to complete
|
|
52
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
53
47
|
|
|
54
48
|
// Verify spaces were created
|
|
55
49
|
const space1Created = await holoSphere.getGlobal('spaces', space1.spacename);
|
|
@@ -58,9 +52,6 @@ describe('Federation Operations', () => {
|
|
|
58
52
|
if (!space1Created || !space2Created) {
|
|
59
53
|
throw new Error('Failed to create test spaces');
|
|
60
54
|
}
|
|
61
|
-
|
|
62
|
-
// Wait for everything to settle
|
|
63
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
64
55
|
}, 30000);
|
|
65
56
|
|
|
66
57
|
beforeEach(async () => {
|
|
@@ -91,9 +82,7 @@ describe('Federation Operations', () => {
|
|
|
91
82
|
await holoSphere.setSchema(testLens, baseSchema);
|
|
92
83
|
await strictHoloSphere.setSchema(testLens, baseSchema);
|
|
93
84
|
|
|
94
|
-
|
|
95
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
96
|
-
|
|
85
|
+
|
|
97
86
|
// Verify spaces exist before proceeding
|
|
98
87
|
const space1Exists = await holoSphere.getGlobal('spaces', space1.spacename);
|
|
99
88
|
const space2Exists = await holoSphere.getGlobal('spaces', space2.spacename);
|
|
@@ -102,7 +91,6 @@ describe('Federation Operations', () => {
|
|
|
102
91
|
if (!space1Exists) {
|
|
103
92
|
try {
|
|
104
93
|
await holoSphere.createSpace(space1.spacename, space1.password);
|
|
105
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
106
94
|
} catch (error) {
|
|
107
95
|
if (error.message !== 'Space already exists') {
|
|
108
96
|
throw error;
|
|
@@ -112,7 +100,6 @@ describe('Federation Operations', () => {
|
|
|
112
100
|
if (!space2Exists) {
|
|
113
101
|
try {
|
|
114
102
|
await holoSphere.createSpace(space2.spacename, space2.password);
|
|
115
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
116
103
|
} catch (error) {
|
|
117
104
|
if (error.message !== 'Space already exists') {
|
|
118
105
|
throw error;
|
|
@@ -120,9 +107,6 @@ describe('Federation Operations', () => {
|
|
|
120
107
|
}
|
|
121
108
|
}
|
|
122
109
|
|
|
123
|
-
// Wait for space creation to complete
|
|
124
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
125
|
-
|
|
126
110
|
// Verify spaces again
|
|
127
111
|
const space1Verified = await holoSphere.getGlobal('spaces', space1.spacename);
|
|
128
112
|
const space2Verified = await holoSphere.getGlobal('spaces', space2.spacename);
|
|
@@ -142,17 +126,12 @@ describe('Federation Operations', () => {
|
|
|
142
126
|
// Login as first space to holoSphere
|
|
143
127
|
await holoSphere.login(space1.spacename, space1.password);
|
|
144
128
|
|
|
145
|
-
// Wait for login to complete
|
|
146
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
147
129
|
}, 20000);
|
|
148
130
|
|
|
149
131
|
test('should create federation relationship between spaces', async () => {
|
|
150
132
|
// Create federation relationship
|
|
151
133
|
await holoSphere.federate(space1.spacename, space2.spacename);
|
|
152
134
|
|
|
153
|
-
// Wait for federation to be established
|
|
154
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
155
|
-
|
|
156
135
|
// Verify federation was created
|
|
157
136
|
const fedInfo = await holoSphere.getFederation(space1.spacename);
|
|
158
137
|
expect(fedInfo).toBeDefined();
|
|
@@ -163,9 +142,6 @@ describe('Federation Operations', () => {
|
|
|
163
142
|
// Create bidirectional federation
|
|
164
143
|
await holoSphere.federate(space1.spacename, space2.spacename);
|
|
165
144
|
|
|
166
|
-
// Wait for federation to be established
|
|
167
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
168
|
-
|
|
169
145
|
// Login to space1 to verify federation
|
|
170
146
|
await holoSphere.login(space1.spacename, space1.password);
|
|
171
147
|
|
|
@@ -186,9 +162,6 @@ describe('Federation Operations', () => {
|
|
|
186
162
|
// Create initial federation
|
|
187
163
|
await holoSphere.federate(space1.spacename, space2.spacename);
|
|
188
164
|
|
|
189
|
-
// Wait for federation to be established
|
|
190
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
191
|
-
|
|
192
165
|
// Verify federation exists
|
|
193
166
|
const fedInfo = await holoSphere.getFederation(space1.spacename);
|
|
194
167
|
expect(fedInfo).toBeDefined();
|
|
@@ -213,18 +186,12 @@ describe('Federation Operations', () => {
|
|
|
213
186
|
// Set up federation
|
|
214
187
|
await holoSphere.federate(space1.spacename, space2.spacename);
|
|
215
188
|
|
|
216
|
-
// Wait for federation to be established
|
|
217
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
218
|
-
|
|
219
189
|
// Login to space2 with strict instance
|
|
220
190
|
await strictHoloSphere.login(space2.spacename, space2.password);
|
|
221
191
|
|
|
222
192
|
// Put data in first space
|
|
223
193
|
await holoSphere.put(testHolon, testLens, testData);
|
|
224
194
|
|
|
225
|
-
// Wait for propagation
|
|
226
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
227
|
-
|
|
228
195
|
// Verify data was propagated to federated space
|
|
229
196
|
const federatedData = await strictHoloSphere.get(testHolon, testLens, testData.id);
|
|
230
197
|
expect(federatedData).toBeDefined();
|
|
@@ -239,9 +206,6 @@ describe('Federation Operations', () => {
|
|
|
239
206
|
// Clean up any existing test data first
|
|
240
207
|
await holoSphere.deleteAll(testHolon, testLens);
|
|
241
208
|
await strictHoloSphere.deleteAll(testHolon, testLens);
|
|
242
|
-
|
|
243
|
-
// Wait for cleanup to complete
|
|
244
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
245
209
|
|
|
246
210
|
// Set up federation using non-strict instance (already logged in as space1)
|
|
247
211
|
await holoSphere.federate(space1.spacename, space2.spacename);
|
|
@@ -249,9 +213,6 @@ describe('Federation Operations', () => {
|
|
|
249
213
|
// Login to space2 with strict instance
|
|
250
214
|
await strictHoloSphere.login(space2.spacename, space2.password);
|
|
251
215
|
|
|
252
|
-
// Wait for federation to be established
|
|
253
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
254
|
-
|
|
255
216
|
// Test data with overlapping IDs and different fields
|
|
256
217
|
const testData1 = {
|
|
257
218
|
id: 'user1',
|
|
@@ -281,18 +242,13 @@ describe('Federation Operations', () => {
|
|
|
281
242
|
// Put data using both instances and wait between puts
|
|
282
243
|
console.log('Putting test data 1:', JSON.stringify(testData1, null, 2));
|
|
283
244
|
await holoSphere.put(testHolon, testLens, testData1);
|
|
284
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
285
245
|
|
|
286
246
|
console.log('Putting test data 2:', JSON.stringify(testData2, null, 2));
|
|
287
247
|
await strictHoloSphere.put(testHolon, testLens, testData2);
|
|
288
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
289
248
|
|
|
290
249
|
console.log('Putting test data 3:', JSON.stringify(testData3, null, 2));
|
|
291
250
|
await holoSphere.put(testHolon, testLens, testData3);
|
|
292
251
|
|
|
293
|
-
// Wait longer for data propagation
|
|
294
|
-
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
295
|
-
|
|
296
252
|
// Test 1: Simple concatenation without deduplication
|
|
297
253
|
const concatenatedResults = await holoSphere.getFederated(testHolon, testLens, {
|
|
298
254
|
aggregate: false,
|
|
@@ -352,9 +308,6 @@ describe('Federation Operations', () => {
|
|
|
352
308
|
// Set up federation
|
|
353
309
|
await holoSphere.federate(space1.spacename, space2.spacename);
|
|
354
310
|
|
|
355
|
-
// Wait for federation to be established
|
|
356
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
357
|
-
|
|
358
311
|
// Verify federation exists
|
|
359
312
|
let fedInfo1 = await holoSphere.getFederation(space1.spacename);
|
|
360
313
|
expect(fedInfo1).toBeDefined();
|
|
@@ -364,9 +317,6 @@ describe('Federation Operations', () => {
|
|
|
364
317
|
// Remove federation
|
|
365
318
|
await holoSphere.unfederate(space1.spacename, space2.spacename);
|
|
366
319
|
|
|
367
|
-
// Wait for unfederation to complete
|
|
368
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
369
|
-
|
|
370
320
|
// Verify federation is removed
|
|
371
321
|
fedInfo1 = await holoSphere.getFederation(space1.spacename);
|
|
372
322
|
const fedInfo2 = await holoSphere.getFederation(space2.spacename);
|
|
@@ -391,9 +341,6 @@ describe('Federation Operations', () => {
|
|
|
391
341
|
if (strictHoloSphere.currentSpace) {
|
|
392
342
|
await strictHoloSphere.logout();
|
|
393
343
|
}
|
|
394
|
-
|
|
395
|
-
// Wait for cleanup
|
|
396
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
397
344
|
});
|
|
398
345
|
|
|
399
346
|
afterAll(async () => {
|
|
@@ -401,8 +348,6 @@ describe('Federation Operations', () => {
|
|
|
401
348
|
try {
|
|
402
349
|
await holoSphere.deleteGlobal('spaces', space1.spacename);
|
|
403
350
|
await holoSphere.deleteGlobal('spaces', space2.spacename);
|
|
404
|
-
// Wait for cleanup
|
|
405
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
406
351
|
} catch (error) {
|
|
407
352
|
console.error('Error during final cleanup:', error.message);
|
|
408
353
|
}
|
package/test/holosphere.test.js
CHANGED
|
@@ -152,8 +152,6 @@ describe('HoloSphere', () => {
|
|
|
152
152
|
// Store schema
|
|
153
153
|
await strictHoloSphere.setSchema(testLensWithIndex, schema);
|
|
154
154
|
|
|
155
|
-
// Add delay to ensure schema is stored
|
|
156
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
157
155
|
|
|
158
156
|
// Retrieve schema
|
|
159
157
|
const retrievedSchema = await strictHoloSphere.getSchema(testLensWithIndex);
|
|
@@ -198,7 +196,6 @@ describe('HoloSphere', () => {
|
|
|
198
196
|
// Clean up the strict instance
|
|
199
197
|
if (strictHoloSphere.gun) {
|
|
200
198
|
await strictHoloSphere.logout();
|
|
201
|
-
await new Promise(resolve => setTimeout(resolve, 100)); // Allow time for cleanup
|
|
202
199
|
}
|
|
203
200
|
}, 10000); // Increase timeout to 10 seconds
|
|
204
201
|
|
|
@@ -220,16 +217,12 @@ describe('HoloSphere', () => {
|
|
|
220
217
|
required: ['id', 'value']
|
|
221
218
|
};
|
|
222
219
|
expectedSchemas.push({ lens, schema });
|
|
223
|
-
// Add small delay between operations to prevent race conditions
|
|
224
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
225
220
|
promises.push(holoSphere.setSchema(lens, schema));
|
|
226
221
|
}
|
|
227
222
|
|
|
228
223
|
// Wait for all operations to complete
|
|
229
224
|
await Promise.all(promises);
|
|
230
225
|
|
|
231
|
-
// Add delay before verification to ensure data is settled
|
|
232
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
233
226
|
|
|
234
227
|
// Verify each schema was stored correctly
|
|
235
228
|
for (const { lens, schema } of expectedSchemas) {
|
|
@@ -480,9 +473,6 @@ describe('HoloSphere', () => {
|
|
|
480
473
|
await holoSphere.put(testHolon, testLens, storeData);
|
|
481
474
|
}
|
|
482
475
|
|
|
483
|
-
// Wait a bit to ensure data is settled
|
|
484
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
485
|
-
|
|
486
476
|
// Get data multiple times
|
|
487
477
|
const results = await Promise.all(
|
|
488
478
|
Array.from({ length: 5 }, () => holoSphere.getAll(testHolon, testLens))
|
|
@@ -671,27 +661,33 @@ describe('HoloSphere', () => {
|
|
|
671
661
|
test('should stop receiving data after unsubscribe', async () => {
|
|
672
662
|
const testData1 = { id: 'test1', data: 'first' };
|
|
673
663
|
const testData2 = { id: 'test2', data: 'second' };
|
|
674
|
-
let
|
|
664
|
+
let receivedData = [];
|
|
675
665
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
received
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
666
|
+
return new Promise(async (resolve, reject) => {
|
|
667
|
+
const timeout = setTimeout(() => {
|
|
668
|
+
// If we only received the first piece of data, test passes
|
|
669
|
+
if (receivedData.length === 1 && receivedData[0].id === testData1.id) {
|
|
670
|
+
resolve();
|
|
671
|
+
} else {
|
|
672
|
+
reject(new Error('Test timeout or received unexpected data'));
|
|
673
|
+
}
|
|
674
|
+
}, 5000);
|
|
675
|
+
|
|
676
|
+
const subscription = await holoSphere.subscribe(testHolon, testLens, async (data) => {
|
|
677
|
+
receivedData.push(data);
|
|
683
678
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
679
|
+
if (data.id === testData1.id) {
|
|
680
|
+
subscription.unsubscribe();
|
|
681
|
+
resolve();
|
|
682
|
+
} else if (data.id === testData2.id) {
|
|
683
|
+
clearTimeout(timeout);
|
|
684
|
+
reject(new Error('Received data after unsubscribe'));
|
|
685
|
+
}
|
|
686
|
+
});
|
|
692
687
|
|
|
693
|
-
|
|
694
|
-
|
|
688
|
+
// Put first piece of data
|
|
689
|
+
await holoSphere.put(testHolon, testLens, testData1);
|
|
690
|
+
});
|
|
695
691
|
}, 10000);
|
|
696
692
|
|
|
697
693
|
test('should handle multiple subscriptions', async () => {
|
|
@@ -846,16 +842,13 @@ describe('HoloSphere', () => {
|
|
|
846
842
|
for (let i = 0; i < numOperations; i++) {
|
|
847
843
|
const data = { id: `concurrent${i}`, value: `value${i}` };
|
|
848
844
|
expectedData.push(data);
|
|
849
|
-
// Add small delay between operations
|
|
850
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
851
845
|
promises.push(holoSphere.putGlobal(testTable, data));
|
|
852
846
|
}
|
|
853
847
|
|
|
854
848
|
// Wait for all operations to complete
|
|
855
849
|
await Promise.all(promises);
|
|
856
850
|
|
|
857
|
-
|
|
858
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
851
|
+
|
|
859
852
|
|
|
860
853
|
// Retrieve and verify data
|
|
861
854
|
const retrievedData = await holoSphere.getAllGlobal(testTable);
|
|
@@ -908,24 +901,31 @@ describe('HoloSphere', () => {
|
|
|
908
901
|
|
|
909
902
|
test('should validate holon resolution', async () => {
|
|
910
903
|
const invalidHolon = h3.latLngToCell(40.7128, -74.0060, 0); // Resolution 0
|
|
911
|
-
await expect(holoSphere.compute(invalidHolon, testLens, 'summarize'))
|
|
904
|
+
await expect(holoSphere.compute(invalidHolon, testLens, { operation: 'summarize' }))
|
|
912
905
|
.rejects.toThrow('compute: Invalid holon resolution (must be between 1 and 15)');
|
|
913
906
|
});
|
|
914
907
|
|
|
915
908
|
test('should validate depth parameters', async () => {
|
|
916
|
-
await expect(holoSphere.compute(testHolon, testLens,
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
909
|
+
await expect(holoSphere.compute(testHolon, testLens, {
|
|
910
|
+
operation: 'summarize',
|
|
911
|
+
depth: -1
|
|
912
|
+
})).rejects.toThrow('compute: Invalid depth parameter');
|
|
913
|
+
|
|
914
|
+
await expect(holoSphere.compute(testHolon, testLens, {
|
|
915
|
+
operation: 'summarize',
|
|
916
|
+
maxDepth: 0
|
|
917
|
+
})).rejects.toThrow('compute: Invalid maxDepth parameter (must be between 1 and 15)');
|
|
918
|
+
|
|
919
|
+
await expect(holoSphere.compute(testHolon, testLens, {
|
|
920
|
+
operation: 'summarize',
|
|
921
|
+
maxDepth: 16
|
|
922
|
+
})).rejects.toThrow('compute: Invalid maxDepth parameter (must be between 1 and 15)');
|
|
924
923
|
});
|
|
925
924
|
|
|
926
925
|
test('should validate operation type', async () => {
|
|
927
|
-
await expect(holoSphere.compute(testHolon, testLens,
|
|
928
|
-
|
|
926
|
+
await expect(holoSphere.compute(testHolon, testLens, {
|
|
927
|
+
operation: 'invalid-operation'
|
|
928
|
+
})).rejects.toThrow('compute: Invalid operation (must be one of summarize, aggregate, concatenate)');
|
|
929
929
|
});
|
|
930
930
|
|
|
931
931
|
afterEach(async () => {
|
|
@@ -967,8 +967,6 @@ describe('HoloSphere', () => {
|
|
|
967
967
|
for (let i = 0; i < numOperations; i++) {
|
|
968
968
|
const id = `concurrent${i}`;
|
|
969
969
|
expectedIds.add(id);
|
|
970
|
-
// Add small delay between operations to prevent race conditions
|
|
971
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
972
970
|
promises.push(holoSphere.put(testHolon, testLens, {
|
|
973
971
|
id: id,
|
|
974
972
|
data: 'test'
|
|
@@ -978,9 +976,6 @@ describe('HoloSphere', () => {
|
|
|
978
976
|
// Wait for all operations to complete
|
|
979
977
|
await Promise.all(promises);
|
|
980
978
|
|
|
981
|
-
// Add delay before verification to ensure data is settled
|
|
982
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
983
|
-
|
|
984
979
|
// Get and verify results
|
|
985
980
|
const results = await holoSphere.getAll(testHolon, testLens);
|
|
986
981
|
const resultIds = new Set(results.map(r => r.id));
|