holosphere 1.1.1 → 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 +1298 -390
- package/package.json +2 -2
- package/test/federation.test.js +418 -0
- package/test/holosphere.test.js +850 -109
- 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,19 +21,27 @@ 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
|
-
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
// Use provided Gun instance or create new one
|
|
26
|
+
this.gun = gunInstance || Gun({
|
|
27
|
+
peers: ['https://gun.holons.io/gun', 'https://59.src.eco/gun'],
|
|
24
28
|
axe: false,
|
|
25
29
|
// uuid: (content) => { // generate a unique id for each node
|
|
26
30
|
// console.log('uuid', content);
|
|
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
|
}
|
|
42
|
+
|
|
43
|
+
// Add currentSpace property to track logged in space
|
|
44
|
+
this.currentSpace = null;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
// ================================ SCHEMA FUNCTIONS ================================
|
|
@@ -44,81 +54,74 @@ class HoloSphere {
|
|
|
44
54
|
*/
|
|
45
55
|
async setSchema(lens, schema) {
|
|
46
56
|
if (!lens || !schema) {
|
|
47
|
-
|
|
48
|
-
return false;
|
|
57
|
+
throw new Error('setSchema: Missing required parameters');
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
// Basic schema validation
|
|
60
|
+
// Basic schema validation
|
|
52
61
|
if (!schema.type || typeof schema.type !== 'string') {
|
|
53
|
-
|
|
54
|
-
return false;
|
|
62
|
+
throw new Error('setSchema: Schema must have a type field');
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
if (this.strict) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
const metaSchema = {
|
|
67
|
+
type: 'object',
|
|
68
|
+
required: ['type', 'properties'],
|
|
69
|
+
properties: {
|
|
70
|
+
type: { type: 'string' },
|
|
63
71
|
properties: {
|
|
64
|
-
type:
|
|
65
|
-
|
|
72
|
+
type: 'object',
|
|
73
|
+
additionalProperties: {
|
|
66
74
|
type: 'object',
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
properties: {
|
|
71
|
-
type: { type: 'string' }
|
|
72
|
-
}
|
|
75
|
+
required: ['type'],
|
|
76
|
+
properties: {
|
|
77
|
+
type: { type: 'string' }
|
|
73
78
|
}
|
|
74
|
-
},
|
|
75
|
-
required: {
|
|
76
|
-
type: 'array',
|
|
77
|
-
items: { type: 'string' }
|
|
78
79
|
}
|
|
80
|
+
},
|
|
81
|
+
required: {
|
|
82
|
+
type: 'array',
|
|
83
|
+
items: { type: 'string' }
|
|
79
84
|
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const valid = this.validator.validate(metaSchema, schema);
|
|
83
|
-
if (!valid) {
|
|
84
|
-
console.error('setSchema: Invalid schema structure:', this.validator.errors);
|
|
85
|
-
return false;
|
|
86
85
|
}
|
|
86
|
+
};
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
88
|
+
const valid = this.validator.validate(metaSchema, schema);
|
|
89
|
+
if (!valid) {
|
|
90
|
+
throw new Error(`Invalid schema structure: ${JSON.stringify(this.validator.errors)}`);
|
|
91
|
+
}
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return false;
|
|
93
|
+
if (!schema.properties || typeof schema.properties !== 'object') {
|
|
94
|
+
throw new Error('Schema must have properties in strict mode');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
|
|
98
|
+
throw new Error('Schema must have required fields in strict mode');
|
|
101
99
|
}
|
|
102
100
|
}
|
|
103
101
|
|
|
104
|
-
return new Promise((resolve) => {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
105
103
|
try {
|
|
106
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
|
+
|
|
107
112
|
this.gun.get(this.appname)
|
|
108
113
|
.get(lens)
|
|
109
114
|
.get('schema')
|
|
110
|
-
.put(
|
|
115
|
+
.put(schemaData, ack => {
|
|
111
116
|
if (ack.err) {
|
|
112
|
-
|
|
113
|
-
resolve(false);
|
|
117
|
+
reject(new Error(ack.err));
|
|
114
118
|
} else {
|
|
115
|
-
|
|
116
|
-
resolve(true);
|
|
119
|
+
// Add small delay to ensure data is written
|
|
120
|
+
setTimeout(() => resolve(true), 50);
|
|
117
121
|
}
|
|
118
122
|
});
|
|
119
123
|
} catch (error) {
|
|
120
|
-
|
|
121
|
-
resolve(false);
|
|
124
|
+
reject(error);
|
|
122
125
|
}
|
|
123
126
|
});
|
|
124
127
|
}
|
|
@@ -130,36 +133,35 @@ class HoloSphere {
|
|
|
130
133
|
*/
|
|
131
134
|
async getSchema(lens) {
|
|
132
135
|
if (!lens) {
|
|
133
|
-
|
|
134
|
-
return null;
|
|
136
|
+
throw new Error('getSchema: Missing lens parameter');
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
return new Promise((resolve) => {
|
|
140
|
+
let timeout = setTimeout(() => {
|
|
141
|
+
console.warn('getSchema: Operation timed out');
|
|
142
|
+
resolve(null);
|
|
143
|
+
}, 5000);
|
|
144
|
+
|
|
138
145
|
this.gun.get(this.appname)
|
|
139
146
|
.get(lens)
|
|
140
147
|
.get('schema')
|
|
141
148
|
.once(data => {
|
|
149
|
+
clearTimeout(timeout);
|
|
142
150
|
if (!data) {
|
|
143
151
|
resolve(null);
|
|
144
152
|
return;
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
try {
|
|
148
|
-
//
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// If data is an object with a string value (GunDB format)
|
|
153
|
-
else if (typeof data === 'object' && data !== null) {
|
|
154
|
-
const schemaStr = Object.values(data).find(v =>
|
|
155
|
-
typeof v === 'string' && v.includes('"type":'));
|
|
156
|
-
if (schemaStr) {
|
|
157
|
-
resolve(JSON.parse(schemaStr));
|
|
158
|
-
} else {
|
|
159
|
-
resolve(null);
|
|
160
|
-
}
|
|
156
|
+
// Handle both new format and legacy format
|
|
157
|
+
if (data.schema) {
|
|
158
|
+
// New format with timestamp
|
|
159
|
+
resolve(JSON.parse(data.schema));
|
|
161
160
|
} else {
|
|
162
|
-
|
|
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);
|
|
163
165
|
}
|
|
164
166
|
} catch (error) {
|
|
165
167
|
console.error('getSchema: Error parsing schema:', error);
|
|
@@ -179,56 +181,140 @@ class HoloSphere {
|
|
|
179
181
|
* @returns {Promise<boolean>} - Returns true if successful, false if there was an error
|
|
180
182
|
*/
|
|
181
183
|
async put(holon, lens, data) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
}
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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) {
|
|
211
|
+
throw new Error('put: Missing required parameters');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!dataWithMeta.id) {
|
|
215
|
+
dataWithMeta.id = this.generateId();
|
|
190
216
|
}
|
|
191
217
|
|
|
192
|
-
//
|
|
218
|
+
// Get and validate schema first
|
|
193
219
|
const schema = await this.getSchema(lens);
|
|
194
220
|
if (schema) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
console.error('put: Schema validation error:', error);
|
|
204
|
-
return false;
|
|
221
|
+
// Deep clone data to avoid modifying the original
|
|
222
|
+
const dataToValidate = JSON.parse(JSON.stringify(dataWithMeta));
|
|
223
|
+
const valid = this.validator.validate(schema, dataToValidate);
|
|
224
|
+
|
|
225
|
+
if (!valid) {
|
|
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);
|
|
205
229
|
}
|
|
206
230
|
} else if (this.strict) {
|
|
207
|
-
|
|
208
|
-
return false;
|
|
231
|
+
throw new Error('Schema required in strict mode');
|
|
209
232
|
}
|
|
210
233
|
|
|
211
|
-
|
|
234
|
+
// Store data in current space
|
|
235
|
+
const putResult = await new Promise((resolve, reject) => {
|
|
212
236
|
try {
|
|
213
|
-
const payload = JSON.stringify(
|
|
214
|
-
|
|
237
|
+
const payload = JSON.stringify(dataWithMeta);
|
|
215
238
|
this.gun.get(this.appname)
|
|
216
239
|
.get(holon)
|
|
217
240
|
.get(lens)
|
|
218
|
-
.get(
|
|
241
|
+
.get(dataWithMeta.id)
|
|
219
242
|
.put(payload, ack => {
|
|
220
243
|
if (ack.err) {
|
|
221
|
-
|
|
222
|
-
resolve(false);
|
|
244
|
+
reject(new Error(ack.err));
|
|
223
245
|
} else {
|
|
224
246
|
resolve(true);
|
|
225
247
|
}
|
|
226
248
|
});
|
|
227
249
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
resolve(false);
|
|
250
|
+
reject(error);
|
|
230
251
|
}
|
|
231
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
|
+
}
|
|
232
318
|
}
|
|
233
319
|
|
|
234
320
|
/**
|
|
@@ -239,120 +325,282 @@ class HoloSphere {
|
|
|
239
325
|
*/
|
|
240
326
|
async getAll(holon, lens) {
|
|
241
327
|
if (!holon || !lens) {
|
|
242
|
-
|
|
243
|
-
return [];
|
|
328
|
+
throw new Error('getAll: Missing required parameters');
|
|
244
329
|
}
|
|
245
330
|
|
|
246
331
|
const schema = await this.getSchema(lens);
|
|
247
332
|
if (!schema && this.strict) {
|
|
248
|
-
|
|
249
|
-
return [];
|
|
333
|
+
throw new Error('getAll: Schema required in strict mode');
|
|
250
334
|
}
|
|
251
335
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
344
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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);
|
|
260
362
|
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
261
365
|
|
|
262
|
-
|
|
366
|
+
return Array.from(combined.values());
|
|
367
|
+
}
|
|
263
368
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
282
395
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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);
|
|
287
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);
|
|
288
454
|
}
|
|
455
|
+
} else {
|
|
456
|
+
output.set(parsed.id, parsed);
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error('Error processing data:', error);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
this.gun.get(this.appname)
|
|
464
|
+
.get(holon)
|
|
465
|
+
.get(lens)
|
|
466
|
+
.once(async (data) => {
|
|
467
|
+
if (!data) {
|
|
468
|
+
cleanup();
|
|
469
|
+
resolve([]);
|
|
470
|
+
return;
|
|
289
471
|
}
|
|
290
472
|
|
|
291
|
-
|
|
292
|
-
|
|
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([]);
|
|
293
487
|
}
|
|
294
488
|
});
|
|
295
|
-
});
|
|
296
489
|
});
|
|
297
490
|
}
|
|
298
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
|
+
|
|
299
555
|
/**
|
|
300
556
|
* Parses data from GunDB, handling various data formats and references.
|
|
301
557
|
* @param {*} data - The data to parse, could be a string, object, or GunDB reference.
|
|
302
558
|
* @returns {Promise<object>} - The parsed data.
|
|
303
559
|
*/
|
|
304
560
|
async parse(rawData) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
console.log('Invalid JSON in node value:', nodeValue);
|
|
329
|
-
parsedData = nodeValue; // return the raw data
|
|
561
|
+
if (!rawData) {
|
|
562
|
+
throw new Error('parse: No data provided');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
if (rawData.soul) {
|
|
567
|
+
const data = await this.getNodeRef(rawData.soul).once();
|
|
568
|
+
if (!data) {
|
|
569
|
+
throw new Error('Referenced data not found');
|
|
570
|
+
}
|
|
571
|
+
return JSON.parse(data);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let parsedData = {};
|
|
575
|
+
if (typeof rawData === 'object' && rawData !== null) {
|
|
576
|
+
if (rawData._ && rawData._["#"]) {
|
|
577
|
+
const pathParts = rawData._['#'].split('/');
|
|
578
|
+
if (pathParts.length < 4) {
|
|
579
|
+
throw new Error('Invalid reference format');
|
|
580
|
+
}
|
|
581
|
+
parsedData = await this.get(pathParts[1], pathParts[2], pathParts[3]);
|
|
582
|
+
if (!parsedData) {
|
|
583
|
+
throw new Error('Referenced data not found');
|
|
330
584
|
}
|
|
585
|
+
} else if (rawData._ && rawData._['>']) {
|
|
586
|
+
const nodeValue = Object.values(rawData).find(v => typeof v !== 'object' && v !== '_');
|
|
587
|
+
if (!nodeValue) {
|
|
588
|
+
throw new Error('Invalid node data');
|
|
589
|
+
}
|
|
590
|
+
parsedData = JSON.parse(nodeValue);
|
|
331
591
|
} else {
|
|
332
|
-
|
|
333
|
-
parsedData = rawData; // return the original data
|
|
592
|
+
parsedData = rawData;
|
|
334
593
|
}
|
|
335
594
|
} else {
|
|
336
|
-
// Treat it as object data
|
|
337
|
-
console.log('Parsing object data:', rawData);
|
|
338
|
-
parsedData = rawData;
|
|
339
|
-
}
|
|
340
|
-
} else {
|
|
341
|
-
// If it's not an object, try parsing it as JSON
|
|
342
|
-
try {
|
|
343
595
|
parsedData = JSON.parse(rawData);
|
|
344
|
-
//if the data has a soul, return the soul node
|
|
345
|
-
if (parsedData.soul) {
|
|
346
|
-
console.log('Parsing link:', parsedData.soul);
|
|
347
|
-
parsedData = await this.get(parsedData.soul.split('/')[1], parsedData.soul.split('/')[2], parsedData.soul.split('/')[3]);
|
|
348
|
-
}
|
|
349
|
-
} catch (e) {
|
|
350
|
-
console.log('Failed to parse, returning raw data', e);
|
|
351
|
-
parsedData = rawData; // return the raw data
|
|
352
596
|
}
|
|
353
|
-
}
|
|
354
597
|
|
|
355
|
-
|
|
598
|
+
return parsedData;
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.log("Parsing not a JSON, returning raw data", rawData);
|
|
601
|
+
return rawData;
|
|
602
|
+
//throw new Error(`Parse error: ${error.message}`);
|
|
603
|
+
}
|
|
356
604
|
}
|
|
357
605
|
|
|
358
606
|
/**
|
|
@@ -371,17 +619,18 @@ class HoloSphere {
|
|
|
371
619
|
// Get schema for validation
|
|
372
620
|
const schema = await this.getSchema(lens);
|
|
373
621
|
|
|
374
|
-
|
|
622
|
+
// First try to get from current space
|
|
623
|
+
const localResult = await new Promise((resolve) => {
|
|
375
624
|
let timeout = setTimeout(() => {
|
|
376
625
|
console.warn('get: Operation timed out');
|
|
377
626
|
resolve(null);
|
|
378
|
-
}, 5000);
|
|
627
|
+
}, 5000);
|
|
379
628
|
|
|
380
629
|
this.gun.get(this.appname)
|
|
381
630
|
.get(holon)
|
|
382
631
|
.get(lens)
|
|
383
632
|
.get(key)
|
|
384
|
-
.once((data
|
|
633
|
+
.once(async (data) => {
|
|
385
634
|
clearTimeout(timeout);
|
|
386
635
|
|
|
387
636
|
if (!data) {
|
|
@@ -390,7 +639,7 @@ class HoloSphere {
|
|
|
390
639
|
}
|
|
391
640
|
|
|
392
641
|
try {
|
|
393
|
-
const parsed = this.parse(data);
|
|
642
|
+
const parsed = await this.parse(data);
|
|
394
643
|
|
|
395
644
|
// Validate against schema if one exists
|
|
396
645
|
if (schema) {
|
|
@@ -403,6 +652,20 @@ class HoloSphere {
|
|
|
403
652
|
}
|
|
404
653
|
}
|
|
405
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
|
+
|
|
406
669
|
resolve(parsed);
|
|
407
670
|
} catch (error) {
|
|
408
671
|
console.error('Error parsing data:', error);
|
|
@@ -410,6 +673,21 @@ class HoloSphere {
|
|
|
410
673
|
}
|
|
411
674
|
});
|
|
412
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;
|
|
413
691
|
}
|
|
414
692
|
|
|
415
693
|
/**
|
|
@@ -419,14 +697,41 @@ class HoloSphere {
|
|
|
419
697
|
* @param {string} key - The specific key to delete.
|
|
420
698
|
*/
|
|
421
699
|
async delete(holon, lens, key) {
|
|
700
|
+
if (!holon || !lens || !key) {
|
|
701
|
+
throw new Error('delete: Missing required parameters');
|
|
702
|
+
}
|
|
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
|
+
|
|
422
719
|
return new Promise((resolve, reject) => {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
720
|
+
try {
|
|
721
|
+
this.gun.get(this.appname)
|
|
722
|
+
.get(holon)
|
|
723
|
+
.get(lens)
|
|
724
|
+
.get(key)
|
|
725
|
+
.put(null, ack => {
|
|
726
|
+
if (ack.err) {
|
|
727
|
+
reject(new Error(ack.err));
|
|
728
|
+
} else {
|
|
729
|
+
resolve(true);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
} catch (error) {
|
|
733
|
+
reject(error);
|
|
734
|
+
}
|
|
430
735
|
});
|
|
431
736
|
}
|
|
432
737
|
|
|
@@ -475,7 +780,7 @@ class HoloSphere {
|
|
|
475
780
|
.catch(error => {
|
|
476
781
|
console.error('Error in deleteAll:', error);
|
|
477
782
|
resolve(false);
|
|
478
|
-
|
|
783
|
+
});
|
|
479
784
|
});
|
|
480
785
|
});
|
|
481
786
|
}
|
|
@@ -487,43 +792,75 @@ class HoloSphere {
|
|
|
487
792
|
* Stores a specific gun node in a given holon and lens.
|
|
488
793
|
* @param {string} holon - The holon identifier.
|
|
489
794
|
* @param {string} lens - The lens under which to store the node.
|
|
490
|
-
* @param {object}
|
|
795
|
+
* @param {object} data - The node to store.
|
|
491
796
|
*/
|
|
492
|
-
async putNode(holon, lens,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
797
|
+
async putNode(holon, lens, data) {
|
|
798
|
+
if (!holon || !lens || !data) {
|
|
799
|
+
throw new Error('putNode: Missing required parameters');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return new Promise((resolve, reject) => {
|
|
803
|
+
try {
|
|
804
|
+
this.gun.get(this.appname)
|
|
805
|
+
.get(holon)
|
|
806
|
+
.get(lens)
|
|
807
|
+
.get('value') // Store at 'value' key
|
|
808
|
+
.put(data.value, ack => { // Store the value directly
|
|
809
|
+
if (ack.err) {
|
|
810
|
+
reject(new Error(ack.err));
|
|
811
|
+
} else {
|
|
812
|
+
resolve(true);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
} catch (error) {
|
|
816
|
+
reject(error);
|
|
817
|
+
}
|
|
502
818
|
});
|
|
503
819
|
}
|
|
504
820
|
|
|
505
821
|
/**
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
getNode(holon, lens, key) {
|
|
822
|
+
* Retrieves a specific gun node from the specified holon and lens.
|
|
823
|
+
* @param {string} holon - The holon identifier.
|
|
824
|
+
* @param {string} lens - The lens identifier.
|
|
825
|
+
* @param {string} key - The specific key to retrieve.
|
|
826
|
+
* @returns {Promise<any>} - The retrieved node or null if not found.
|
|
827
|
+
*/
|
|
828
|
+
async getNode(holon, lens, key) {
|
|
513
829
|
if (!holon || !lens || !key) {
|
|
514
|
-
|
|
515
|
-
return null;
|
|
830
|
+
throw new Error('getNode: Missing required parameters');
|
|
516
831
|
}
|
|
517
832
|
|
|
518
|
-
return
|
|
519
|
-
.get(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
833
|
+
return new Promise((resolve) => {
|
|
834
|
+
this.gun.get(this.appname)
|
|
835
|
+
.get(holon)
|
|
836
|
+
.get(lens)
|
|
837
|
+
.get(key)
|
|
838
|
+
.once((data) => {
|
|
839
|
+
if (!data) {
|
|
840
|
+
resolve(null);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
resolve(data); // Return the data directly
|
|
844
|
+
});
|
|
845
|
+
});
|
|
523
846
|
}
|
|
524
847
|
|
|
525
848
|
getNodeRef(soul) {
|
|
526
|
-
|
|
849
|
+
if (typeof soul !== 'string' || !soul) {
|
|
850
|
+
throw new Error('getNodeRef: Invalid soul parameter');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const parts = soul.split('/').filter(part => {
|
|
854
|
+
if (!part.trim() || /[<>:"/\\|?*]/.test(part)) {
|
|
855
|
+
throw new Error('getNodeRef: Invalid path segment');
|
|
856
|
+
}
|
|
857
|
+
return part.trim();
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
if (parts.length === 0) {
|
|
861
|
+
throw new Error('getNodeRef: Invalid soul format');
|
|
862
|
+
}
|
|
863
|
+
|
|
527
864
|
let ref = this.gun.get(this.appname);
|
|
528
865
|
parts.forEach(part => {
|
|
529
866
|
ref = ref.get(part);
|
|
@@ -540,19 +877,16 @@ class HoloSphere {
|
|
|
540
877
|
*/
|
|
541
878
|
async deleteNode(holon, lens, key) {
|
|
542
879
|
if (!holon || !lens || !key) {
|
|
543
|
-
|
|
544
|
-
return false;
|
|
880
|
+
throw new Error('deleteNode: Missing required parameters');
|
|
545
881
|
}
|
|
546
|
-
|
|
547
|
-
return new Promise((resolve) => {
|
|
882
|
+
return new Promise((resolve, reject) => {
|
|
548
883
|
this.gun.get(this.appname)
|
|
549
884
|
.get(holon)
|
|
550
885
|
.get(lens)
|
|
551
886
|
.get(key)
|
|
552
887
|
.put(null, ack => {
|
|
553
888
|
if (ack.err) {
|
|
554
|
-
|
|
555
|
-
resolve(false);
|
|
889
|
+
reject(new Error(ack.err));
|
|
556
890
|
} else {
|
|
557
891
|
resolve(true);
|
|
558
892
|
}
|
|
@@ -568,30 +902,31 @@ class HoloSphere {
|
|
|
568
902
|
* @returns {Promise<void>}
|
|
569
903
|
*/
|
|
570
904
|
async putGlobal(tableName, data) {
|
|
571
|
-
|
|
572
905
|
return new Promise((resolve, reject) => {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
906
|
+
try {
|
|
907
|
+
if (!tableName || !data) {
|
|
908
|
+
throw new Error('Table name and data are required');
|
|
909
|
+
}
|
|
578
910
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
911
|
+
if (data.id) {
|
|
912
|
+
this.gun.get(this.appname).get(tableName).get(data.id).put(JSON.stringify(data), ack => {
|
|
913
|
+
if (ack.err) {
|
|
914
|
+
reject(new Error(ack.err));
|
|
915
|
+
} else {
|
|
916
|
+
resolve();
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
} else {
|
|
920
|
+
this.gun.get(this.appname).get(tableName).put(JSON.stringify(data), ack => {
|
|
921
|
+
if (ack.err) {
|
|
922
|
+
reject(new Error(ack.err));
|
|
923
|
+
} else {
|
|
924
|
+
resolve();
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
} catch (error) {
|
|
929
|
+
reject(error);
|
|
595
930
|
}
|
|
596
931
|
});
|
|
597
932
|
}
|
|
@@ -626,88 +961,54 @@ class HoloSphere {
|
|
|
626
961
|
* @param {string} tableName - The table name to retrieve data from.
|
|
627
962
|
* @returns {Promise<object|null>} - The parsed data from the table or null if not found.
|
|
628
963
|
*/
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// let counter = 0
|
|
633
|
-
// this.gun.get(this.appname).get(tableName.toString()).once((data, key) => {
|
|
634
|
-
// if (data) {
|
|
635
|
-
// const maplenght = Object.keys(data).length - 1
|
|
636
|
-
// this.gun.get(this.appname).get(tableName.toString()).map().once(async (itemdata, key) => {
|
|
637
|
-
|
|
638
|
-
// counter += 1
|
|
639
|
-
// if (itemdata) {
|
|
640
|
-
// let parsed = await this.parse(itemdata)
|
|
641
|
-
// output.push(parsed);
|
|
642
|
-
// console.log('getAllGlobal: parsed: ', parsed)
|
|
643
|
-
// }
|
|
644
|
-
|
|
645
|
-
// if (counter == maplenght) {
|
|
646
|
-
// resolve(output);
|
|
647
|
-
|
|
648
|
-
// }
|
|
649
|
-
// }
|
|
650
|
-
// );
|
|
651
|
-
// } else resolve(output)
|
|
652
|
-
// })
|
|
653
|
-
// }
|
|
654
|
-
// )
|
|
655
|
-
// }
|
|
656
|
-
async getAllGlobal(lens) {
|
|
657
|
-
if ( !lens) {
|
|
658
|
-
console.error('getAll: Missing required parameters:', { lens });
|
|
659
|
-
return [];
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const schema = await this.getSchema(lens);
|
|
663
|
-
if (!schema && this.strict) {
|
|
664
|
-
console.error('getAll: Schema required in strict mode for lens:', lens);
|
|
665
|
-
return [];
|
|
964
|
+
async getAllGlobal(tableName) {
|
|
965
|
+
if (!tableName) {
|
|
966
|
+
throw new Error('getAllGlobal: Missing table name parameter');
|
|
666
967
|
}
|
|
667
968
|
|
|
668
969
|
return new Promise((resolve) => {
|
|
669
970
|
let output = [];
|
|
670
|
-
let
|
|
971
|
+
let isResolved = false;
|
|
972
|
+
let timeout = setTimeout(() => {
|
|
973
|
+
if (!isResolved) {
|
|
974
|
+
isResolved = true;
|
|
975
|
+
resolve(output);
|
|
976
|
+
}
|
|
977
|
+
}, 5000);
|
|
671
978
|
|
|
672
|
-
this.gun.get(this.appname).get(
|
|
979
|
+
this.gun.get(this.appname).get(tableName).once(async (data) => {
|
|
673
980
|
if (!data) {
|
|
674
|
-
|
|
981
|
+
clearTimeout(timeout);
|
|
982
|
+
isResolved = true;
|
|
983
|
+
resolve([]);
|
|
675
984
|
return;
|
|
676
985
|
}
|
|
677
986
|
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
await this.delete(holon, lens, key);
|
|
692
|
-
} else {
|
|
693
|
-
console.warn('Invalid data found:', key, this.validator.errors);
|
|
694
|
-
output.push(parsed);
|
|
695
|
-
}
|
|
696
|
-
} else {
|
|
697
|
-
output.push(parsed);
|
|
698
|
-
}
|
|
699
|
-
} catch (error) {
|
|
700
|
-
console.error('Error parsing data:', error);
|
|
701
|
-
if (this.strict) {
|
|
702
|
-
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);
|
|
703
1000
|
}
|
|
704
1001
|
}
|
|
705
|
-
|
|
1002
|
+
resolveItem();
|
|
1003
|
+
})
|
|
1004
|
+
);
|
|
706
1005
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1006
|
+
await Promise.all(promises);
|
|
1007
|
+
clearTimeout(timeout);
|
|
1008
|
+
if (!isResolved) {
|
|
1009
|
+
isResolved = true;
|
|
1010
|
+
resolve(output);
|
|
1011
|
+
}
|
|
711
1012
|
});
|
|
712
1013
|
});
|
|
713
1014
|
}
|
|
@@ -718,7 +1019,36 @@ class HoloSphere {
|
|
|
718
1019
|
* @returns {Promise<void>}
|
|
719
1020
|
*/
|
|
720
1021
|
async deleteGlobal(tableName, key) {
|
|
721
|
-
|
|
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
|
+
});
|
|
722
1052
|
}
|
|
723
1053
|
|
|
724
1054
|
/**
|
|
@@ -727,16 +1057,65 @@ class HoloSphere {
|
|
|
727
1057
|
* @returns {Promise<void>}
|
|
728
1058
|
*/
|
|
729
1059
|
async deleteAllGlobal(tableName) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
//
|
|
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
|
+
});
|
|
740
1119
|
}
|
|
741
1120
|
|
|
742
1121
|
// ================================ COMPUTE FUNCTIONS ================================
|
|
@@ -745,44 +1124,107 @@ class HoloSphere {
|
|
|
745
1124
|
* @param {string} holon - The holon identifier.
|
|
746
1125
|
* @param {string} lens - The lens to compute.
|
|
747
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
|
|
748
1130
|
*/
|
|
749
|
-
async compute(holon, lens, operation) {
|
|
1131
|
+
async compute(holon, lens, operation, depth = 0, maxDepth = 15) {
|
|
1132
|
+
// Validate required parameters
|
|
1133
|
+
if (!holon || !lens || !operation) {
|
|
1134
|
+
throw new Error('compute: Missing required parameters');
|
|
1135
|
+
}
|
|
750
1136
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
+
}
|
|
757
1144
|
|
|
758
|
-
|
|
759
|
-
|
|
1145
|
+
if (res < 1 || res > 15) {
|
|
1146
|
+
throw new Error('compute: Invalid holon resolution (must be between 1 and 15)');
|
|
1147
|
+
}
|
|
760
1148
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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")');
|
|
776
1165
|
}
|
|
777
1166
|
|
|
1167
|
+
const parent = h3.cellToParent(holon, res - 1);
|
|
1168
|
+
const siblings = h3.cellToChildren(parent, res);
|
|
1169
|
+
|
|
1170
|
+
const content = [];
|
|
1171
|
+
const promises = siblings.map(sibling =>
|
|
1172
|
+
new Promise((resolve) => {
|
|
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
|
+
})
|
|
1201
|
+
);
|
|
1202
|
+
|
|
778
1203
|
await Promise.all(promises);
|
|
779
|
-
console.log('Content:', content);
|
|
780
|
-
let computed = await this.summarize(content.join('\n'))
|
|
781
|
-
console.log('Computed:', computed)
|
|
782
|
-
let node = await this.gun.get(this.appname).get(parent + '_summary').put({ id: parent + '_summary', content: computed })
|
|
783
1204
|
|
|
784
|
-
|
|
785
|
-
|
|
1205
|
+
if (content.length > 0) {
|
|
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
|
+
});
|
|
1215
|
+
|
|
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
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Return successfully even if no content was found or processed
|
|
1227
|
+
return;
|
|
786
1228
|
}
|
|
787
1229
|
|
|
788
1230
|
/**
|
|
@@ -791,14 +1233,51 @@ class HoloSphere {
|
|
|
791
1233
|
* @param {string} lens - The lens to clear.
|
|
792
1234
|
*/
|
|
793
1235
|
async clearlens(holon, lens) {
|
|
794
|
-
|
|
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);
|
|
795
1248
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
+
});
|
|
802
1281
|
}
|
|
803
1282
|
|
|
804
1283
|
|
|
@@ -811,32 +1290,29 @@ class HoloSphere {
|
|
|
811
1290
|
if (!this.openai) {
|
|
812
1291
|
return 'OpenAI not initialized, please specify the API key in the constructor.'
|
|
813
1292
|
}
|
|
814
|
-
//const run = await this.openai.beta.threads.runs.retrieve(thread.id,run.id)
|
|
815
|
-
const assistant = await this.openai.beta.assistants.retrieve("asst_qhk79F8wV9BDNuwfOI80TqzC")
|
|
816
|
-
const thread = await this.openai.beta.threads.create()
|
|
817
|
-
const message = await this.openai.beta.threads.messages.create(thread.id, {
|
|
818
|
-
role: "user",
|
|
819
|
-
content: history
|
|
820
|
-
})
|
|
821
|
-
const run = await this.openai.beta.threads.runs.create(thread.id, {
|
|
822
|
-
assistant_id: assistant.id //,
|
|
823
|
-
//instructions: "What is the meaning of life?",
|
|
824
|
-
});
|
|
825
1293
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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');
|
|
835
1315
|
}
|
|
836
|
-
// Get the latest messages from the thread
|
|
837
|
-
const messages = await this.openai.beta.threads.messages.list(thread.id)
|
|
838
|
-
const summary = messages.data[0].content[0].text.value.replace(/\`\`\`json\n/, '').replace(/\`\`\`/, '').trim()
|
|
839
|
-
return summary
|
|
840
1316
|
}
|
|
841
1317
|
|
|
842
1318
|
/**
|
|
@@ -849,12 +1325,12 @@ class HoloSphere {
|
|
|
849
1325
|
async upcast(holon, lens, content) {
|
|
850
1326
|
let res = h3.getResolution(holon)
|
|
851
1327
|
if (res == 0) {
|
|
852
|
-
await this.
|
|
1328
|
+
await this.put(holon, lens, content)
|
|
853
1329
|
return content
|
|
854
1330
|
}
|
|
855
1331
|
else {
|
|
856
1332
|
console.log('Upcasting ', holon, lens, content, res)
|
|
857
|
-
await this.
|
|
1333
|
+
await this.put(holon, lens, content)
|
|
858
1334
|
let parent = h3.cellToParent(holon, res - 1)
|
|
859
1335
|
return this.upcast(parent, lens, content)
|
|
860
1336
|
}
|
|
@@ -862,7 +1338,7 @@ class HoloSphere {
|
|
|
862
1338
|
|
|
863
1339
|
|
|
864
1340
|
/**
|
|
865
|
-
* Updates the parent
|
|
1341
|
+
* Updates the parent holon with a new report.
|
|
866
1342
|
* @param {string} id - The child holon identifier.
|
|
867
1343
|
* @param {string} report - The report to update.
|
|
868
1344
|
* @returns {Promise<object>} - The updated parent information.
|
|
@@ -930,10 +1406,442 @@ class HoloSphere {
|
|
|
930
1406
|
* @param {function} callback - The callback to execute on changes.
|
|
931
1407
|
*/
|
|
932
1408
|
async subscribe(holon, lens, callback) {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1409
|
+
if (!holon || !lens || !callback) {
|
|
1410
|
+
throw new Error('subscribe: Missing required parameters');
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const ref = this.gun.get(this.appname)
|
|
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
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
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
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// Add ID generation method
|
|
1446
|
+
generateId() {
|
|
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());
|
|
937
1845
|
}
|
|
938
1846
|
}
|
|
939
1847
|
|