holosphere 1.0.7 → 1.1.0
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/README.md +272 -147
- package/babel.config.json +12 -0
- package/holosphere.d.ts +41 -15
- package/holosphere.js +554 -291
- package/package.json +13 -2
- package/test/holosphere.test.js +299 -17
package/holosphere.js
CHANGED
|
@@ -7,22 +7,25 @@ class HoloSphere {
|
|
|
7
7
|
/**
|
|
8
8
|
* Initializes a new instance of the HoloSphere class.
|
|
9
9
|
* @param {string} appname - The name of the application.
|
|
10
|
+
* @param {boolean} strict - Whether to enforce strict schema validation.
|
|
10
11
|
* @param {string|null} openaikey - The OpenAI API key.
|
|
11
12
|
*/
|
|
12
|
-
constructor(appname, openaikey = null) {
|
|
13
|
-
this.
|
|
13
|
+
constructor(appname, strict = false, openaikey = null) {
|
|
14
|
+
this.appname = appname
|
|
15
|
+
this.strict = strict;
|
|
16
|
+
this.validator = new Ajv2019({
|
|
17
|
+
allErrors: true,
|
|
18
|
+
strict: false, // Keep this false to avoid Ajv strict mode issues
|
|
19
|
+
validateSchema: true // Always validate schemas
|
|
20
|
+
});
|
|
14
21
|
this.gun = Gun({
|
|
15
|
-
peers: ['https://59.src.eco/gun'],
|
|
22
|
+
peers: ['http://gun.holons.io', 'https://59.src.eco/gun'],
|
|
16
23
|
axe: false,
|
|
17
24
|
// uuid: (content) => { // generate a unique id for each node
|
|
18
25
|
// console.log('uuid', content);
|
|
19
26
|
// return content;}
|
|
20
27
|
});
|
|
21
28
|
|
|
22
|
-
this.gun = this.gun.get(appname)
|
|
23
|
-
this.users = {}; // Initialize users
|
|
24
|
-
this.hexagonVotes = {}; // Initialize hexagonVotes
|
|
25
|
-
|
|
26
29
|
if (openaikey != null) {
|
|
27
30
|
this.openai = new OpenAI({
|
|
28
31
|
apiKey: openaikey,
|
|
@@ -30,6 +33,8 @@ class HoloSphere {
|
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
// ================================ SCHEMA FUNCTIONS ================================
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Sets the JSON schema for a specific lens.
|
|
35
40
|
* @param {string} lens - The lens identifier.
|
|
@@ -37,16 +42,84 @@ class HoloSphere {
|
|
|
37
42
|
* @returns {Promise} - Resolves when the schema is set.
|
|
38
43
|
*/
|
|
39
44
|
async setSchema(lens, schema) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
if (!lens || !schema) {
|
|
46
|
+
console.error('setSchema: Missing required parameters');
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Basic schema validation - check for required fields
|
|
51
|
+
if (!schema.type || typeof schema.type !== 'string') {
|
|
52
|
+
console.error('setSchema: Schema must have a type field');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (this.strict) {
|
|
57
|
+
try {
|
|
58
|
+
// Validate schema against JSON Schema meta-schema
|
|
59
|
+
const metaSchema = {
|
|
60
|
+
type: 'object',
|
|
61
|
+
required: ['type', 'properties'],
|
|
62
|
+
properties: {
|
|
63
|
+
type: { type: 'string' },
|
|
64
|
+
properties: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
additionalProperties: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
required: ['type'],
|
|
69
|
+
properties: {
|
|
70
|
+
type: { type: 'string' }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
required: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
items: { type: 'string' }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const valid = this.validator.validate(metaSchema, schema);
|
|
82
|
+
if (!valid) {
|
|
83
|
+
console.error('setSchema: Invalid schema structure:', this.validator.errors);
|
|
84
|
+
return false;
|
|
47
85
|
}
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
|
|
87
|
+
// Additional strict mode checks
|
|
88
|
+
if (!schema.properties || typeof schema.properties !== 'object') {
|
|
89
|
+
console.error('setSchema: Schema must have properties in strict mode');
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!schema.required || !Array.isArray(schema.required) || schema.required.length === 0) {
|
|
94
|
+
console.error('setSchema: Schema must have required fields in strict mode');
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('setSchema: Schema validation error:', error);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
try {
|
|
105
|
+
const schemaString = JSON.stringify(schema);
|
|
106
|
+
this.gun.get(this.appname)
|
|
107
|
+
.get(lens)
|
|
108
|
+
.get('schema')
|
|
109
|
+
.put(schemaString, ack => {
|
|
110
|
+
if (ack.err) {
|
|
111
|
+
console.error('Failed to add schema:', ack.err);
|
|
112
|
+
resolve(false);
|
|
113
|
+
} else {
|
|
114
|
+
console.log('Schema added successfully for lens:', lens);
|
|
115
|
+
resolve(true);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('setSchema: Error stringifying schema:', error);
|
|
120
|
+
resolve(false);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
50
123
|
}
|
|
51
124
|
|
|
52
125
|
/**
|
|
@@ -55,232 +128,505 @@ class HoloSphere {
|
|
|
55
128
|
* @returns {Promise<object|null>} - The retrieved schema or null if not found.
|
|
56
129
|
*/
|
|
57
130
|
async getSchema(lens) {
|
|
131
|
+
if (!lens) {
|
|
132
|
+
console.error('getSchema: Missing lens parameter');
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
58
136
|
return new Promise((resolve) => {
|
|
59
|
-
this.gun.get(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
137
|
+
this.gun.get(this.appname)
|
|
138
|
+
.get(lens)
|
|
139
|
+
.get('schema')
|
|
140
|
+
.once(data => {
|
|
141
|
+
if (!data) {
|
|
142
|
+
resolve(null);
|
|
143
|
+
return;
|
|
64
144
|
}
|
|
65
|
-
|
|
66
|
-
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// If data is already a string, parse it
|
|
148
|
+
if (typeof data === 'string') {
|
|
149
|
+
resolve(JSON.parse(data));
|
|
150
|
+
}
|
|
151
|
+
// If data is an object with a string value (GunDB format)
|
|
152
|
+
else if (typeof data === 'object' && data !== null) {
|
|
153
|
+
const schemaStr = Object.values(data).find(v =>
|
|
154
|
+
typeof v === 'string' && v.includes('"type":'));
|
|
155
|
+
if (schemaStr) {
|
|
156
|
+
resolve(JSON.parse(schemaStr));
|
|
157
|
+
} else {
|
|
158
|
+
resolve(null);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
resolve(null);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('getSchema: Error parsing schema:', error);
|
|
165
|
+
resolve(null);
|
|
67
166
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
resolve(null);
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Deletes a specific tag from a given ID.
|
|
77
|
-
* @param {string} id - The identifier from which to delete the tag.
|
|
78
|
-
* @param {string} tag - The tag to delete.
|
|
79
|
-
*/
|
|
80
|
-
async delete(id, tag) {
|
|
81
|
-
await this.gun.get(id).get(tag).put(null)
|
|
167
|
+
});
|
|
168
|
+
});
|
|
82
169
|
}
|
|
83
170
|
|
|
171
|
+
// ================================ CONTENT FUNCTIONS ================================
|
|
172
|
+
|
|
84
173
|
/**
|
|
85
|
-
* Stores content in the specified
|
|
86
|
-
* @param {string}
|
|
174
|
+
* Stores content in the specified holon and lens.
|
|
175
|
+
* @param {string} holon - The holon identifier.
|
|
87
176
|
* @param {string} lens - The lens under which to store the content.
|
|
88
|
-
* @param {object}
|
|
177
|
+
* @param {object} data - The data to store.
|
|
178
|
+
* @returns {Promise<boolean>} - Returns true if successful, false if there was an error
|
|
89
179
|
*/
|
|
90
|
-
async put(
|
|
91
|
-
if (!
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
let schema = await this.getSchema(lens)
|
|
95
|
-
if (schema) {
|
|
96
|
-
// Validate the content against the schema
|
|
97
|
-
const valid = this.validator.validate(schema, content);
|
|
98
|
-
if (!valid) {
|
|
99
|
-
console.error('Not committing invalid content:', this.validator.errors);
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
180
|
+
async put(holon, lens, data) {
|
|
181
|
+
if (!holon || !lens || !data) {
|
|
182
|
+
console.error('put: Missing required parameters:', { holon, lens, data });
|
|
183
|
+
return false;
|
|
102
184
|
}
|
|
103
185
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
let noderef;
|
|
108
|
-
|
|
109
|
-
if (content.id) { //use the user-defined id. Important to be able to send updates using put
|
|
110
|
-
noderef = this.gun.get(lens).get(content.id).put(payload)
|
|
111
|
-
this.gun.get(hex.toString()).get(lens).get(content.id).put(payload)
|
|
112
|
-
} else { // create a content-addressable reference like IPFS. Note: no updates possible using put
|
|
113
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
|
|
114
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
115
|
-
const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, "0")).join("");
|
|
116
|
-
noderef = this.gun.get(lens).get(hashHex).put(payload)
|
|
117
|
-
this.gun.get(hex.toString()).get(lens).get(hashHex).put(payload)
|
|
186
|
+
if (!data.id) {
|
|
187
|
+
console.error('put: Data must have an id field');
|
|
188
|
+
return false;
|
|
118
189
|
}
|
|
119
190
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.gun.get(hex).get(lens).set(node)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async parse(data) {
|
|
127
|
-
let parsed = {};
|
|
128
|
-
|
|
129
|
-
if (typeof data === 'object' && data !== null) {
|
|
130
|
-
if (data._ && data._["#"]) {
|
|
131
|
-
// If the data is a reference, fetch the actual content
|
|
132
|
-
let query = data._['#'].split('/');
|
|
133
|
-
let hex = query[1];
|
|
134
|
-
let lens = query[2];
|
|
135
|
-
let key = query[3];
|
|
136
|
-
parsed = await this.getKey(hex, lens, key);
|
|
137
|
-
} else if (data._ && data._['>']) {
|
|
138
|
-
// This might be a GunDB node, try to get its value
|
|
139
|
-
const nodeValue = Object.values(data).find(v => typeof v !== 'object' && v !== '_');
|
|
140
|
-
if (nodeValue) {
|
|
141
|
-
try {
|
|
142
|
-
parsed = JSON.parse(nodeValue);
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.log('Invalid JSON in node value:', nodeValue);
|
|
145
|
-
parsed = nodeValue; // return the raw data
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
console.log('Unable to parse GunDB node:', data);
|
|
149
|
-
parsed = data; // return the original data
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
// Treat it as regular data
|
|
153
|
-
parsed = data;
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
// If it's not an object, try parsing it as JSON
|
|
191
|
+
// Strict validation of schema and data
|
|
192
|
+
const schema = await this.getSchema(lens);
|
|
193
|
+
if (schema) {
|
|
157
194
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
195
|
+
const valid = this.validator.validate(schema, data);
|
|
196
|
+
if (!valid) {
|
|
197
|
+
const errors = this.validator.errors;
|
|
198
|
+
console.error('put: Schema validation failed:', errors);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error('put: Schema validation error:', error);
|
|
203
|
+
return false;
|
|
162
204
|
}
|
|
205
|
+
} else if (this.strict) {
|
|
206
|
+
console.error('put: Schema required in strict mode for lens:', lens);
|
|
207
|
+
return false;
|
|
163
208
|
}
|
|
164
209
|
|
|
165
|
-
return
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
try {
|
|
212
|
+
const payload = JSON.stringify(data);
|
|
213
|
+
|
|
214
|
+
this.gun.get(this.appname)
|
|
215
|
+
.get(holon)
|
|
216
|
+
.get(lens)
|
|
217
|
+
.get(data.id)
|
|
218
|
+
.put(payload, ack => {
|
|
219
|
+
if (ack.err) {
|
|
220
|
+
console.error("Error adding data to GunDB:", ack.err);
|
|
221
|
+
resolve(false);
|
|
222
|
+
} else {
|
|
223
|
+
resolve(true);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Error in put operation:', error);
|
|
228
|
+
resolve(false);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
166
231
|
}
|
|
167
232
|
|
|
168
233
|
/**
|
|
169
|
-
* Retrieves content from the specified
|
|
170
|
-
* @param {string}
|
|
234
|
+
* Retrieves content from the specified holon and lens.
|
|
235
|
+
* @param {string} holon - The holon identifier.
|
|
171
236
|
* @param {string} lens - The lens from which to retrieve content.
|
|
172
237
|
* @returns {Promise<Array<object>>} - The retrieved content.
|
|
173
238
|
*/
|
|
174
|
-
async
|
|
175
|
-
if (!
|
|
176
|
-
console.
|
|
177
|
-
return;
|
|
239
|
+
async getAll(holon, lens) {
|
|
240
|
+
if (!holon || !lens) {
|
|
241
|
+
console.error('getAll: Missing required parameters:', { holon, lens });
|
|
242
|
+
return [];
|
|
178
243
|
}
|
|
179
|
-
// Wrap the GunDB operation in a promise
|
|
180
|
-
//retrieve lens schema
|
|
181
|
-
const schema = await this.getSchema(lens);
|
|
182
244
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
245
|
+
const schema = await this.getSchema(lens);
|
|
246
|
+
if (!schema && this.strict) {
|
|
247
|
+
console.error('getAll: Schema required in strict mode for lens:', lens);
|
|
248
|
+
return [];
|
|
186
249
|
}
|
|
187
250
|
|
|
188
|
-
return new Promise(
|
|
189
|
-
let output = []
|
|
190
|
-
let counter = 0
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (itemdata) {
|
|
198
|
-
let parsed = await this.parse (itemdata)
|
|
199
|
-
|
|
251
|
+
return new Promise((resolve) => {
|
|
252
|
+
let output = [];
|
|
253
|
+
let counter = 0;
|
|
254
|
+
|
|
255
|
+
this.gun.get(this.appname).get(holon).get(lens).once((data, key) => {
|
|
256
|
+
if (!data) {
|
|
257
|
+
resolve(output);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
200
260
|
|
|
261
|
+
const mapLength = Object.keys(data).length - 1;
|
|
262
|
+
|
|
263
|
+
this.gun.get(this.appname).get(holon).get(lens).map().once(async (itemdata, key) => {
|
|
264
|
+
counter += 1;
|
|
265
|
+
if (itemdata) {
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(itemdata);
|
|
268
|
+
|
|
201
269
|
if (schema) {
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
270
|
+
const valid = this.validator.validate(schema, parsed);
|
|
271
|
+
if (valid) {
|
|
272
|
+
output.push(parsed);
|
|
273
|
+
} else if (this.strict) {
|
|
274
|
+
console.warn('Invalid data removed:', key, this.validator.errors);
|
|
275
|
+
await this.delete(holon, lens, key);
|
|
207
276
|
} else {
|
|
277
|
+
console.warn('Invalid data found:', key, this.validator.errors);
|
|
208
278
|
output.push(parsed);
|
|
209
279
|
}
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
280
|
+
} else {
|
|
212
281
|
output.push(parsed);
|
|
213
282
|
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('Error parsing data:', error);
|
|
285
|
+
if (this.strict) {
|
|
286
|
+
await this.delete(holon, lens, key);
|
|
287
|
+
}
|
|
214
288
|
}
|
|
289
|
+
}
|
|
215
290
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
291
|
+
if (counter === mapLength) {
|
|
292
|
+
resolve(output);
|
|
219
293
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
225
297
|
}
|
|
226
298
|
|
|
227
|
-
|
|
228
|
-
* Retrieves a specific key from the specified
|
|
229
|
-
* @param {string}
|
|
299
|
+
/**
|
|
300
|
+
* Retrieves a specific key from the specified holon and lens.
|
|
301
|
+
* @param {string} holon - The holon identifier.
|
|
230
302
|
* @param {string} lens - The lens from which to retrieve the key.
|
|
231
303
|
* @param {string} key - The specific key to retrieve.
|
|
232
304
|
* @returns {Promise<object|null>} - The retrieved content or null if not found.
|
|
233
305
|
*/
|
|
234
|
-
|
|
306
|
+
async get(holon, lens, key) {
|
|
307
|
+
if (!holon || !lens || !key) {
|
|
308
|
+
console.error('get: Missing required parameters:', { holon, lens, key });
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Get schema for validation
|
|
313
|
+
const schema = await this.getSchema(lens);
|
|
314
|
+
|
|
235
315
|
return new Promise((resolve) => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
316
|
+
let timeout = setTimeout(() => {
|
|
317
|
+
console.warn('get: Operation timed out');
|
|
318
|
+
resolve(null);
|
|
319
|
+
}, 5000); // 5 second timeout
|
|
320
|
+
|
|
321
|
+
this.gun.get(this.appname)
|
|
322
|
+
.get(holon)
|
|
323
|
+
.get(lens)
|
|
324
|
+
.get(key)
|
|
325
|
+
.once((data) => {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
|
|
328
|
+
if (!data) {
|
|
329
|
+
resolve(null);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
240
333
|
try {
|
|
241
|
-
|
|
334
|
+
const parsed = JSON.parse(data);
|
|
335
|
+
|
|
336
|
+
// Validate against schema if one exists
|
|
337
|
+
if (schema) {
|
|
338
|
+
const valid = this.validator.validate(schema, parsed);
|
|
339
|
+
if (!valid) {
|
|
340
|
+
console.error('get: Invalid data according to schema:', this.validator.errors);
|
|
341
|
+
if (this.strict) {
|
|
342
|
+
resolve(null);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
242
348
|
resolve(parsed);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error('Error parsing data:', error);
|
|
351
|
+
resolve(null);
|
|
243
352
|
}
|
|
244
|
-
|
|
245
|
-
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Deletes a specific key from a given holon and lens.
|
|
359
|
+
* @param {string} holon - The holon identifier.
|
|
360
|
+
* @param {string} lens - The lens from which to delete the key.
|
|
361
|
+
* @param {string} key - The specific key to delete.
|
|
362
|
+
*/
|
|
363
|
+
async delete (holon, lens, key) {
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
this.gun.get(this.appname).get(holon).get(lens).get(key).put(null, ack => {
|
|
366
|
+
if (ack.err) {
|
|
367
|
+
resolve(ack.err);
|
|
368
|
+
} else {
|
|
369
|
+
resolve(ack.ok);
|
|
246
370
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Deletes all keys from a given holon and lens.
|
|
377
|
+
* @param {string} holon - The holon identifier.
|
|
378
|
+
* @param {string} lens - The lens from which to delete all keys.
|
|
379
|
+
* @returns {Promise<boolean>} - Returns true if successful, false if there was an error
|
|
380
|
+
*/
|
|
381
|
+
async deleteAll(holon, lens) {
|
|
382
|
+
if (!holon || !lens) {
|
|
383
|
+
console.error('deleteAll: Missing holon or lens parameter');
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return new Promise((resolve) => {
|
|
388
|
+
let deletionPromises = [];
|
|
389
|
+
|
|
390
|
+
// First get all the data to find keys to delete
|
|
391
|
+
this.gun.get(this.appname).get(holon).get(lens).once((data) => {
|
|
392
|
+
if (!data) {
|
|
393
|
+
resolve(true); // Nothing to delete
|
|
394
|
+
return;
|
|
250
395
|
}
|
|
396
|
+
|
|
397
|
+
// Get all keys except Gun's metadata key '_'
|
|
398
|
+
const keys = Object.keys(data).filter(key => key !== '_');
|
|
399
|
+
|
|
400
|
+
// Create deletion promises for each key
|
|
401
|
+
keys.forEach(key => {
|
|
402
|
+
deletionPromises.push(
|
|
403
|
+
new Promise((resolveDelete) => {
|
|
404
|
+
this.gun.get(this.appname).get(holon).get(lens).get(key).put(null, ack => {
|
|
405
|
+
resolveDelete(!!ack.ok); // Convert to boolean
|
|
406
|
+
});
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Wait for all deletions to complete
|
|
412
|
+
Promise.all(deletionPromises)
|
|
413
|
+
.then(results => {
|
|
414
|
+
const allSuccessful = results.every(result => result === true);
|
|
415
|
+
resolve(allSuccessful);
|
|
416
|
+
})
|
|
417
|
+
.catch(error => {
|
|
418
|
+
console.error('Error in deleteAll:', error);
|
|
419
|
+
resolve(false);
|
|
420
|
+
});
|
|
251
421
|
});
|
|
252
422
|
});
|
|
253
|
-
|
|
254
423
|
}
|
|
255
424
|
|
|
425
|
+
// ================================ NODE FUNCTIONS ================================
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Stores a specific gun node in a given holon and lens.
|
|
430
|
+
* @param {string} holon - The holon identifier.
|
|
431
|
+
* @param {string} lens - The lens under which to store the node.
|
|
432
|
+
* @param {object} node - The node to store.
|
|
433
|
+
*/
|
|
434
|
+
async putNode(holon, lens, node) {
|
|
435
|
+
this.gun.get(this.appname).get(holon).get(lens).put(node)
|
|
436
|
+
}
|
|
437
|
+
|
|
256
438
|
/**
|
|
257
|
-
* Retrieves a specific
|
|
258
|
-
* @param {string}
|
|
259
|
-
* @param {string} lens - The lens
|
|
439
|
+
* Retrieves a specific gun node from the specified holon and lens.
|
|
440
|
+
* @param {string} holon - The holon identifier.
|
|
441
|
+
* @param {string} lens - The lens identifier.
|
|
260
442
|
* @param {string} key - The specific key to retrieve.
|
|
261
|
-
* @returns {Promise<object|null>} - The retrieved
|
|
443
|
+
* @returns {Promise<object|null>} - The retrieved node or null if not found.
|
|
262
444
|
*/
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
445
|
+
async getNode(holon, lens, key) {
|
|
446
|
+
if (!holon || !lens || !key) {
|
|
447
|
+
console.error('getNode: Missing required parameters');
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return new Promise((resolve) => {
|
|
452
|
+
let timeout = setTimeout(() => {
|
|
453
|
+
console.warn('getNode: Operation timed out');
|
|
454
|
+
resolve(null);
|
|
455
|
+
}, 5000);
|
|
456
|
+
|
|
457
|
+
this.gun.get(this.appname)
|
|
458
|
+
.get(holon)
|
|
459
|
+
.get(lens)
|
|
460
|
+
.get(key)
|
|
461
|
+
.once((data) => {
|
|
462
|
+
clearTimeout(timeout);
|
|
463
|
+
resolve(data || null);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Deletes a specific gun node from a given holon and lens.
|
|
470
|
+
* @param {string} holon - The holon identifier.
|
|
471
|
+
* @param {string} lens - The lens identifier.
|
|
472
|
+
* @param {string} key - The key of the node to delete.
|
|
473
|
+
* @returns {Promise<boolean>} - Returns true if successful
|
|
474
|
+
*/
|
|
475
|
+
async deleteNode(holon, lens, key) {
|
|
476
|
+
if (!holon || !lens || !key) {
|
|
477
|
+
console.error('deleteNode: Missing required parameters');
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return new Promise((resolve) => {
|
|
482
|
+
this.gun.get(this.appname)
|
|
483
|
+
.get(holon)
|
|
484
|
+
.get(lens)
|
|
485
|
+
.get(key)
|
|
486
|
+
.put(null, ack => {
|
|
487
|
+
if (ack.err) {
|
|
488
|
+
console.error('deleteNode: Error deleting node:', ack.err);
|
|
489
|
+
resolve(false);
|
|
490
|
+
} else {
|
|
491
|
+
resolve(true);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
266
495
|
}
|
|
267
496
|
|
|
497
|
+
// ================================ GLOBAL FUNCTIONS ================================
|
|
498
|
+
/**
|
|
499
|
+
* Stores data in a global (non-holon-specific) table.
|
|
500
|
+
* @param {string} tableName - The table name to store data in.
|
|
501
|
+
* @param {object} data - The data to store. If it has an 'id' field, it will be used as the key.
|
|
502
|
+
* @returns {Promise<void>}
|
|
503
|
+
*/
|
|
504
|
+
async putGlobal(tableName, data) {
|
|
505
|
+
|
|
506
|
+
return new Promise((resolve, reject) => {
|
|
507
|
+
if (!tableName || !data) {
|
|
508
|
+
reject(new Error('Table name and data are required'));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
if (data.id) {
|
|
514
|
+
this.gun.get(this.appname).get(tableName).get(data.id).put(JSON.stringify(data), ack => {
|
|
515
|
+
if (ack.err) {
|
|
516
|
+
reject(new Error(ack.err));
|
|
517
|
+
} else {
|
|
518
|
+
resolve();
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
} else {
|
|
522
|
+
this.gun.get(this.appname).get(tableName).put(JSON.stringify(data), ack => {
|
|
523
|
+
if (ack.err) {
|
|
524
|
+
reject(new Error(ack.err));
|
|
525
|
+
} else {
|
|
526
|
+
resolve();
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Retrieves a specific key from a global table.
|
|
535
|
+
* @param {string} tableName - The table name to retrieve from.
|
|
536
|
+
* @param {string} key - The key to retrieve.
|
|
537
|
+
* @returns {Promise<object|null>} - The parsed data for the key or null if not found.
|
|
538
|
+
*/
|
|
539
|
+
async getGlobal(tableName, key) {
|
|
540
|
+
return new Promise((resolve) => {
|
|
541
|
+
this.gun.get(this.appname).get(tableName).get(key).once((data) => {
|
|
542
|
+
if (!data) {
|
|
543
|
+
resolve(null);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const parsed = this.parse(data);
|
|
548
|
+
resolve(parsed);
|
|
549
|
+
} catch (e) {
|
|
550
|
+
resolve(null);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
268
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Retrieves all data from a global table.
|
|
560
|
+
* @param {string} tableName - The table name to retrieve data from.
|
|
561
|
+
* @returns {Promise<object|null>} - The parsed data from the table or null if not found.
|
|
562
|
+
*/
|
|
563
|
+
async getAllGlobal(tableName) {
|
|
564
|
+
return new Promise(async (resolve, reject) => {
|
|
565
|
+
let output = []
|
|
566
|
+
let counter = 0
|
|
567
|
+
this.gun.get(tableName.toString()).once((data, key) => {
|
|
568
|
+
if (data) {
|
|
569
|
+
const maplenght = Object.keys(data).length - 1
|
|
570
|
+
this.gun.get(tableName.toString()).map().once(async (itemdata, key) => {
|
|
571
|
+
counter += 1
|
|
572
|
+
if (itemdata) {
|
|
573
|
+
let parsed = await this.parse(itemdata)
|
|
574
|
+
output.push(parsed);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (counter == maplenght) {
|
|
578
|
+
resolve(output);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
} else resolve(output)
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Deletes a specific key from a global table.
|
|
590
|
+
* @param {string} tableName - The table name to delete from.
|
|
591
|
+
* @param {string} key - The key to delete.
|
|
592
|
+
* @returns {Promise<void>}
|
|
593
|
+
*/
|
|
594
|
+
async deleteGlobal(tableName, key) {
|
|
595
|
+
await this.gun.get(this.appname).get(tableName).get(key).put(null)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Deletes an entire global table.
|
|
600
|
+
* @param {string} tableName - The table name to delete.
|
|
601
|
+
* @returns {Promise<void>}
|
|
602
|
+
*/
|
|
603
|
+
async deleteAllGlobal(tableName) {
|
|
604
|
+
|
|
605
|
+
return new Promise((resolve) => {
|
|
606
|
+
this.gun.get(this.appname).get(tableName).map().put(null).once(
|
|
607
|
+
(data, key) => this.gun.get(this.appname).get(tableName).get(key).put(null)
|
|
608
|
+
)
|
|
609
|
+
this.gun.get(this.appname).get(tableName).put({}, ack => {
|
|
610
|
+
resolve();
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
}
|
|
269
614
|
|
|
615
|
+
// ================================ COMPUTE FUNCTIONS ================================
|
|
270
616
|
/**
|
|
271
|
-
* Computes summaries based on the content within a
|
|
272
|
-
* @param {string}
|
|
617
|
+
* Computes summaries based on the content within a holon and lens.
|
|
618
|
+
* @param {string} holon - The holon identifier.
|
|
273
619
|
* @param {string} lens - The lens to compute.
|
|
274
620
|
* @param {string} operation - The operation to perform.
|
|
275
621
|
*/
|
|
276
|
-
async compute(
|
|
622
|
+
async compute(holon, lens, operation) {
|
|
277
623
|
|
|
278
|
-
|
|
279
|
-
|
|
624
|
+
let res = h3.getResolution(holon);
|
|
625
|
+
if(res < 1 || res > 15) return;
|
|
280
626
|
console.log(res)
|
|
281
|
-
let parent = h3.cellToParent(
|
|
627
|
+
let parent = h3.cellToParent(holon, res - 1);
|
|
282
628
|
let siblings = h3.cellToChildren(parent, res);
|
|
283
|
-
console.log(
|
|
629
|
+
console.log(holon, parent, siblings, res)
|
|
284
630
|
|
|
285
631
|
let content = [];
|
|
286
632
|
let promises = [];
|
|
@@ -292,7 +638,7 @@ class HoloSphere {
|
|
|
292
638
|
resolve(); // Resolve the promise to prevent it from hanging
|
|
293
639
|
}, 1000); // Timeout of 5 seconds
|
|
294
640
|
|
|
295
|
-
this.gun.get(siblings[i]).get(lens).map().once((data, key) => {
|
|
641
|
+
this.gun.get(this.appname).get(siblings[i]).get(lens).map().once((data, key) => {
|
|
296
642
|
clearTimeout(timeout); // Clear the timeout if data is received
|
|
297
643
|
if (data) {
|
|
298
644
|
content.push(data.content);
|
|
@@ -306,25 +652,25 @@ class HoloSphere {
|
|
|
306
652
|
console.log('Content:', content);
|
|
307
653
|
let computed = await this.summarize(content.join('\n'))
|
|
308
654
|
console.log('Computed:', computed)
|
|
309
|
-
let node = await this.gun.get(parent + '_summary').put({ id: parent + '_summary', content: computed })
|
|
655
|
+
let node = await this.gun.get(this.appname).get(parent + '_summary').put({ id: parent + '_summary', content: computed })
|
|
310
656
|
|
|
311
657
|
this.put(parent, lens, node);
|
|
312
658
|
this.compute(parent, lens, operation)
|
|
313
659
|
}
|
|
314
660
|
|
|
315
661
|
/**
|
|
316
|
-
* Clears all entities under a specific
|
|
317
|
-
* @param {string}
|
|
662
|
+
* Clears all entities under a specific holon and lens.
|
|
663
|
+
* @param {string} holon - The holon identifier.
|
|
318
664
|
* @param {string} lens - The lens to clear.
|
|
319
665
|
*/
|
|
320
|
-
async clearlens(
|
|
666
|
+
async clearlens(holon, lens) {
|
|
321
667
|
let entities = {};
|
|
322
668
|
|
|
323
669
|
// Get list out of Gun
|
|
324
|
-
this.gun.get(
|
|
670
|
+
this.gun.get(this.appname).get(holon).get(lens).map().once((data, key) => {
|
|
325
671
|
//entities = data;
|
|
326
672
|
//const id = Object.keys(entities)[0] // since this would be in object form, you can manipulate it as you would like.
|
|
327
|
-
this.gun.get(
|
|
673
|
+
this.gun.get(this.appname).get(holon).get(lens).put({ [key]: null })
|
|
328
674
|
})
|
|
329
675
|
}
|
|
330
676
|
|
|
@@ -367,30 +713,30 @@ class HoloSphere {
|
|
|
367
713
|
}
|
|
368
714
|
|
|
369
715
|
/**
|
|
370
|
-
* Upcasts content to parent
|
|
371
|
-
* @param {string}
|
|
716
|
+
* Upcasts content to parent holonagons recursively.
|
|
717
|
+
* @param {string} holon - The current holon identifier.
|
|
372
718
|
* @param {string} lens - The lens under which to upcast.
|
|
373
719
|
* @param {object} content - The content to upcast.
|
|
374
720
|
* @returns {Promise<object>} - The upcasted content.
|
|
375
721
|
*/
|
|
376
|
-
async upcast(
|
|
377
|
-
let res = h3.getResolution(
|
|
722
|
+
async upcast(holon, lens, content) {
|
|
723
|
+
let res = h3.getResolution(holon)
|
|
378
724
|
if (res == 0) {
|
|
379
|
-
await this.putNode(
|
|
725
|
+
await this.putNode(holon, lens, content)
|
|
380
726
|
return content
|
|
381
727
|
}
|
|
382
728
|
else {
|
|
383
|
-
console.log('Upcasting ',
|
|
384
|
-
await this.putNode(
|
|
385
|
-
let parent = h3.cellToParent(
|
|
729
|
+
console.log('Upcasting ', holon, lens, content, res)
|
|
730
|
+
await this.putNode(holon, lens, content)
|
|
731
|
+
let parent = h3.cellToParent(holon, res - 1)
|
|
386
732
|
return this.upcast(parent, lens, content)
|
|
387
733
|
}
|
|
388
734
|
}
|
|
389
735
|
|
|
390
736
|
|
|
391
737
|
/**
|
|
392
|
-
* Updates the parent
|
|
393
|
-
* @param {string} id - The child
|
|
738
|
+
* Updates the parent holonagon with a new report.
|
|
739
|
+
* @param {string} id - The child holon identifier.
|
|
394
740
|
* @param {string} report - The report to update.
|
|
395
741
|
* @returns {Promise<object>} - The updated parent information.
|
|
396
742
|
*/
|
|
@@ -410,21 +756,21 @@ class HoloSphere {
|
|
|
410
756
|
|
|
411
757
|
|
|
412
758
|
/**
|
|
413
|
-
* Converts latitude and longitude to a
|
|
759
|
+
* Converts latitude and longitude to a holon identifier.
|
|
414
760
|
* @param {number} lat - The latitude.
|
|
415
761
|
* @param {number} lng - The longitude.
|
|
416
762
|
* @param {number} resolution - The resolution level.
|
|
417
|
-
* @returns {Promise<string>} - The resulting
|
|
763
|
+
* @returns {Promise<string>} - The resulting holon identifier.
|
|
418
764
|
*/
|
|
419
|
-
async
|
|
765
|
+
async getHolon(lat, lng, resolution) {
|
|
420
766
|
return h3.latLngToCell(lat, lng, resolution);
|
|
421
767
|
}
|
|
422
768
|
|
|
423
769
|
/**
|
|
424
|
-
* Retrieves all containing
|
|
770
|
+
* Retrieves all containing holonagons at all scales for given coordinates.
|
|
425
771
|
* @param {number} lat - The latitude.
|
|
426
772
|
* @param {number} lng - The longitude.
|
|
427
|
-
* @returns {Array<string>} - List of
|
|
773
|
+
* @returns {Array<string>} - List of holon identifiers.
|
|
428
774
|
*/
|
|
429
775
|
getScalespace(lat, lng) {
|
|
430
776
|
let list = []
|
|
@@ -437,113 +783,30 @@ class HoloSphere {
|
|
|
437
783
|
}
|
|
438
784
|
|
|
439
785
|
/**
|
|
440
|
-
* Retrieves all containing
|
|
441
|
-
* @param {string}
|
|
442
|
-
* @returns {Array<string>} - List of
|
|
786
|
+
* Retrieves all containing holonagons at all scales for a given holon.
|
|
787
|
+
* @param {string} holon - The holon identifier.
|
|
788
|
+
* @returns {Array<string>} - List of holon identifiers.
|
|
443
789
|
*/
|
|
444
|
-
|
|
790
|
+
getHolonScalespace(holon) {
|
|
445
791
|
let list = []
|
|
446
|
-
let res = h3.getResolution(
|
|
792
|
+
let res = h3.getResolution(holon)
|
|
447
793
|
for (let i = res; i >= 0; i--) {
|
|
448
|
-
list.push(h3.cellToParent(
|
|
794
|
+
list.push(h3.cellToParent(holon, i))
|
|
449
795
|
}
|
|
450
796
|
return list
|
|
451
797
|
}
|
|
452
798
|
|
|
453
799
|
/**
|
|
454
|
-
* Subscribes to changes in a specific
|
|
455
|
-
* @param {string}
|
|
800
|
+
* Subscribes to changes in a specific holon and lens.
|
|
801
|
+
* @param {string} holon - The holon identifier.
|
|
456
802
|
* @param {string} lens - The lens to subscribe to.
|
|
457
803
|
* @param {function} callback - The callback to execute on changes.
|
|
458
804
|
*/
|
|
459
|
-
subscribe(
|
|
460
|
-
this.gun.get(
|
|
805
|
+
subscribe(holon, lens, callback) {
|
|
806
|
+
this.gun.get(this.appname).get(holon).get(lens).map().on((data, key) => {
|
|
461
807
|
callback(data, key)
|
|
462
808
|
})
|
|
463
809
|
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Retrieves the final vote for a user, considering delegations.
|
|
467
|
-
* @param {string} userId - The user's identifier.
|
|
468
|
-
* @param {string} topic - The voting topic.
|
|
469
|
-
* @param {object} votes - The current votes.
|
|
470
|
-
* @param {Set<string>} [visited=new Set()] - Set of visited users to prevent cycles.
|
|
471
|
-
* @returns {string|null} - The final vote or null if not found.
|
|
472
|
-
*/
|
|
473
|
-
getFinalVote(userId, topic, votes, visited = new Set()) {
|
|
474
|
-
if (this.users[userId]) { // Added this.users
|
|
475
|
-
if (visited.has(userId)) {
|
|
476
|
-
return null; // Avoid circular delegations
|
|
477
|
-
}
|
|
478
|
-
visited.add(userId);
|
|
479
|
-
|
|
480
|
-
const delegation = this.users[userId].delegations[topic];
|
|
481
|
-
if (delegation && votes[delegation] === undefined) {
|
|
482
|
-
return this.getFinalVote(delegation, topic, votes, visited); // Prefixed with this
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return votes[userId] !== undefined ? votes[userId] : null;
|
|
486
|
-
}
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Aggregates votes for a specific hex and topic.
|
|
492
|
-
* @param {string} hexId - The hex identifier.
|
|
493
|
-
* @param {string} topic - The voting topic.
|
|
494
|
-
* @returns {object} - Aggregated vote counts.
|
|
495
|
-
*/
|
|
496
|
-
aggregateVotes(hexId, topic) {
|
|
497
|
-
if (!this.hexagonVotes[hexId] || !this.hexagonVotes[hexId][topic]) {
|
|
498
|
-
return {}; // Handle undefined votes
|
|
499
|
-
}
|
|
500
|
-
const votes = this.hexagonVotes[hexId][topic];
|
|
501
|
-
const aggregatedVotes = {};
|
|
502
|
-
|
|
503
|
-
Object.keys(votes).forEach(userId => {
|
|
504
|
-
const finalVote = this.getFinalVote(userId, topic, votes); // Prefixed with this
|
|
505
|
-
if (finalVote !== null) {
|
|
506
|
-
aggregatedVotes[finalVote] = (aggregatedVotes[finalVote] || 0) + 1;
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
return aggregatedVotes;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Delegates a user's vote to another user.
|
|
515
|
-
* @param {string} userId - The user's identifier.
|
|
516
|
-
* @param {string} topic - The voting topic.
|
|
517
|
-
* @param {string} delegateTo - The user to delegate the vote to.
|
|
518
|
-
*/
|
|
519
|
-
async delegateVote(userId, topic, delegateTo) {
|
|
520
|
-
const response = await fetch('/delegate', {
|
|
521
|
-
method: 'POST',
|
|
522
|
-
headers: { 'Content-Type': 'application/json' },
|
|
523
|
-
body: JSON.stringify({ userId, topic, delegateTo })
|
|
524
|
-
});
|
|
525
|
-
alert(await response.text());
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Casts a vote for a user on a specific topic and hex.
|
|
530
|
-
* @param {string} userId - The user's identifier.
|
|
531
|
-
* @param {string} hexId - The hex identifier.
|
|
532
|
-
* @param {string} topic - The voting topic.
|
|
533
|
-
* @param {string} vote - The vote choice.
|
|
534
|
-
*/
|
|
535
|
-
async vote(userId, hexId, topic, vote) {
|
|
536
|
-
const response = await fetch('/vote', {
|
|
537
|
-
method: 'POST',
|
|
538
|
-
headers: { 'Content-Type': 'application/json' },
|
|
539
|
-
body: JSON.stringify({ userId, hexId, topic, vote })
|
|
540
|
-
});
|
|
541
|
-
alert(await response.text());
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
810
|
}
|
|
548
811
|
|
|
549
812
|
export default HoloSphere;
|