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