holosphere 1.1.10 → 1.1.12

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/content.js ADDED
@@ -0,0 +1,946 @@
1
+ // holo_content.js
2
+
3
+ /**
4
+ * Stores content in the specified holon and lens.
5
+ * If the target path already contains a hologram, the put operation will be
6
+ * redirected to store the new data at the location specified in the existing
7
+ * hologram's soul.
8
+ * If the stored data (after potential redirection) is a hologram, this function
9
+ * also attempts to update the target data node's `_holograms` set.
10
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
11
+ * @param {string} holon - The initial holon identifier.
12
+ * @param {string} lens - The initial lens under which to store the content.
13
+ * @param {object} data - The data to store.
14
+ * @param {string} [password] - Optional password for private holon.
15
+ * @param {object} [options] - Additional options
16
+ * @param {boolean} [options.autoPropagate=true] - Whether to automatically propagate to federated holons (default: true)
17
+ * @param {object} [options.propagationOptions] - Options to pass to propagate
18
+ * @param {boolean} [options.propagationOptions.useHolograms=true] - Whether to use holograms instead of duplicating data
19
+ * @param {boolean} [options.disableHologramRedirection=false] - Whether to disable hologram redirection
20
+ * @returns {Promise<object>} - Returns an object with success status, path info, propagation result, and list of updated holograms
21
+ */
22
+ export async function put(holoInstance, holon, lens, data, password = null, options = {}) {
23
+ if (!data) { // Check data first as it's used for id generation
24
+ throw new Error('put: Missing required data parameter');
25
+ }
26
+ if (!holon || !lens) {
27
+ throw new Error('put: Missing required holon or lens parameters:', holon, lens);
28
+ }
29
+
30
+ const { disableHologramRedirection = false } = options; // Extract new option
31
+
32
+ let targetHolon = holon;
33
+ let targetLens = lens;
34
+ let targetKey = data.id; // Use data.id as the key
35
+
36
+ if (!targetKey) {
37
+ targetKey = holoInstance.generateId();
38
+ data.id = targetKey; // Assign the generated ID back to the data
39
+ }
40
+
41
+ // --- Start: Target Path Hologram Redirection Logic ---
42
+ try {
43
+ // Get the item at the original target path, WITHOUT resolving holograms
44
+ const existingItemAtPath = await get(holoInstance, targetHolon, targetLens, targetKey, password, { resolveHolograms: false });
45
+
46
+ if (!disableHologramRedirection && existingItemAtPath && holoInstance.isHologram(existingItemAtPath)) {
47
+ const soulInfo = holoInstance.parseSoulPath(existingItemAtPath.soul);
48
+ if (soulInfo) {
49
+ // Optional: Check if soulInfo.appname matches holoInstance.appname
50
+ if (soulInfo.appname !== holoInstance.appname) {
51
+ console.warn(`Existing hologram at ${targetHolon}/${targetLens}/${targetKey} has appname (${soulInfo.appname}) in its soul ${existingItemAtPath.soul} which does not match current HoloSphere instance appname (${holoInstance.appname}). Redirecting put to soul's holon/lens within this instance.`);
52
+ }
53
+ console.log(`Redirecting put for data (ID: ${data.id}). Original target ${targetHolon}/${targetLens}/${targetKey} contained hologram (ID: ${existingItemAtPath.id}, Soul: ${existingItemAtPath.soul}). New target is ${soulInfo.holon}/${soulInfo.lens}/${soulInfo.key}.`);
54
+ targetHolon = soulInfo.holon; // Redirect holon
55
+ targetLens = soulInfo.lens; // Redirect lens
56
+ targetKey = soulInfo.key; // Redirect key (important!)
57
+ // data.id should ideally match soulInfo.key if this is consistent.
58
+ // If data.id is different, it means we are writing data with one ID to a path derived from another ID's soul.
59
+ if (data.id !== targetKey) {
60
+ console.warn(`Data ID ('${data.id}') differs from redirected target key ('${targetKey}') derived from existing hologram's soul. Data will be stored under key '${targetKey}'.`);
61
+ // It's crucial that the actual GunDB path uses targetKey.
62
+ // The 'data' object itself retains its original 'data.id' unless explicitly changed.
63
+ }
64
+
65
+ } else {
66
+ console.warn(`Existing item at ${targetHolon}/${targetLens}/${targetKey} (ID: ${existingItemAtPath.id}) is a hologram, but its soul ('${existingItemAtPath.soul}') is invalid. Proceeding with original target.`);
67
+ }
68
+ }
69
+ } catch (error) {
70
+ // If 'get' fails (e.g., item not found, auth error), proceed with original target.
71
+ // A "not found" error is expected if the path is new.
72
+ if (error.message && error.message.includes('RESOLVED_NULL')) {
73
+ // This is fine, means nothing was at the path.
74
+ } else {
75
+ console.warn(`Error checking for existing hologram at ${targetHolon}/${targetLens}/${targetKey}: ${error.message}. Proceeding with original target.`);
76
+ }
77
+ }
78
+ // --- End: Target Path Hologram Redirection Logic ---
79
+
80
+ // The data being stored is 'data'. Its 'id' property is 'data.id'.
81
+ // The final storage path key is 'targetKey'.
82
+
83
+ // Check if the data *being put* is a hologram (this variable is used later for schema and propagation)
84
+ const isHologram = holoInstance.isHologram(data);
85
+
86
+ // Get and validate schema only in strict mode for non-holograms (data being put)
87
+ if (holoInstance.strict && !isHologram) {
88
+ const schema = await holoInstance.getSchema(targetLens); // Use targetLens for schema
89
+ if (!schema) {
90
+ throw new Error('Schema required in strict mode');
91
+ }
92
+ const dataToValidate = JSON.parse(JSON.stringify(data)); // Validate the actual data
93
+ const valid = holoInstance.validator.validate(schema, dataToValidate);
94
+
95
+ if (!valid) {
96
+ const errorMsg = `Schema validation failed: ${JSON.stringify(holoInstance.validator.errors)}`;
97
+ throw new Error(errorMsg);
98
+ }
99
+ }
100
+
101
+ try {
102
+ let user = null;
103
+ if (password) {
104
+ user = holoInstance.gun.user();
105
+ await new Promise((resolve, reject) => {
106
+ const userNameString = holoInstance.userName(targetHolon); // Use targetHolon for put
107
+ user.auth(userNameString, password, (authAck) => {
108
+ if (authAck.err) {
109
+ console.log(`Initial auth failed for ${userNameString} during put, attempting to create...`);
110
+ user.create(userNameString, password, (createAck) => {
111
+ if (createAck.err) {
112
+ if (createAck.err.includes("already created")) {
113
+ console.log(`User ${userNameString} already existed during put, re-attempting auth with fresh user object.`);
114
+ const freshUser = holoInstance.gun.user(); // Get a new user object
115
+ freshUser.auth(userNameString, password, (secondAuthAck) => {
116
+ if (secondAuthAck.err) {
117
+ reject(new Error(`Failed to auth with fresh user object after create attempt (user existed) for ${userNameString} during put: ${secondAuthAck.err}`));
118
+ } else {
119
+ resolve();
120
+ }
121
+ });
122
+ } else {
123
+ reject(new Error(`Failed to create user ${userNameString} during put: ${createAck.err}`));
124
+ }
125
+ } else {
126
+ console.log(`User ${userNameString} created successfully during put, attempting auth...`);
127
+ user.auth(userNameString, password, (secondAuthAck) => {
128
+ if (secondAuthAck.err) {
129
+ reject(new Error(`Failed to auth after create for ${userNameString} during put: ${secondAuthAck.err}`));
130
+ } else {
131
+ resolve();
132
+ }
133
+ });
134
+ }
135
+ });
136
+ } else {
137
+ resolve(); // Auth successful
138
+ }
139
+ });
140
+ });
141
+ }
142
+
143
+ return new Promise((resolve, reject) => {
144
+ try {
145
+ // Create a copy of data without the _meta field if it exists
146
+ let dataToStore = { ...data };
147
+ if (dataToStore._meta !== undefined) {
148
+ delete dataToStore._meta;
149
+ }
150
+ const payload = JSON.stringify(dataToStore); // The data being stored
151
+
152
+ const putCallback = async (ack) => {
153
+ if (ack.err) {
154
+ reject(new Error(ack.err));
155
+ } else {
156
+ // --- Start: Hologram Tracking Logic (for data *being put*, if it's a hologram) ---
157
+ if (isHologram) {
158
+ try {
159
+ const storedDataSoulInfo = holoInstance.parseSoulPath(data.soul);
160
+ if (storedDataSoulInfo) {
161
+ const targetNodeRef = holoInstance.getNodeRef(data.soul); // Target of the data *being put*
162
+ // Soul of the hologram that was *actually stored* at targetHolon/targetLens/targetKey
163
+ const storedHologramInstanceSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
164
+
165
+ targetNodeRef.get('_holograms').get(storedHologramInstanceSoul).put(true);
166
+
167
+ console.log(`Data (ID: ${data.id}) being put is a hologram. Added its instance soul ${storedHologramInstanceSoul} to its target ${data.soul}'s _holograms set.`);
168
+ } else {
169
+ console.warn(`Data (ID: ${data.id}) being put is a hologram, but could not parse its soul ${data.soul} for tracking.`);
170
+ }
171
+ } catch (trackingError) {
172
+ console.warn(`Error updating _holograms set for the target of the data being put (data ID: ${data.id}, soul: ${data.soul}):`, trackingError);
173
+ }
174
+ }
175
+ // --- End: Hologram Tracking Logic ---
176
+
177
+ // --- Start: Active Hologram Update Logic (for actual data being stored) ---
178
+ let updatedHolograms = [];
179
+ if (!isHologram && !options.isHologramUpdate) {
180
+ try {
181
+ const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
182
+ const currentNodeRef = holoInstance.getNodeRef(currentDataSoul);
183
+
184
+ // Get the _holograms set for this data
185
+ await new Promise((resolveHologramUpdate) => {
186
+ currentNodeRef.get('_holograms').once(async (hologramsSet) => {
187
+ if (hologramsSet) {
188
+ const hologramSouls = Object.keys(hologramsSet).filter(k =>
189
+ k !== '_' && hologramsSet[k] === true // Only active holograms (deleted ones are null/removed)
190
+ );
191
+
192
+ if (hologramSouls.length > 0) {
193
+ console.log(`Updating ${hologramSouls.length} active holograms for data ${data.id}`);
194
+
195
+ // Update each active hologram with an 'updated' timestamp
196
+ const updatePromises = hologramSouls.map(async (hologramSoul) => {
197
+ try {
198
+ const hologramSoulInfo = holoInstance.parseSoulPath(hologramSoul);
199
+ if (hologramSoulInfo) {
200
+ // Get the current hologram data
201
+ const currentHologram = await holoInstance.get(
202
+ hologramSoulInfo.holon,
203
+ hologramSoulInfo.lens,
204
+ hologramSoulInfo.key,
205
+ null,
206
+ { resolveHolograms: false }
207
+ );
208
+
209
+ if (currentHologram) {
210
+ // Update the hologram with an 'updated' timestamp
211
+ const updatedHologram = {
212
+ ...currentHologram,
213
+ updated: Date.now()
214
+ };
215
+
216
+ await holoInstance.put(
217
+ hologramSoulInfo.holon,
218
+ hologramSoulInfo.lens,
219
+ updatedHologram,
220
+ null,
221
+ {
222
+ autoPropagate: false, // Don't auto-propagate hologram updates
223
+ disableHologramRedirection: true, // Prevent redirection when updating holograms
224
+ isHologramUpdate: true // Prevent recursive hologram updates
225
+ }
226
+ );
227
+
228
+ console.log(`Updated hologram at ${hologramSoul} with timestamp`);
229
+
230
+ // Add to the list of updated holograms
231
+ updatedHolograms.push({
232
+ soul: hologramSoul,
233
+ holon: hologramSoulInfo.holon,
234
+ lens: hologramSoulInfo.lens,
235
+ key: hologramSoulInfo.key,
236
+ id: hologramSoulInfo.key,
237
+ timestamp: updatedHologram.updated
238
+ });
239
+ }
240
+ }
241
+ } catch (hologramUpdateError) {
242
+ console.warn(`Error updating hologram ${hologramSoul}:`, hologramUpdateError);
243
+ }
244
+ });
245
+
246
+ await Promise.all(updatePromises);
247
+ }
248
+ }
249
+ resolveHologramUpdate(); // Resolve the promise to continue with the main put logic
250
+ });
251
+ });
252
+ } catch (hologramUpdateError) {
253
+ console.warn(`Error checking for active holograms to update:`, hologramUpdateError);
254
+ }
255
+ }
256
+ // --- End: Active Hologram Update Logic ---
257
+
258
+ // Only notify subscribers for actual data, not holograms
259
+ if (!isHologram) {
260
+ holoInstance.notifySubscribers({
261
+ holon: targetHolon, // Notify with final target
262
+ lens: targetLens,
263
+ ...data // The data that was put
264
+ });
265
+ }
266
+
267
+ // Auto-propagate to federation by default (if data *being put* is not a hologram)
268
+ const shouldPropagate = options.autoPropagate !== false && !isHologram;
269
+ let propagationResult = null;
270
+
271
+ if (shouldPropagate) {
272
+ try {
273
+ const propagationOptions = {
274
+ useHolograms: true,
275
+ ...options.propagationOptions
276
+ };
277
+
278
+ propagationResult = await holoInstance.propagate(
279
+ targetHolon, // Propagate from final target
280
+ targetLens,
281
+ data, // The data that was put
282
+ propagationOptions
283
+ );
284
+
285
+ if (propagationResult && propagationResult.errors > 0) {
286
+ console.warn('Auto-propagation had errors:', propagationResult);
287
+ }
288
+ } catch (propError) {
289
+ console.warn('Error in auto-propagation:', propError);
290
+ }
291
+ }
292
+
293
+ resolve({
294
+ success: true,
295
+ isHologramAtPath: isHologram, // whether the data *put* was a hologram
296
+ pathHolon: targetHolon,
297
+ pathLens: targetLens,
298
+ pathKey: targetKey,
299
+ propagationResult,
300
+ updatedHolograms: updatedHolograms // List of holograms that were updated
301
+ });
302
+ }
303
+ };
304
+
305
+ // Use targetHolon, targetLens, and targetKey for the actual storage path
306
+ const dataPath = password ?
307
+ user.get('private').get(targetLens).get(targetKey) :
308
+ holoInstance.gun.get(holoInstance.appname).get(targetHolon).get(targetLens).get(targetKey);
309
+
310
+ dataPath.put(payload, putCallback);
311
+ } catch (error) {
312
+ reject(error);
313
+ }
314
+ });
315
+ } catch (error) {
316
+ console.error('Error in put:', error);
317
+ throw error;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Retrieves content from the specified holon and lens.
323
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
324
+ * @param {string} holon - The holon identifier.
325
+ * @param {string} lens - The lens from which to retrieve content.
326
+ * @param {string} key - The specific key to retrieve.
327
+ * @param {string} [password] - Optional password for private holon.
328
+ * @param {object} [options] - Additional options
329
+ * @param {boolean} [options.resolveHolograms=true] - Whether to automatically resolve holograms
330
+ * @param {object} [options.validationOptions] - Options passed to the schema validator
331
+ * @returns {Promise<object|null>} - The retrieved content or null if not found.
332
+ */
333
+ export async function get(holoInstance, holon, lens, key, password = null, options = {}) {
334
+ if (!holon || !lens || !key) {
335
+ console.error('get: Missing required parameters');
336
+ return null;
337
+ }
338
+
339
+ // Destructure options, including visited
340
+ const { resolveHolograms = true, validationOptions = {}, visited } = options;
341
+
342
+ // Get schema for validation if in strict mode
343
+ let schema = null;
344
+ if (holoInstance.strict) {
345
+ schema = await holoInstance.getSchema(lens);
346
+ if (!schema) {
347
+ throw new Error('Schema required in strict mode');
348
+ }
349
+ }
350
+
351
+ try {
352
+ let user = null;
353
+ if (password) {
354
+ user = holoInstance.gun.user();
355
+ await new Promise((resolve, reject) => {
356
+ const userNameString = holoInstance.userName(holon); // Use holon for get
357
+ user.auth(userNameString, password, (authAck) => {
358
+ if (authAck.err) {
359
+ // If auth fails, reject immediately. Do not attempt to create user.
360
+ reject(new Error(`Authentication failed for ${userNameString} during get: ${authAck.err}`));
361
+ } else {
362
+ resolve(); // Auth successful
363
+ }
364
+ });
365
+ });
366
+ }
367
+
368
+ return new Promise((resolve) => {
369
+ const handleData = async (data) => {
370
+ let parsed = null; // Declare parsed here to make it available in catch
371
+ if (!data) {
372
+ resolve(null);
373
+ return;
374
+ }
375
+
376
+ try {
377
+ parsed = await holoInstance.parse(data); // Assign to the outer scoped parsed
378
+
379
+ if (!parsed) {
380
+ resolve(null);
381
+ return;
382
+ }
383
+
384
+ // Check if this is a hologram that needs to be resolved
385
+ if (resolveHolograms && holoInstance.isHologram(parsed)) {
386
+ const resolvedValue = await holoInstance.resolveHologram(parsed, {
387
+ followHolograms: resolveHolograms,
388
+ visited: visited
389
+ });
390
+
391
+ console.log(`### get/handleData received resolved value:`, resolvedValue);
392
+
393
+ if (resolvedValue === null) {
394
+ // This means resolveHologram determined the target doesn't exist or a sub-resolution failed to null.
395
+ console.warn(`Hologram at ${holon}/${lens}/${key} could not be fully resolved (target not found or sub-problem). Resolving null.`);
396
+ resolve(null);
397
+ return; // Important to return after resolving
398
+ }
399
+ // If resolveHologram encountered a circular ref, it would throw, not return.
400
+ // If it returned the hologram itself (if we ever revert to that), this logic would need adjustment.
401
+ // For now, assume resolvedValue is either the resolved data or we've returned null above.
402
+
403
+ if (resolvedValue !== parsed) {
404
+ console.log(`### get/handleData using resolved data:`, resolvedValue);
405
+ parsed = resolvedValue;
406
+ }
407
+ }
408
+
409
+ // Perform schema validation if needed
410
+ if (schema) {
411
+ const valid = holoInstance.validator.validate(schema, parsed);
412
+ if (!valid) {
413
+ console.error('get: Invalid data according to schema:', holoInstance.validator.errors);
414
+ if (holoInstance.strict) {
415
+ resolve(null);
416
+ return;
417
+ }
418
+ }
419
+ }
420
+
421
+ resolve(parsed);
422
+ } catch (error) {
423
+ if (error.message?.startsWith('CIRCULAR_REFERENCE')) {
424
+ console.warn(`Caught circular reference during get/handleData for key ${key}. Resolving null.`);
425
+ resolve(null);
426
+ } else {
427
+ console.error('Error processing data in get/handleData:', error);
428
+ resolve(null); // For other errors, resolve null
429
+ }
430
+ }
431
+ };
432
+
433
+ const dataPath = password ?
434
+ user.get('private').get(lens).get(key) :
435
+ holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).get(key);
436
+
437
+ dataPath.once(handleData);
438
+ });
439
+ } catch (error) {
440
+ console.error('Error in get:', error);
441
+ return null;
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Retrieves all content from the specified holon and lens.
447
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
448
+ * @param {string} holon - The holon identifier.
449
+ * @param {string} lens - The lens from which to retrieve content.
450
+ * @param {string} [password] - Optional password for private holon.
451
+ * @returns {Promise<Array<object>>} - The retrieved content.
452
+ */
453
+ export async function getAll(holoInstance, holon, lens, password = null) {
454
+ if (!holon || !lens) {
455
+ throw new Error('getAll: Missing required parameters');
456
+ }
457
+
458
+ const schema = await holoInstance.getSchema(lens);
459
+ if (!schema && holoInstance.strict) {
460
+ throw new Error('getAll: Schema required in strict mode');
461
+ }
462
+
463
+ try {
464
+ let user = null;
465
+ if (password) {
466
+ user = holoInstance.gun.user();
467
+ await new Promise((resolve, reject) => {
468
+ const userNameString = holoInstance.userName(holon); // Use holon for getAll
469
+ user.auth(userNameString, password, (authAck) => {
470
+ if (authAck.err) {
471
+ console.log(`Initial auth failed for ${userNameString} during getAll, attempting to create...`);
472
+ user.create(userNameString, password, (createAck) => {
473
+ if (createAck.err) {
474
+ if (createAck.err.includes("already created")) {
475
+ console.log(`User ${userNameString} already existed during getAll, re-attempting auth with fresh user object.`);
476
+ const freshUser = holoInstance.gun.user(); // Get a new user object
477
+ freshUser.auth(userNameString, password, (secondAuthAck) => {
478
+ if (secondAuthAck.err) {
479
+ reject(new Error(`Failed to auth with fresh user object after create attempt (user existed) for ${userNameString} during getAll: ${secondAuthAck.err}`));
480
+ } else {
481
+ resolve();
482
+ }
483
+ });
484
+ } else {
485
+ reject(new Error(`Failed to create user ${userNameString} during getAll: ${createAck.err}`));
486
+ }
487
+ } else {
488
+ console.log(`User ${userNameString} created successfully during getAll, attempting auth...`);
489
+ user.auth(userNameString, password, (secondAuthAck) => {
490
+ if (secondAuthAck.err) {
491
+ reject(new Error(`Failed to auth after create for ${userNameString} during getAll: ${secondAuthAck.err}`));
492
+ } else {
493
+ resolve();
494
+ }
495
+ });
496
+ }
497
+ });
498
+ } else {
499
+ resolve(); // Auth successful
500
+ }
501
+ });
502
+ });
503
+ }
504
+
505
+ return new Promise((resolve) => {
506
+ const output = new Map();
507
+
508
+ const processData = async (data, key) => {
509
+ if (!data || key === '_') return;
510
+
511
+ try {
512
+ const parsed = await holoInstance.parse(data); // Use instance's parse
513
+ if (!parsed || !parsed.id) return;
514
+
515
+ // Check if this is a hologram that needs to be resolved
516
+ if (holoInstance.isHologram(parsed)) {
517
+ const resolved = await holoInstance.resolveHologram(parsed, {
518
+ followHolograms: true
519
+ });
520
+
521
+ if (resolved !== parsed) {
522
+ // Hologram was resolved successfully
523
+ if (schema) {
524
+ const valid = holoInstance.validator.validate(schema, resolved);
525
+ if (valid || !holoInstance.strict) {
526
+ output.set(resolved.id, resolved);
527
+ }
528
+ } else {
529
+ output.set(resolved.id, resolved);
530
+ }
531
+ return;
532
+ }
533
+ }
534
+
535
+ if (schema) {
536
+ const valid = holoInstance.validator.validate(schema, parsed);
537
+ if (valid || !holoInstance.strict) {
538
+ output.set(parsed.id, parsed);
539
+ }
540
+ } else {
541
+ output.set(parsed.id, parsed);
542
+ }
543
+ } catch (error) {
544
+ console.error('Error processing data:', error);
545
+ }
546
+ };
547
+
548
+ const handleData = async (data) => {
549
+ if (!data) {
550
+ resolve([]);
551
+ return;
552
+ }
553
+
554
+ const initialPromises = [];
555
+ Object.keys(data)
556
+ .filter(key => key !== '_')
557
+ .forEach(key => {
558
+ initialPromises.push(processData(data[key], key));
559
+ });
560
+
561
+ try {
562
+ await Promise.all(initialPromises);
563
+ resolve(Array.from(output.values()));
564
+ } catch (error) {
565
+ console.error('Error in getAll:', error);
566
+ resolve([]);
567
+ }
568
+ };
569
+
570
+ const dataPath = password ?
571
+ user.get('private').get(lens) :
572
+ holoInstance.gun.get(holoInstance.appname).get(holon).get(lens);
573
+
574
+ dataPath.once(handleData);
575
+ });
576
+ } catch (error) {
577
+ console.error('Error in getAll:', error);
578
+ return [];
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Parses data from GunDB, handling various data formats and references.
584
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
585
+ * @param {*} data - The data to parse, could be a string, object, or GunDB reference.
586
+ * @returns {Promise<object>} - The parsed data.
587
+ */
588
+ export async function parse(holoInstance, rawData) {
589
+ if (rawData === null || rawData === undefined) {
590
+ console.warn('Parse received null or undefined data.');
591
+ return null;
592
+ }
593
+
594
+ // 1. Handle string data (attempt JSON parse)
595
+ if (typeof rawData === 'string') {
596
+ try {
597
+ return JSON.parse(rawData);
598
+ } catch (error) {
599
+ // It's a string, but not valid JSON. Return null.
600
+ console.warn("Data was a string but not valid JSON, returning null:", rawData);
601
+ return null;
602
+ }
603
+ }
604
+
605
+ // 2. Handle object data
606
+ if (typeof rawData === 'object' && rawData !== null) {
607
+ // Check for GunDB soul link (less common now?)
608
+ if (rawData.soul && typeof rawData.soul === 'string' && rawData.id) {
609
+ // This looks like a Hologram object based on structure.
610
+ // Return it as is; resolution happens later if needed.
611
+ return rawData;
612
+ } else if (holoInstance.isHologram(rawData)) {
613
+ // Explicitly check using isHologram (might be redundant if structure check above is reliable)
614
+ return rawData;
615
+ } else if (rawData._) {
616
+ // Handle potential GunDB metadata remnants (attempt cleanup)
617
+ console.warn('Parsing raw Gun object with metadata (_) - attempting cleanup:', rawData);
618
+ const potentialData = Object.keys(rawData).reduce((acc, k) => {
619
+ if (k !== '_') {
620
+ acc[k] = rawData[k];
621
+ }
622
+ return acc;
623
+ }, {});
624
+ if (Object.keys(potentialData).length === 0) {
625
+ console.warn('Raw Gun object had only metadata (_), returning null.');
626
+ return null;
627
+ }
628
+ return potentialData; // Return cleaned-up object
629
+ } else {
630
+ // Assume it's a regular plain object
631
+ return rawData;
632
+ }
633
+ }
634
+
635
+ // 3. Handle other unexpected types
636
+ console.warn("Parsing encountered unexpected data type, returning null:", typeof rawData, rawData);
637
+ return null;
638
+ }
639
+
640
+ /**
641
+ * Deletes a specific key from a given holon and lens.
642
+ * If the deleted data was a hologram, this function also attempts to update the
643
+ * target data node's `_holograms` set by marking the deleted hologram's soul as 'DELETED'.
644
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
645
+ * @param {string} holon - The holon identifier.
646
+ * @param {string} lens - The lens from which to delete the key.
647
+ * @param {string} key - The specific key to delete.
648
+ * @param {string} [password] - Optional password for private holon.
649
+ * @returns {Promise<boolean>} - Returns true if successful
650
+ */
651
+ export async function deleteFunc(holoInstance, holon, lens, key, password = null) { // Renamed to deleteFunc to avoid keyword conflict
652
+ if (!holon || !lens || !key) {
653
+ throw new Error('delete: Missing required parameters');
654
+ }
655
+
656
+ try {
657
+ let user = null;
658
+ if (password) {
659
+ user = holoInstance.gun.user();
660
+ await new Promise((resolve, reject) => {
661
+ const userNameString = holoInstance.userName(holon); // Use holon for deleteFunc
662
+ user.auth(userNameString, password, (authAck) => {
663
+ if (authAck.err) {
664
+ console.log(`Initial auth failed for ${userNameString} during deleteFunc, attempting to create...`);
665
+ user.create(userNameString, password, (createAck) => {
666
+ if (createAck.err) {
667
+ if (createAck.err.includes("already created")) {
668
+ console.log(`User ${userNameString} already existed during deleteFunc, re-attempting auth with fresh user object.`);
669
+ const freshUser = holoInstance.gun.user(); // Get a new user object
670
+ freshUser.auth(userNameString, password, (secondAuthAck) => {
671
+ if (secondAuthAck.err) {
672
+ reject(new Error(`Failed to auth with fresh user object after create attempt (user existed) for ${userNameString} during deleteFunc: ${secondAuthAck.err}`));
673
+ } else {
674
+ resolve();
675
+ }
676
+ });
677
+ } else {
678
+ reject(new Error(`Failed to create user ${userNameString} during deleteFunc: ${createAck.err}`));
679
+ }
680
+ } else {
681
+ console.log(`User ${userNameString} created successfully during deleteFunc, attempting auth...`);
682
+ user.auth(userNameString, password, (secondAuthAck) => {
683
+ if (secondAuthAck.err) {
684
+ reject(new Error(`Failed to auth after create for ${userNameString} during deleteFunc: ${secondAuthAck.err}`));
685
+ } else {
686
+ resolve();
687
+ }
688
+ });
689
+ }
690
+ });
691
+ } else {
692
+ resolve(); // Auth successful
693
+ }
694
+ });
695
+ });
696
+ }
697
+
698
+ const dataPath = password ?
699
+ user.get('private').get(lens).get(key) :
700
+ holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).get(key);
701
+
702
+ // --- Start: Hologram Tracking Removal ---
703
+ let trackingRemovalPromise = Promise.resolve(); // Default to resolved promise
704
+
705
+ // 1. Get the data first to check if it's a hologram
706
+ const rawDataToDelete = await new Promise((resolve) => dataPath.once(resolve));
707
+ let dataToDelete = null;
708
+ try {
709
+ if (typeof rawDataToDelete === 'string') {
710
+ dataToDelete = JSON.parse(rawDataToDelete);
711
+ } else {
712
+ // Handle cases where it might already be an object (though likely string)
713
+ dataToDelete = rawDataToDelete;
714
+ }
715
+ } catch(e) {
716
+ console.warn("[deleteFunc] Could not JSON parse data for deletion check:", rawDataToDelete, e);
717
+ dataToDelete = null; // Ensure it's null if parsing fails
718
+ }
719
+
720
+ // 2. If it is a hologram, try to remove its reference from the target
721
+ const isDataHologram = dataToDelete && holoInstance.isHologram(dataToDelete);
722
+
723
+ if (isDataHologram) {
724
+ try {
725
+ const targetSoul = dataToDelete.soul;
726
+ const targetSoulInfo = holoInstance.parseSoulPath(targetSoul);
727
+
728
+ if (targetSoulInfo) {
729
+ const targetNodeRef = holoInstance.getNodeRef(targetSoul);
730
+ const deletedHologramSoul = `${holoInstance.appname}/${holon}/${lens}/${key}`;
731
+
732
+ // Create a promise that resolves when the hologram is removed from the list
733
+ trackingRemovalPromise = new Promise((resolveTrack) => { // No reject needed, just warn on error
734
+ targetNodeRef.get('_holograms').get(deletedHologramSoul).put(null, (ack) => { // Remove the hologram entry completely
735
+ if (ack.err) {
736
+ console.warn(`[deleteFunc] Error removing hologram ${deletedHologramSoul} from target ${targetSoul}:`, ack.err);
737
+ }
738
+ console.log(`Removed hologram ${deletedHologramSoul} from target ${targetSoul}'s _holograms list`);
739
+ resolveTrack(); // Resolve regardless of ack error to not block main delete
740
+ });
741
+ });
742
+ } else {
743
+ // Keep this warning
744
+ console.warn(`Could not parse target soul ${targetSoul} for hologram tracking removal.`);
745
+ }
746
+ } catch (trackingError) {
747
+ // Keep this warning
748
+ console.warn(`Error initiating hologram reference removal from target ${dataToDelete.soul}:`, trackingError);
749
+ // Ensure trackingRemovalPromise remains resolved if setup fails
750
+ trackingRemovalPromise = Promise.resolve();
751
+ }
752
+ }
753
+ // --- End: Hologram Tracking Removal ---
754
+
755
+ // 3. Wait for the tracking removal attempt to be acknowledged
756
+ await trackingRemovalPromise;
757
+ // Log removed
758
+
759
+ // 4. Proceed with the actual deletion of the hologram node itself
760
+ return new Promise((resolve, reject) => {
761
+ dataPath.put(null, ack => {
762
+ if (ack.err) {
763
+ reject(new Error(ack.err));
764
+ } else {
765
+ resolve(true);
766
+ }
767
+ });
768
+ });
769
+ } catch (error) {
770
+ console.error('Error in delete:', error);
771
+ throw error;
772
+ }
773
+ }
774
+
775
+ /**
776
+ * Deletes all keys from a given holon and lens.
777
+ * @param {HoloSphere} holoInstance - The HoloSphere instance.
778
+ * @param {string} holon - The holon identifier.
779
+ * @param {string} lens - The lens from which to delete all keys.
780
+ * @param {string} [password] - Optional password for private holon.
781
+ * @returns {Promise<boolean>} - Returns true if successful
782
+ */
783
+ export async function deleteAll(holoInstance, holon, lens, password = null) {
784
+ if (!holon || !lens) {
785
+ console.error('deleteAll: Missing holon or lens parameter');
786
+ return false;
787
+ }
788
+
789
+ try {
790
+ let user = null;
791
+ if (password) {
792
+ user = holoInstance.gun.user();
793
+ await new Promise((resolve, reject) => {
794
+ const userNameString = holoInstance.userName(holon); // Use holon for deleteAll
795
+ user.auth(userNameString, password, (authAck) => {
796
+ if (authAck.err) {
797
+ console.log(`Initial auth failed for ${userNameString} during deleteAll, attempting to create...`);
798
+ user.create(userNameString, password, (createAck) => {
799
+ if (createAck.err) {
800
+ if (createAck.err.includes("already created")) {
801
+ console.log(`User ${userNameString} already existed during deleteAll, re-attempting auth with fresh user object.`);
802
+ const freshUser = holoInstance.gun.user(); // Get a new user object
803
+ freshUser.auth(userNameString, password, (secondAuthAck) => {
804
+ if (secondAuthAck.err) {
805
+ reject(new Error(`Failed to auth with fresh user object after create attempt (user existed) for ${userNameString} during deleteAll: ${secondAuthAck.err}`));
806
+ } else {
807
+ resolve();
808
+ }
809
+ });
810
+ } else {
811
+ reject(new Error(`Failed to create user ${userNameString} during deleteAll: ${createAck.err}`));
812
+ }
813
+ } else {
814
+ console.log(`User ${userNameString} created successfully during deleteAll, attempting auth...`);
815
+ user.auth(userNameString, password, (secondAuthAck) => {
816
+ if (secondAuthAck.err) {
817
+ reject(new Error(`Failed to auth after create for ${userNameString} during deleteAll: ${secondAuthAck.err}`));
818
+ } else {
819
+ resolve();
820
+ }
821
+ });
822
+ }
823
+ });
824
+ } else {
825
+ resolve(); // Auth successful
826
+ }
827
+ });
828
+ });
829
+ }
830
+
831
+ return new Promise((resolve) => {
832
+ let deletionPromises = [];
833
+
834
+ const dataPath = password ?
835
+ user.get('private').get(lens) :
836
+ holoInstance.gun.get(holoInstance.appname).get(holon).get(lens);
837
+
838
+ // First get all the data to find keys to delete
839
+ dataPath.once(async (data) => {
840
+ if (!data) {
841
+ resolve(true); // Nothing to delete
842
+ return;
843
+ }
844
+
845
+ // Get all keys except Gun's metadata key '_'
846
+ const keys = Object.keys(data).filter(key => key !== '_');
847
+
848
+ // Process each key to handle holograms properly
849
+ for (const key of keys) {
850
+ try {
851
+ // Get the data to check if it's a hologram
852
+ const itemPath = password ?
853
+ user.get('private').get(lens).get(key) :
854
+ holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).get(key);
855
+
856
+ const rawDataToDelete = await new Promise((resolveItem) => itemPath.once(resolveItem));
857
+ let dataToDelete = null;
858
+
859
+ try {
860
+ if (typeof rawDataToDelete === 'string') {
861
+ dataToDelete = JSON.parse(rawDataToDelete);
862
+ } else {
863
+ dataToDelete = rawDataToDelete;
864
+ }
865
+ } catch(e) {
866
+ console.warn("[deleteAll] Could not JSON parse data for deletion check:", rawDataToDelete, e);
867
+ dataToDelete = null;
868
+ }
869
+
870
+ // Check if it's a hologram and handle accordingly
871
+ const isDataHologram = dataToDelete && holoInstance.isHologram(dataToDelete);
872
+
873
+ if (isDataHologram) {
874
+ // Handle hologram deletion - remove from target's _holograms list
875
+ try {
876
+ const targetSoul = dataToDelete.soul;
877
+ const targetSoulInfo = holoInstance.parseSoulPath(targetSoul);
878
+
879
+ if (targetSoulInfo) {
880
+ const targetNodeRef = holoInstance.getNodeRef(targetSoul);
881
+ const deletedHologramSoul = `${holoInstance.appname}/${holon}/${lens}/${key}`;
882
+
883
+ // Remove the hologram from target's _holograms list
884
+ await new Promise((resolveTrack) => {
885
+ targetNodeRef.get('_holograms').get(deletedHologramSoul).put(null, (ack) => {
886
+ if (ack.err) {
887
+ console.warn(`[deleteAll] Error removing hologram ${deletedHologramSoul} from target ${targetSoul}:`, ack.err);
888
+ }
889
+ console.log(`Removed hologram ${deletedHologramSoul} from target ${targetSoul}'s _holograms list`);
890
+ resolveTrack();
891
+ });
892
+ });
893
+ } else {
894
+ console.warn(`Could not parse target soul ${targetSoul} for hologram tracking removal during deleteAll.`);
895
+ }
896
+ } catch (trackingError) {
897
+ console.warn(`Error removing hologram reference from target ${dataToDelete.soul} during deleteAll:`, trackingError);
898
+ }
899
+ }
900
+
901
+ // Create deletion promise for this key (whether it's a hologram or not)
902
+ deletionPromises.push(
903
+ new Promise((resolveDelete) => {
904
+ const deletePath = password ?
905
+ user.get('private').get(lens).get(key) :
906
+ holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).get(key);
907
+
908
+ deletePath.put(null, ack => {
909
+ resolveDelete(!!ack.ok); // Convert to boolean
910
+ });
911
+ })
912
+ );
913
+ } catch (error) {
914
+ console.warn(`Error processing key ${key} during deleteAll:`, error);
915
+ // Still try to delete the item even if hologram processing failed
916
+ deletionPromises.push(
917
+ new Promise((resolveDelete) => {
918
+ const deletePath = password ?
919
+ user.get('private').get(lens).get(key) :
920
+ holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).get(key);
921
+
922
+ deletePath.put(null, ack => {
923
+ resolveDelete(!!ack.ok);
924
+ });
925
+ })
926
+ );
927
+ }
928
+ }
929
+
930
+ // Wait for all deletions to complete
931
+ Promise.all(deletionPromises)
932
+ .then(results => {
933
+ const allSuccessful = results.every(result => result === true);
934
+ resolve(allSuccessful);
935
+ })
936
+ .catch(error => {
937
+ console.error('Error in deleteAll:', error);
938
+ resolve(false);
939
+ });
940
+ });
941
+ });
942
+ } catch (error) {
943
+ console.error('Error in deleteAll:', error);
944
+ return false;
945
+ }
946
+ }