holosphere 1.1.5 → 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 +855 -987
- package/package.json +3 -6
- 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 +109 -955
- package/test/sea.html +33 -0
- package/test/jest.setup.js +0 -5
- package/test/spacesauth.test.js +0 -335
- /package/services/{environmentalApi.test.js → environmentalApitest.js} +0 -0
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,7 +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) {
|
|
17
|
-
console.log('HoloSphere v1.1.
|
|
18
|
+
console.log('HoloSphere v1.1.6');
|
|
18
19
|
this.appname = appname
|
|
19
20
|
this.strict = strict;
|
|
20
21
|
this.validator = new Ajv2019({
|
|
@@ -23,14 +24,17 @@ class HoloSphere {
|
|
|
23
24
|
validateSchema: true // Always validate schemas
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
}
|
|
34
38
|
|
|
35
39
|
// Initialize SEA
|
|
36
40
|
this.sea = SEA;
|
|
@@ -41,9 +45,6 @@ class HoloSphere {
|
|
|
41
45
|
});
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
// Add currentSpace property to track logged in space
|
|
45
|
-
this.currentSpace = null;
|
|
46
|
-
|
|
47
48
|
// Initialize subscriptions
|
|
48
49
|
this.subscriptions = {};
|
|
49
50
|
}
|
|
@@ -105,8 +106,7 @@ class HoloSphere {
|
|
|
105
106
|
await this.putGlobal('schemas', {
|
|
106
107
|
id: lens,
|
|
107
108
|
schema: schema,
|
|
108
|
-
timestamp: Date.now()
|
|
109
|
-
owner: this.currentSpace?.alias
|
|
109
|
+
timestamp: Date.now()
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
return true;
|
|
@@ -137,148 +137,142 @@ class HoloSphere {
|
|
|
137
137
|
* @param {string} holon - The holon identifier.
|
|
138
138
|
* @param {string} lens - The lens under which to store the content.
|
|
139
139
|
* @param {object} data - The data to store.
|
|
140
|
+
* @param {string} [password] - Optional password for private space.
|
|
140
141
|
* @returns {Promise<boolean>} - Returns true if successful, false if there was an error
|
|
141
142
|
*/
|
|
142
|
-
async put(holon, lens, data) {
|
|
143
|
-
|
|
144
|
-
if (!this.currentSpace) {
|
|
145
|
-
throw new Error('Unauthorized to modify this data');
|
|
146
|
-
}
|
|
147
|
-
this._checkSession();
|
|
148
|
-
|
|
149
|
-
// If updating existing data, check ownership
|
|
150
|
-
if (data.id) {
|
|
151
|
-
const existing = await this.get(holon, lens, data.id);
|
|
152
|
-
if (existing && existing.owner &&
|
|
153
|
-
existing.owner !== this.currentSpace.alias &&
|
|
154
|
-
!existing.federation) { // Skip ownership check for federated data
|
|
155
|
-
throw new Error('Unauthorized to modify this data');
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Add owner and federation information to data
|
|
160
|
-
const dataWithMeta = {
|
|
161
|
-
...data,
|
|
162
|
-
owner: this.currentSpace.alias,
|
|
163
|
-
federation: {
|
|
164
|
-
origin: this.currentSpace.alias,
|
|
165
|
-
timestamp: Date.now()
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
if (!holon || !lens || !dataWithMeta) {
|
|
143
|
+
async put(holon, lens, data, password = null) {
|
|
144
|
+
if (!holon || !lens || !data) {
|
|
170
145
|
throw new Error('put: Missing required parameters');
|
|
171
146
|
}
|
|
172
147
|
|
|
173
|
-
if (!
|
|
174
|
-
|
|
148
|
+
if (!data.id) {
|
|
149
|
+
data.id = this.generateId();
|
|
175
150
|
}
|
|
176
151
|
|
|
177
|
-
// Get and validate schema
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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));
|
|
182
159
|
const valid = this.validator.validate(schema, dataToValidate);
|
|
183
160
|
|
|
184
161
|
if (!valid) {
|
|
185
162
|
const errorMsg = `Schema validation failed: ${JSON.stringify(this.validator.errors)}`;
|
|
186
|
-
// Always throw on schema validation failure, regardless of strict mode
|
|
187
163
|
throw new Error(errorMsg);
|
|
188
164
|
}
|
|
189
|
-
} else if (this.strict) {
|
|
190
|
-
throw new Error('Schema required in strict mode');
|
|
191
165
|
}
|
|
192
166
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
211
|
});
|
|
211
|
-
|
|
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');
|
|
212
231
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
reject(error);
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// If successful, propagate to federated spaces
|
|
220
|
-
if (putResult) {
|
|
221
|
-
await this._propagateToFederation(holon, lens, dataWithMeta);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return putResult;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Propagates data to federated spaces
|
|
229
|
-
* @private
|
|
230
|
-
* @param {string} holon - The holon identifier
|
|
231
|
-
* @param {string} lens - The lens identifier
|
|
232
|
-
* @param {object} data - The data to propagate
|
|
233
|
-
*/
|
|
234
|
-
async _propagateToFederation(holon, lens, data) {
|
|
235
|
-
try {
|
|
236
|
-
// Get federation info for current space
|
|
237
|
-
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
238
|
-
if (!fedInfo || !fedInfo.notify || fedInfo.notify.length === 0) {
|
|
239
|
-
return; // No federation to propagate to
|
|
232
|
+
}
|
|
233
|
+
}
|
|
240
234
|
}
|
|
241
235
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
.get(lens)
|
|
249
|
-
.get(data.id)
|
|
250
|
-
.put(JSON.stringify({
|
|
251
|
-
...data,
|
|
252
|
-
federation: {
|
|
253
|
-
...data.federation,
|
|
254
|
-
notified: Date.now()
|
|
255
|
-
}
|
|
256
|
-
}), 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 => {
|
|
257
243
|
if (ack.err) {
|
|
258
|
-
|
|
244
|
+
reject(new Error(ack.err));
|
|
245
|
+
} else {
|
|
246
|
+
this.notifySubscribers({
|
|
247
|
+
holon,
|
|
248
|
+
lens,
|
|
249
|
+
...data
|
|
250
|
+
});
|
|
251
|
+
resolve(true);
|
|
259
252
|
}
|
|
260
|
-
resolve();
|
|
261
253
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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);
|
|
273
266
|
}
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
reject(error);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
279
273
|
} catch (error) {
|
|
280
|
-
console.
|
|
281
|
-
|
|
274
|
+
console.error('Error in put:', error);
|
|
275
|
+
throw error;
|
|
282
276
|
}
|
|
283
277
|
}
|
|
284
278
|
|
|
@@ -286,151 +280,160 @@ class HoloSphere {
|
|
|
286
280
|
* Retrieves content from the specified holon and lens.
|
|
287
281
|
* @param {string} holon - The holon identifier.
|
|
288
282
|
* @param {string} lens - The lens from which to retrieve content.
|
|
289
|
-
* @
|
|
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.
|
|
290
286
|
*/
|
|
291
|
-
async
|
|
292
|
-
if (!holon || !lens) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const schema = await this.getSchema(lens);
|
|
297
|
-
if (!schema && this.strict) {
|
|
298
|
-
throw new Error('getAll: Schema required in strict mode');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Get local data
|
|
302
|
-
const localData = await this._getAllLocal(holon, lens, schema);
|
|
303
|
-
|
|
304
|
-
// If authenticated, get federated data
|
|
305
|
-
let federatedData = [];
|
|
306
|
-
if (this.currentSpace) {
|
|
307
|
-
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;
|
|
308
291
|
}
|
|
309
292
|
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
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');
|
|
317
299
|
}
|
|
318
|
-
}
|
|
300
|
+
}
|
|
319
301
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
});
|
|
327
326
|
}
|
|
328
327
|
}
|
|
329
|
-
});
|
|
330
328
|
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
return new Promise((resolve) => {
|
|
330
|
+
const handleData = async (data) => {
|
|
331
|
+
if (!data) {
|
|
332
|
+
resolve(null);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
333
335
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
* @private
|
|
337
|
-
* @param {string} holon - The holon identifier
|
|
338
|
-
* @param {string} lens - The lens identifier
|
|
339
|
-
* @param {string} key - The key to get
|
|
340
|
-
* @returns {Promise<object|null>} - The federated data or null if not found
|
|
341
|
-
*/
|
|
342
|
-
async _getFederatedData(holon, lens, key) {
|
|
343
|
-
try {
|
|
344
|
-
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
345
|
-
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
336
|
+
try {
|
|
337
|
+
const parsed = await this.parse(data);
|
|
348
338
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (!data) {
|
|
358
|
-
resolve(null);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
try {
|
|
362
|
-
const parsed = await this.parse(data);
|
|
363
|
-
resolve(parsed);
|
|
364
|
-
} catch (error) {
|
|
365
|
-
console.warn(`Error parsing federated data from ${spaceId}:`, error);
|
|
366
|
-
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
|
+
}
|
|
367
347
|
}
|
|
368
|
-
}
|
|
369
|
-
});
|
|
348
|
+
}
|
|
370
349
|
|
|
371
|
-
|
|
372
|
-
|
|
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);
|
|
373
363
|
}
|
|
374
|
-
}
|
|
364
|
+
});
|
|
375
365
|
} catch (error) {
|
|
376
|
-
console.
|
|
366
|
+
console.error('Error in get:', error);
|
|
367
|
+
return null;
|
|
377
368
|
}
|
|
378
|
-
return null;
|
|
379
369
|
}
|
|
380
370
|
|
|
381
371
|
/**
|
|
382
|
-
*
|
|
383
|
-
* @private
|
|
372
|
+
* Propagates data to federated spaces
|
|
384
373
|
* @param {string} holon - The holon identifier
|
|
385
374
|
* @param {string} lens - The lens identifier
|
|
386
|
-
* @param {object}
|
|
387
|
-
* @
|
|
375
|
+
* @param {object} data - The data to propagate
|
|
376
|
+
* @param {object} [options] - Propagation options
|
|
377
|
+
* @returns {Promise<object>} - Result with success count and errors
|
|
388
378
|
*/
|
|
389
|
-
async
|
|
390
|
-
return
|
|
391
|
-
|
|
392
|
-
let isResolved = false;
|
|
393
|
-
let listener = null;
|
|
394
|
-
|
|
395
|
-
const hardTimeout = setTimeout(() => {
|
|
396
|
-
cleanup();
|
|
397
|
-
resolve(Array.from(output.values()));
|
|
398
|
-
}, 5000);
|
|
399
|
-
|
|
400
|
-
const cleanup = () => {
|
|
401
|
-
if (listener) {
|
|
402
|
-
listener.off();
|
|
403
|
-
}
|
|
404
|
-
clearTimeout(hardTimeout);
|
|
405
|
-
isResolved = true;
|
|
406
|
-
};
|
|
379
|
+
async propagateToFederation(holon, lens, data, options = {}) {
|
|
380
|
+
return Federation.propagateToFederation(this, holon, lens, data, options);
|
|
381
|
+
}
|
|
407
382
|
|
|
408
|
-
|
|
409
|
-
|
|
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
|
+
}
|
|
410
391
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
+
}
|
|
414
403
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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;
|
|
421
|
+
|
|
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 {
|
|
418
428
|
output.set(parsed.id, parsed);
|
|
419
429
|
}
|
|
420
|
-
}
|
|
421
|
-
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error('Error processing data:', error);
|
|
422
432
|
}
|
|
423
|
-
}
|
|
424
|
-
console.error('Error processing data:', error);
|
|
425
|
-
}
|
|
426
|
-
};
|
|
433
|
+
};
|
|
427
434
|
|
|
428
|
-
|
|
429
|
-
.get(holon)
|
|
430
|
-
.get(lens)
|
|
431
|
-
.once(async (data) => {
|
|
435
|
+
const handleData = async (data) => {
|
|
432
436
|
if (!data) {
|
|
433
|
-
cleanup();
|
|
434
437
|
resolve([]);
|
|
435
438
|
return;
|
|
436
439
|
}
|
|
@@ -444,75 +447,23 @@ class HoloSphere {
|
|
|
444
447
|
|
|
445
448
|
try {
|
|
446
449
|
await Promise.all(initialPromises);
|
|
447
|
-
cleanup();
|
|
448
450
|
resolve(Array.from(output.values()));
|
|
449
451
|
} catch (error) {
|
|
450
|
-
|
|
452
|
+
console.error('Error in getAll:', error);
|
|
451
453
|
resolve([]);
|
|
452
454
|
}
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Gets all data from federated spaces
|
|
459
|
-
* @private
|
|
460
|
-
* @param {string} holon - The holon identifier
|
|
461
|
-
* @param {string} lens - The lens identifier
|
|
462
|
-
* @param {object} schema - The schema to validate against
|
|
463
|
-
* @returns {Promise<Array>} - Array of federated data
|
|
464
|
-
*/
|
|
465
|
-
async _getAllFederated(holon, lens, schema) {
|
|
466
|
-
try {
|
|
467
|
-
const fedInfo = await this.getFederation(this.currentSpace.alias);
|
|
468
|
-
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
469
|
-
return [];
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const federatedData = new Map();
|
|
473
|
-
|
|
474
|
-
// Get data from each federated space
|
|
475
|
-
const fedPromises = fedInfo.federation.map(spaceId =>
|
|
476
|
-
new Promise((resolve) => {
|
|
477
|
-
this.gun.get(this.appname)
|
|
478
|
-
.get(spaceId)
|
|
479
|
-
.get(lens)
|
|
480
|
-
.once(async (data) => {
|
|
481
|
-
if (!data) {
|
|
482
|
-
resolve();
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const processPromises = Object.keys(data)
|
|
487
|
-
.filter(key => key !== '_')
|
|
488
|
-
.map(async key => {
|
|
489
|
-
try {
|
|
490
|
-
const parsed = await this.parse(data[key]);
|
|
491
|
-
if (parsed && parsed.id) {
|
|
492
|
-
if (schema) {
|
|
493
|
-
const valid = this.validator.validate(schema, parsed);
|
|
494
|
-
if (valid || !this.strict) {
|
|
495
|
-
federatedData.set(parsed.id, parsed);
|
|
496
|
-
}
|
|
497
|
-
} else {
|
|
498
|
-
federatedData.set(parsed.id, parsed);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
} catch (error) {
|
|
502
|
-
console.warn(`Error processing federated data from ${spaceId}:`, error);
|
|
503
|
-
}
|
|
504
|
-
});
|
|
455
|
+
};
|
|
505
456
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
});
|
|
514
465
|
} catch (error) {
|
|
515
|
-
console.
|
|
466
|
+
console.error('Error in getAll:', error);
|
|
516
467
|
return [];
|
|
517
468
|
}
|
|
518
469
|
}
|
|
@@ -575,186 +526,116 @@ class HoloSphere {
|
|
|
575
526
|
}
|
|
576
527
|
}
|
|
577
528
|
|
|
578
|
-
/**
|
|
579
|
-
* Retrieves a specific key from the specified holon and lens.
|
|
580
|
-
* @param {string} holon - The holon identifier.
|
|
581
|
-
* @param {string} lens - The lens from which to retrieve the key.
|
|
582
|
-
* @param {string} key - The specific key to retrieve.
|
|
583
|
-
* @returns {Promise<object|null>} - The retrieved content or null if not found.
|
|
584
|
-
*/
|
|
585
|
-
async get(holon, lens, key) {
|
|
586
|
-
if (!holon || !lens || !key) {
|
|
587
|
-
console.error('get: Missing required parameters:', { holon, lens, key });
|
|
588
|
-
return null;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Get schema for validation
|
|
592
|
-
const schema = await this.getSchema(lens);
|
|
593
|
-
|
|
594
|
-
// First try to get from current space
|
|
595
|
-
const localResult = await new Promise((resolve) => {
|
|
596
|
-
let timeout = setTimeout(() => {
|
|
597
|
-
console.warn('get: Operation timed out');
|
|
598
|
-
resolve(null);
|
|
599
|
-
}, 5000);
|
|
600
|
-
|
|
601
|
-
this.gun.get(this.appname)
|
|
602
|
-
.get(holon)
|
|
603
|
-
.get(lens)
|
|
604
|
-
.get(key)
|
|
605
|
-
.once(async (data) => {
|
|
606
|
-
clearTimeout(timeout);
|
|
607
|
-
|
|
608
|
-
if (!data) {
|
|
609
|
-
resolve(null);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
try {
|
|
614
|
-
const parsed = await this.parse(data);
|
|
615
|
-
|
|
616
|
-
// Validate against schema if one exists
|
|
617
|
-
if (schema) {
|
|
618
|
-
const valid = this.validator.validate(schema, parsed);
|
|
619
|
-
if (!valid) {
|
|
620
|
-
console.error('get: Invalid data according to schema:', this.validator.errors);
|
|
621
|
-
if (this.strict) {
|
|
622
|
-
resolve(null);
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Check if user has access - only allow if:
|
|
629
|
-
// 1. No owner (public data)
|
|
630
|
-
// 2. User is the owner
|
|
631
|
-
// 3. User is in shared list
|
|
632
|
-
// 4. Data is from federation
|
|
633
|
-
if (parsed.owner &&
|
|
634
|
-
this.currentSpace?.alias !== parsed.owner &&
|
|
635
|
-
(!parsed.shared || !parsed.shared.includes(this.currentSpace?.alias)) &&
|
|
636
|
-
(!parsed.federation || !parsed.federation.origin)) {
|
|
637
|
-
resolve(null);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
resolve(parsed);
|
|
642
|
-
} catch (error) {
|
|
643
|
-
console.error('Error parsing data:', error);
|
|
644
|
-
resolve(null);
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
// If found locally, return it
|
|
650
|
-
if (localResult) {
|
|
651
|
-
return localResult;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// If not found locally and we're authenticated, try federated spaces
|
|
655
|
-
if (this.currentSpace) {
|
|
656
|
-
const fedResult = await this._getFederatedData(holon, lens, key);
|
|
657
|
-
if (fedResult) {
|
|
658
|
-
return fedResult;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
return null;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
529
|
/**
|
|
666
530
|
* Deletes a specific key from a given holon and lens.
|
|
667
531
|
* @param {string} holon - The holon identifier.
|
|
668
532
|
* @param {string} lens - The lens from which to delete the key.
|
|
669
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
|
|
670
536
|
*/
|
|
671
|
-
async delete(holon, lens, key) {
|
|
537
|
+
async delete(holon, lens, key, password = null) {
|
|
672
538
|
if (!holon || !lens || !key) {
|
|
673
539
|
throw new Error('delete: Missing required parameters');
|
|
674
540
|
}
|
|
675
541
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (data.owner && data.owner !== this.currentSpace.alias) {
|
|
688
|
-
throw new Error('Unauthorized to delete this data');
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
return new Promise((resolve, reject) => {
|
|
692
|
-
try {
|
|
693
|
-
this.gun.get(this.appname)
|
|
694
|
-
.get(holon)
|
|
695
|
-
.get(lens)
|
|
696
|
-
.get(key)
|
|
697
|
-
.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 => {
|
|
698
551
|
if (ack.err) {
|
|
699
552
|
reject(new Error(ack.err));
|
|
700
553
|
} else {
|
|
701
554
|
resolve(true);
|
|
702
555
|
}
|
|
703
556
|
});
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
+
}
|
|
708
572
|
}
|
|
709
573
|
|
|
710
574
|
/**
|
|
711
575
|
* Deletes all keys from a given holon and lens.
|
|
712
576
|
* @param {string} holon - The holon identifier.
|
|
713
577
|
* @param {string} lens - The lens from which to delete all keys.
|
|
714
|
-
* @
|
|
578
|
+
* @param {string} [password] - Optional password for private space.
|
|
579
|
+
* @returns {Promise<boolean>} - Returns true if successful
|
|
715
580
|
*/
|
|
716
|
-
async deleteAll(holon, lens) {
|
|
581
|
+
async deleteAll(holon, lens, password = null) {
|
|
717
582
|
if (!holon || !lens) {
|
|
718
583
|
console.error('deleteAll: Missing holon or lens parameter');
|
|
719
584
|
return false;
|
|
720
585
|
}
|
|
721
586
|
|
|
722
|
-
|
|
723
|
-
|
|
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
|
+
}
|
|
724
604
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
if (!data) {
|
|
728
|
-
resolve(true); // Nothing to delete
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
605
|
+
// Get all keys except Gun's metadata key '_'
|
|
606
|
+
const keys = Object.keys(data).filter(key => key !== '_');
|
|
731
607
|
|
|
732
|
-
|
|
733
|
-
|
|
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);
|
|
734
615
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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);
|
|
742
628
|
})
|
|
743
|
-
|
|
629
|
+
.catch(error => {
|
|
630
|
+
console.error('Error in deleteAll:', error);
|
|
631
|
+
resolve(false);
|
|
632
|
+
});
|
|
744
633
|
});
|
|
745
|
-
|
|
746
|
-
// Wait for all deletions to complete
|
|
747
|
-
Promise.all(deletionPromises)
|
|
748
|
-
.then(results => {
|
|
749
|
-
const allSuccessful = results.every(result => result === true);
|
|
750
|
-
resolve(allSuccessful);
|
|
751
|
-
})
|
|
752
|
-
.catch(error => {
|
|
753
|
-
console.error('Error in deleteAll:', error);
|
|
754
|
-
resolve(false);
|
|
755
|
-
});
|
|
756
634
|
});
|
|
757
|
-
})
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error('Error in deleteAll:', error);
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
758
639
|
}
|
|
759
640
|
|
|
760
641
|
// ================================ NODE FUNCTIONS ================================
|
|
@@ -871,243 +752,418 @@ class HoloSphere {
|
|
|
871
752
|
* Stores data in a global (non-holon-specific) table.
|
|
872
753
|
* @param {string} tableName - The table name to store data in.
|
|
873
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.
|
|
874
756
|
* @returns {Promise<void>}
|
|
875
757
|
*/
|
|
876
|
-
async putGlobal(tableName, data) {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
758
|
+
async putGlobal(tableName, data, password = null) {
|
|
759
|
+
try {
|
|
760
|
+
if (!tableName || !data) {
|
|
761
|
+
throw new Error('Table name and data are required');
|
|
762
|
+
}
|
|
882
763
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
+
});
|
|
898
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
|
+
}
|
|
899
821
|
}
|
|
900
|
-
} catch (error) {
|
|
901
|
-
reject(error);
|
|
902
822
|
}
|
|
903
|
-
|
|
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
|
+
}
|
|
904
875
|
}
|
|
905
876
|
|
|
906
877
|
/**
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
}
|
|
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) {
|
|
919
889
|
try {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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);
|
|
924
950
|
}
|
|
925
951
|
});
|
|
926
|
-
})
|
|
952
|
+
} catch (error) {
|
|
953
|
+
console.error('Error in getGlobal:', error);
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
927
956
|
}
|
|
928
957
|
|
|
929
|
-
|
|
930
|
-
|
|
931
958
|
/**
|
|
932
959
|
* Retrieves all data from a global table.
|
|
933
960
|
* @param {string} tableName - The table name to retrieve data from.
|
|
934
|
-
* @
|
|
961
|
+
* @param {string} [password] - Optional password for private space.
|
|
962
|
+
* @returns {Promise<Array<object>>} - The parsed data from the table as an array.
|
|
935
963
|
*/
|
|
936
|
-
async getAllGlobal(tableName) {
|
|
964
|
+
async getAllGlobal(tableName, password = null) {
|
|
937
965
|
if (!tableName) {
|
|
938
966
|
throw new Error('getAllGlobal: Missing table name parameter');
|
|
939
967
|
}
|
|
940
968
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
let timeout = setTimeout(() => {
|
|
945
|
-
if (!isResolved) {
|
|
946
|
-
isResolved = true;
|
|
947
|
-
resolve(output);
|
|
948
|
-
}
|
|
949
|
-
}, 5000);
|
|
969
|
+
try {
|
|
970
|
+
// Get the appropriate space
|
|
971
|
+
const user = this.gun.user();
|
|
950
972
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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);
|
|
958
982
|
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
+
};
|
|
965
1021
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
resolveItem();
|
|
975
|
-
})
|
|
976
|
-
);
|
|
977
|
-
|
|
978
|
-
await Promise.all(promises);
|
|
979
|
-
clearTimeout(timeout);
|
|
980
|
-
if (!isResolved) {
|
|
981
|
-
isResolved = true;
|
|
982
|
-
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);
|
|
983
1028
|
}
|
|
984
1029
|
});
|
|
985
|
-
})
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
console.error('Error in getAllGlobal:', error);
|
|
1032
|
+
return [];
|
|
1033
|
+
}
|
|
986
1034
|
}
|
|
1035
|
+
|
|
987
1036
|
/**
|
|
988
1037
|
* Deletes a specific key from a global table.
|
|
989
1038
|
* @param {string} tableName - The table name to delete from.
|
|
990
1039
|
* @param {string} key - The key to delete.
|
|
991
|
-
* @
|
|
1040
|
+
* @param {string} [password] - Optional password for private space.
|
|
1041
|
+
* @returns {Promise<boolean>}
|
|
992
1042
|
*/
|
|
993
|
-
async deleteGlobal(tableName, key) {
|
|
1043
|
+
async deleteGlobal(tableName, key, password = null) {
|
|
994
1044
|
if (!tableName || !key) {
|
|
995
1045
|
throw new Error('deleteGlobal: Missing required parameters');
|
|
996
1046
|
}
|
|
997
1047
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Skip session check for spaces table
|
|
1004
|
-
if (tableName !== 'spaces') {
|
|
1005
|
-
this._checkSession();
|
|
1006
|
-
}
|
|
1048
|
+
try {
|
|
1049
|
+
// Get the appropriate space
|
|
1050
|
+
const user = this.gun.user();
|
|
1007
1051
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
.get(tableName)
|
|
1012
|
-
.get(key)
|
|
1013
|
-
.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 => {
|
|
1014
1056
|
if (ack.err) {
|
|
1015
1057
|
reject(new Error(ack.err));
|
|
1016
1058
|
} else {
|
|
1017
1059
|
resolve(true);
|
|
1018
1060
|
}
|
|
1019
1061
|
});
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
+
}
|
|
1024
1077
|
}
|
|
1025
1078
|
|
|
1026
1079
|
/**
|
|
1027
1080
|
* Deletes an entire global table.
|
|
1028
1081
|
* @param {string} tableName - The table name to delete.
|
|
1029
|
-
* @
|
|
1082
|
+
* @param {string} [password] - Optional password for private space.
|
|
1083
|
+
* @returns {Promise<boolean>}
|
|
1030
1084
|
*/
|
|
1031
|
-
async deleteAllGlobal(tableName) {
|
|
1085
|
+
async deleteAllGlobal(tableName, password = null) {
|
|
1032
1086
|
if (!tableName) {
|
|
1033
1087
|
throw new Error('deleteAllGlobal: Missing table name parameter');
|
|
1034
1088
|
}
|
|
1035
1089
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}
|
|
1090
|
+
try {
|
|
1091
|
+
// Get the appropriate space
|
|
1092
|
+
const user = this.gun.user();
|
|
1040
1093
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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);
|
|
1045
1102
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
let timeout = setTimeout(() => {
|
|
1050
|
-
if (deletions.size === 0) {
|
|
1051
|
-
resolve(true); // No data to delete
|
|
1052
|
-
}
|
|
1053
|
-
}, 5000);
|
|
1103
|
+
const dataPath = password ?
|
|
1104
|
+
user.get('private').get(tableName) :
|
|
1105
|
+
this.gun.get(this.appname).get(tableName);
|
|
1054
1106
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1107
|
+
dataPath.once(async (data) => {
|
|
1108
|
+
if (!data) {
|
|
1109
|
+
clearTimeout(timeout);
|
|
1110
|
+
resolve(true);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1061
1113
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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 => {
|
|
1069
1122
|
if (ack.err) {
|
|
1070
1123
|
console.error(`Failed to delete ${key}:`, ack.err);
|
|
1071
1124
|
}
|
|
1072
1125
|
resolveDelete();
|
|
1073
1126
|
});
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
+
}
|
|
1091
1148
|
}
|
|
1092
1149
|
|
|
1093
1150
|
// ================================ COMPUTE FUNCTIONS ================================
|
|
1094
1151
|
/**
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
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) {
|
|
1104
1160
|
let currentHolon = holon;
|
|
1105
1161
|
let currentRes = h3.getResolution(currentHolon);
|
|
1106
1162
|
const results = [];
|
|
1107
1163
|
|
|
1108
1164
|
while (currentRes > 0 && maxLevels > 0) {
|
|
1109
1165
|
try {
|
|
1110
|
-
const result = await this.compute(currentHolon, lens, options);
|
|
1166
|
+
const result = await this.compute(currentHolon, lens, options, password);
|
|
1111
1167
|
if (result) {
|
|
1112
1168
|
results.push(result);
|
|
1113
1169
|
}
|
|
@@ -1123,16 +1179,18 @@ class HoloSphere {
|
|
|
1123
1179
|
return results;
|
|
1124
1180
|
}
|
|
1125
1181
|
|
|
1126
|
-
|
|
1182
|
+
/**
|
|
1183
|
+
* Computes operations on content within a holon and lens for one layer up.
|
|
1127
1184
|
* @param {string} holon - The holon identifier.
|
|
1128
1185
|
* @param {string} lens - The lens to compute.
|
|
1129
1186
|
* @param {object} options - Computation options
|
|
1130
1187
|
* @param {string} options.operation - The operation to perform ('summarize', 'aggregate', 'concatenate')
|
|
1131
1188
|
* @param {string[]} [options.fields] - Fields to perform operation on
|
|
1132
1189
|
* @param {string} [options.targetField] - Field to store the result in
|
|
1190
|
+
* @param {string} [password] - Optional password for private spaces
|
|
1133
1191
|
* @throws {Error} If parameters are invalid or missing
|
|
1134
1192
|
*/
|
|
1135
|
-
async compute(holon, lens, options) {
|
|
1193
|
+
async compute(holon, lens, options, password = null) {
|
|
1136
1194
|
// Validate required parameters
|
|
1137
1195
|
if (!holon || !lens) {
|
|
1138
1196
|
throw new Error('compute: Missing required parameters');
|
|
@@ -1187,7 +1245,7 @@ class HoloSphere {
|
|
|
1187
1245
|
|
|
1188
1246
|
// Collect all content from siblings
|
|
1189
1247
|
const contents = await Promise.all(
|
|
1190
|
-
siblings.map(sibling => this.getAll(sibling, lens))
|
|
1248
|
+
siblings.map(sibling => this.getAll(sibling, lens, password))
|
|
1191
1249
|
);
|
|
1192
1250
|
|
|
1193
1251
|
const flatContents = contents.flat().filter(Boolean);
|
|
@@ -1249,7 +1307,7 @@ class HoloSphere {
|
|
|
1249
1307
|
result.value = computed;
|
|
1250
1308
|
}
|
|
1251
1309
|
|
|
1252
|
-
await this.put(parent, lens, result);
|
|
1310
|
+
await this.put(parent, lens, result, password);
|
|
1253
1311
|
return result;
|
|
1254
1312
|
}
|
|
1255
1313
|
} catch (error) {
|
|
@@ -1384,307 +1442,143 @@ class HoloSphere {
|
|
|
1384
1442
|
* @param {string} holon - The holon identifier.
|
|
1385
1443
|
* @param {string} lens - The lens to subscribe to.
|
|
1386
1444
|
* @param {function} callback - The callback to execute on changes.
|
|
1445
|
+
* @returns {Promise<object>} - Subscription object with unsubscribe method
|
|
1387
1446
|
*/
|
|
1388
1447
|
async subscribe(holon, lens, callback) {
|
|
1448
|
+
if (!holon || !lens || typeof callback !== 'function') {
|
|
1449
|
+
throw new Error('subscribe: Missing required parameters');
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1389
1452
|
const subscriptionId = this.generateId();
|
|
1390
|
-
|
|
1391
|
-
|
|
1453
|
+
|
|
1454
|
+
try {
|
|
1455
|
+
// Create the subscription
|
|
1456
|
+
const gunSubscription = this.gun.get(this.appname).get(holon).get(lens).map().on(async (data, key) => {
|
|
1392
1457
|
if (data) {
|
|
1393
1458
|
try {
|
|
1394
|
-
let parsed = await this.parse(data)
|
|
1395
|
-
callback(parsed, key)
|
|
1459
|
+
let parsed = await this.parse(data);
|
|
1460
|
+
callback(parsed, key);
|
|
1396
1461
|
} catch (error) {
|
|
1397
1462
|
console.error('Error in subscribe:', error);
|
|
1398
1463
|
}
|
|
1399
1464
|
}
|
|
1400
|
-
})
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
notifySubscribers(data) {
|
|
1411
|
-
Object.values(this.subscriptions).forEach(subscription => {
|
|
1412
|
-
if (subscription.active && this.matchesQuery(data, subscription.query)) {
|
|
1413
|
-
subscription.callback(data);
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
// Add ID generation method
|
|
1419
|
-
generateId() {
|
|
1420
|
-
return Date.now().toString(10) + Math.random().toString(2);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
matchesQuery(data, query) {
|
|
1424
|
-
return data && query &&
|
|
1425
|
-
data.holon === query.holon &&
|
|
1426
|
-
data.lens === query.lens;
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
/**
|
|
1430
|
-
* Creates a new space with the given credentials
|
|
1431
|
-
* @param {string} spacename - The space identifier/username
|
|
1432
|
-
* @param {string} password - The space password
|
|
1433
|
-
* @returns {Promise<boolean>} - True if space was created successfully
|
|
1434
|
-
*/
|
|
1435
|
-
async createSpace(spacename, password) {
|
|
1436
|
-
if (!spacename || !password) {
|
|
1437
|
-
throw new Error('Invalid credentials format');
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
// Check if space already exists
|
|
1441
|
-
const existingSpace = await this.getGlobal('spaces', spacename);
|
|
1442
|
-
if (existingSpace) {
|
|
1443
|
-
throw new Error('Space already exists');
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
try {
|
|
1447
|
-
// Generate key pair
|
|
1448
|
-
const pair = await Gun.SEA.pair();
|
|
1449
|
-
|
|
1450
|
-
// Create auth record with SEA
|
|
1451
|
-
const salt = await Gun.SEA.random(64).toString('base64');
|
|
1452
|
-
const hash = await Gun.SEA.work(password, salt);
|
|
1453
|
-
const auth = {
|
|
1454
|
-
salt: salt,
|
|
1455
|
-
hash: hash,
|
|
1456
|
-
pub: pair.pub
|
|
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
|
|
1457
1474
|
};
|
|
1458
|
-
|
|
1459
|
-
//
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
+
}
|
|
1466
1492
|
};
|
|
1467
|
-
|
|
1468
|
-
await this.putGlobal('spaces', {
|
|
1469
|
-
...space,
|
|
1470
|
-
id: spacename
|
|
1471
|
-
});
|
|
1472
|
-
|
|
1473
|
-
return true;
|
|
1474
1493
|
} catch (error) {
|
|
1475
|
-
|
|
1494
|
+
console.error('Error creating subscription:', error);
|
|
1495
|
+
throw error;
|
|
1476
1496
|
}
|
|
1477
1497
|
}
|
|
1478
1498
|
|
|
1499
|
+
|
|
1479
1500
|
/**
|
|
1480
|
-
*
|
|
1481
|
-
* @param {
|
|
1482
|
-
* @
|
|
1483
|
-
* @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
|
|
1484
1504
|
*/
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
typeof spacename !== 'string' ||
|
|
1489
|
-
typeof password !== 'string') {
|
|
1490
|
-
throw new Error('Invalid credentials format');
|
|
1505
|
+
notifySubscribers(data) {
|
|
1506
|
+
if (!data || !data.holon || !data.lens) {
|
|
1507
|
+
return;
|
|
1491
1508
|
}
|
|
1492
|
-
|
|
1509
|
+
|
|
1493
1510
|
try {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
this.currentSpace = {
|
|
1508
|
-
...space,
|
|
1509
|
-
exp: Date.now() + (24 * 60 * 60 * 1000) // 24 hour expiration
|
|
1510
|
-
};
|
|
1511
|
-
|
|
1512
|
-
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
|
+
});
|
|
1513
1524
|
} catch (error) {
|
|
1514
|
-
|
|
1525
|
+
console.warn('Error notifying subscribers:', error);
|
|
1515
1526
|
}
|
|
1516
1527
|
}
|
|
1517
1528
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
*/
|
|
1522
|
-
async logout() {
|
|
1523
|
-
this.currentSpace = null;
|
|
1529
|
+
// Add ID generation method
|
|
1530
|
+
generateId() {
|
|
1531
|
+
return Date.now().toString(10) + Math.random().toString(2);
|
|
1524
1532
|
}
|
|
1525
1533
|
|
|
1526
|
-
|
|
1527
|
-
* Checks if the current session is valid
|
|
1528
|
-
* @private
|
|
1529
|
-
*/
|
|
1530
|
-
_checkSession() {
|
|
1531
|
-
if (!this.currentSpace) {
|
|
1532
|
-
throw new Error('No active session');
|
|
1533
|
-
}
|
|
1534
|
-
if (this.currentSpace.exp < Date.now()) {
|
|
1535
|
-
this.currentSpace = null;
|
|
1536
|
-
throw new Error('Session expired');
|
|
1537
|
-
}
|
|
1538
|
-
return true;
|
|
1539
|
-
}
|
|
1534
|
+
// ================================ FEDERATION FUNCTIONS ================================
|
|
1540
1535
|
|
|
1541
1536
|
/**
|
|
1542
1537
|
* Creates a federation relationship between two spaces
|
|
1543
1538
|
* @param {string} spaceId1 - The first space ID
|
|
1544
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
|
|
1545
1542
|
* @returns {Promise<boolean>} - True if federation was created successfully
|
|
1546
1543
|
*/
|
|
1547
|
-
async federate(spaceId1, spaceId2) {
|
|
1548
|
-
|
|
1549
|
-
throw new Error('federate: Missing required parameters');
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// Get existing federation info for both spaces
|
|
1553
|
-
let fedInfo1 = await this.getGlobal('federation', spaceId1);
|
|
1554
|
-
let fedInfo2 = await this.getGlobal('federation', spaceId2);
|
|
1555
|
-
|
|
1556
|
-
// Check if federation already exists
|
|
1557
|
-
if (fedInfo1 && fedInfo1.federation && fedInfo1.federation.includes(spaceId2)) {
|
|
1558
|
-
throw new Error('Federation already exists');
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
// Create or update federation info for first space
|
|
1562
|
-
if (!fedInfo1) {
|
|
1563
|
-
fedInfo1 = {
|
|
1564
|
-
id: spaceId1,
|
|
1565
|
-
name: spaceId1,
|
|
1566
|
-
federation: [],
|
|
1567
|
-
notify: []
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
if (!fedInfo1.federation) fedInfo1.federation = [];
|
|
1571
|
-
fedInfo1.federation.push(spaceId2);
|
|
1572
|
-
|
|
1573
|
-
// Create or update federation info for second space
|
|
1574
|
-
if (!fedInfo2) {
|
|
1575
|
-
fedInfo2 = {
|
|
1576
|
-
id: spaceId2,
|
|
1577
|
-
name: spaceId2,
|
|
1578
|
-
federation: [],
|
|
1579
|
-
notify: []
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
if (!fedInfo2.notify) fedInfo2.notify = [];
|
|
1583
|
-
fedInfo2.notify.push(spaceId1);
|
|
1584
|
-
|
|
1585
|
-
// Save both federation records
|
|
1586
|
-
await this.putGlobal('federation', fedInfo1);
|
|
1587
|
-
await this.putGlobal('federation', fedInfo2);
|
|
1588
|
-
|
|
1589
|
-
return true;
|
|
1544
|
+
async federate(spaceId1, spaceId2, password1, password2 = null) {
|
|
1545
|
+
return Federation.federate(this, spaceId1, spaceId2, password1, password2);
|
|
1590
1546
|
}
|
|
1591
1547
|
|
|
1592
1548
|
/**
|
|
1593
1549
|
* Subscribes to federation notifications for a space
|
|
1594
1550
|
* @param {string} spaceId - The space ID to subscribe to
|
|
1551
|
+
* @param {string} password - Password for the space
|
|
1595
1552
|
* @param {function} callback - The callback to execute on notifications
|
|
1596
|
-
* @
|
|
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
|
|
1597
1557
|
*/
|
|
1598
|
-
async subscribeFederation(spaceId, callback) {
|
|
1599
|
-
|
|
1600
|
-
throw new Error('subscribeFederation: Missing required parameters');
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
// Get federation info
|
|
1604
|
-
const fedInfo = await this.getGlobal('federation', spaceId);
|
|
1605
|
-
if (!fedInfo) {
|
|
1606
|
-
throw new Error('No federation info found for space');
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
// Create subscription for each federated space
|
|
1610
|
-
const subscriptions = [];
|
|
1611
|
-
if (fedInfo.federation && fedInfo.federation.length > 0) {
|
|
1612
|
-
for (const federatedSpace of fedInfo.federation) {
|
|
1613
|
-
// Subscribe to all lenses in the federated space
|
|
1614
|
-
const sub = await this.subscribe(federatedSpace, '*', async (data) => {
|
|
1615
|
-
try {
|
|
1616
|
-
// Only notify if the data has federation info and is from the federated space
|
|
1617
|
-
if (data && data.federation && data.federation.origin === federatedSpace) {
|
|
1618
|
-
await callback(data);
|
|
1619
|
-
}
|
|
1620
|
-
} catch (error) {
|
|
1621
|
-
console.warn('Federation notification error:', error);
|
|
1622
|
-
}
|
|
1623
|
-
});
|
|
1624
|
-
subscriptions.push(sub);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
// Return combined subscription object
|
|
1629
|
-
return {
|
|
1630
|
-
off: () => {
|
|
1631
|
-
subscriptions.forEach(sub => {
|
|
1632
|
-
if (sub && typeof sub.off === 'function') {
|
|
1633
|
-
sub.off();
|
|
1634
|
-
}
|
|
1635
|
-
});
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1558
|
+
async subscribeFederation(spaceId, password, callback, options = {}) {
|
|
1559
|
+
return Federation.subscribeFederation(this, spaceId, password, callback, options);
|
|
1638
1560
|
}
|
|
1639
1561
|
|
|
1640
1562
|
/**
|
|
1641
1563
|
* Gets federation info for a space
|
|
1642
1564
|
* @param {string} spaceId - The space ID
|
|
1565
|
+
* @param {string} [password] - Optional password for the space
|
|
1643
1566
|
* @returns {Promise<object|null>} - Federation info or null if not found
|
|
1644
1567
|
*/
|
|
1645
|
-
async getFederation(spaceId) {
|
|
1646
|
-
|
|
1647
|
-
throw new Error('getFederationInfo: Missing space ID');
|
|
1648
|
-
}
|
|
1649
|
-
return await this.getGlobal('federation', spaceId);
|
|
1568
|
+
async getFederation(spaceId, password = null) {
|
|
1569
|
+
return Federation.getFederation(this, spaceId, password);
|
|
1650
1570
|
}
|
|
1651
1571
|
|
|
1652
1572
|
/**
|
|
1653
1573
|
* Removes a federation relationship between spaces
|
|
1654
1574
|
* @param {string} spaceId1 - The first space ID
|
|
1655
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
|
|
1656
1578
|
* @returns {Promise<boolean>} - True if federation was removed successfully
|
|
1657
1579
|
*/
|
|
1658
|
-
async unfederate(spaceId1, spaceId2) {
|
|
1659
|
-
|
|
1660
|
-
throw new Error('unfederate: Missing required parameters');
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
// Get federation info for both spaces
|
|
1664
|
-
const fedInfo1 = await this.getGlobal('federation', spaceId1);
|
|
1665
|
-
const fedInfo2 = await this.getGlobal('federation', spaceId2);
|
|
1666
|
-
|
|
1667
|
-
if (fedInfo1) {
|
|
1668
|
-
fedInfo1.federation = fedInfo1.federation.filter(id => id !== spaceId2);
|
|
1669
|
-
await this.putGlobal('federation', fedInfo1);
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
if (fedInfo2) {
|
|
1673
|
-
fedInfo2.notify = fedInfo2.notify.filter(id => id !== spaceId1);
|
|
1674
|
-
await this.putGlobal('federation', fedInfo2);
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
return true;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
/**
|
|
1681
|
-
* Gets the name of a chat/space
|
|
1682
|
-
* @param {string} spaceId - The space ID
|
|
1683
|
-
* @returns {Promise<string>} - The space name or the ID if not found
|
|
1684
|
-
*/
|
|
1685
|
-
async getChatName(spaceId) {
|
|
1686
|
-
const spaceInfo = await this.getGlobal('spaces', spaceId);
|
|
1687
|
-
return spaceInfo?.name || spaceId;
|
|
1580
|
+
async unfederate(spaceId1, spaceId2, password1, password2 = null) {
|
|
1581
|
+
return Federation.unfederate(this, spaceId1, spaceId2, password1, password2);
|
|
1688
1582
|
}
|
|
1689
1583
|
|
|
1690
1584
|
/**
|
|
@@ -1692,135 +1586,109 @@ class HoloSphere {
|
|
|
1692
1586
|
* @param {string} holon - The holon identifier
|
|
1693
1587
|
* @param {string} lens - The lens identifier
|
|
1694
1588
|
* @param {object} options - Options for data retrieval and aggregation
|
|
1695
|
-
* @param {
|
|
1696
|
-
* @param {string} options.idField - Field to use as identifier for aggregation (default: 'id')
|
|
1697
|
-
* @param {string[]} options.sumFields - Numeric fields to sum during aggregation (e.g., ['received', 'sent'])
|
|
1698
|
-
* @param {string[]} options.concatArrays - Array fields to concatenate during aggregation (e.g., ['wants', 'offers'])
|
|
1699
|
-
* @param {boolean} options.removeDuplicates - Whether to remove duplicates when not aggregating (default: true)
|
|
1700
|
-
* @param {function} options.mergeStrategy - Custom function to merge items during aggregation
|
|
1589
|
+
* @param {string} [password] - Optional password for accessing private data
|
|
1701
1590
|
* @returns {Promise<Array>} - Combined array of local and federated data
|
|
1702
1591
|
*/
|
|
1703
|
-
async getFederated(holon, lens, options = {}) {
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
throw new Error('getFederated: Missing required parameters');
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
const {
|
|
1710
|
-
aggregate = false,
|
|
1711
|
-
idField = 'id',
|
|
1712
|
-
sumFields = [],
|
|
1713
|
-
concatArrays = [],
|
|
1714
|
-
removeDuplicates = true,
|
|
1715
|
-
mergeStrategy = null
|
|
1716
|
-
} = options;
|
|
1717
|
-
|
|
1718
|
-
// Get federation info for current space
|
|
1719
|
-
const fedInfo = await this.getFederation(this.currentSpace?.alias);
|
|
1720
|
-
|
|
1721
|
-
// Get local data
|
|
1722
|
-
const localData = await this.getAll(holon, lens);
|
|
1723
|
-
|
|
1724
|
-
// If no federation or not authenticated, return local data only
|
|
1725
|
-
if (!fedInfo || !fedInfo.federation || fedInfo.federation.length === 0) {
|
|
1726
|
-
return localData;
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
// Get data from each federated space
|
|
1730
|
-
const federatedData = await Promise.all(
|
|
1731
|
-
fedInfo.federation.map(async (federatedSpace) => {
|
|
1732
|
-
try {
|
|
1733
|
-
const data = await this.getAll(federatedSpace, lens);
|
|
1734
|
-
return data || [];
|
|
1735
|
-
} catch (error) {
|
|
1736
|
-
console.warn(`Error getting data from federated space ${federatedSpace}:`, error);
|
|
1737
|
-
return [];
|
|
1738
|
-
}
|
|
1739
|
-
})
|
|
1740
|
-
);
|
|
1741
|
-
|
|
1742
|
-
// Combine all data
|
|
1743
|
-
const allData = [...localData, ...federatedData.flat()];
|
|
1744
|
-
|
|
1745
|
-
// If aggregating, use enhanced aggregation logic
|
|
1746
|
-
if (aggregate) {
|
|
1747
|
-
const aggregated = new Map();
|
|
1748
|
-
|
|
1749
|
-
for (const item of allData) {
|
|
1750
|
-
const itemId = item[idField];
|
|
1751
|
-
if (!itemId) continue;
|
|
1752
|
-
|
|
1753
|
-
const existing = aggregated.get(itemId);
|
|
1754
|
-
if (!existing) {
|
|
1755
|
-
aggregated.set(itemId, { ...item });
|
|
1756
|
-
} else {
|
|
1757
|
-
// If custom merge strategy is provided, use it
|
|
1758
|
-
if (mergeStrategy && typeof mergeStrategy === 'function') {
|
|
1759
|
-
aggregated.set(itemId, mergeStrategy(existing, item));
|
|
1760
|
-
continue;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
// Enhanced default merge strategy
|
|
1764
|
-
const merged = { ...existing };
|
|
1592
|
+
async getFederated(holon, lens, options = {}, password = null) {
|
|
1593
|
+
return Federation.getFederated(this, holon, lens, options, password);
|
|
1594
|
+
}
|
|
1765
1595
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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;
|
|
1770
1617
|
}
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
console.warn(`Error cleaning up subscription ${id}:`, error);
|
|
1771
1620
|
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Clear subscriptions
|
|
1624
|
+
this.subscriptions = {};
|
|
1772
1625
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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
|
+
}
|
|
1782
1649
|
}
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
console.warn('Error accessing Gun mesh:', error);
|
|
1783
1652
|
}
|
|
1653
|
+
}
|
|
1784
1654
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
item.federation?.timestamp || 0
|
|
1791
|
-
),
|
|
1792
|
-
origins: Array.from(new Set([
|
|
1793
|
-
...(merged.federation?.origins || [merged.federation?.origin]),
|
|
1794
|
-
...(item.federation?.origins || [item.federation?.origin])
|
|
1795
|
-
]).filter(Boolean))
|
|
1796
|
-
};
|
|
1797
|
-
|
|
1798
|
-
// Update the aggregated item
|
|
1799
|
-
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);
|
|
1800
1660
|
}
|
|
1661
|
+
|
|
1662
|
+
// Wait a moment for cleanup to complete
|
|
1663
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1801
1664
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
// If not aggregating, optionally remove duplicates based on idField
|
|
1807
|
-
if (!removeDuplicates) {
|
|
1808
|
-
return allData;
|
|
1665
|
+
|
|
1666
|
+
console.log('HoloSphere instance closed successfully');
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
console.error('Error closing HoloSphere instance:', error);
|
|
1809
1669
|
}
|
|
1670
|
+
}
|
|
1810
1671
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
+
}
|
|
1816
1682
|
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
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}`;
|
|
1824
1692
|
}
|
|
1825
1693
|
}
|
|
1826
1694
|
|