holosphere 1.3.0-alpha0 → 1.3.0-alpha4

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 CHANGED
@@ -1,5 +1,64 @@
1
1
  // holo_content.js
2
2
 
3
+ /**
4
+ * Recursively sanitizes a value for storage in GunDB.
5
+ *
6
+ * Drops keys whose values would corrupt the graph or round-trip incorrectly:
7
+ * - undefined, NaN, Infinity, -Infinity (JSON.stringify silently turns
8
+ * these into nothing or "null", which is how malformed payloads like
9
+ * `"initiated":,` end up in the graph and surface later as per-character
10
+ * parse warnings).
11
+ * - functions, symbols, bigints (not JSON-representable).
12
+ * Preserves null (legitimate Gun tombstone / explicit empty value).
13
+ * Guards against circular references.
14
+ *
15
+ * Logs one warning per dropped path so the caller can fix the producer.
16
+ */
17
+ function sanitizeForStorage(value, path = '', seen = new WeakSet(), warnings = []) {
18
+ if (value === null) return null;
19
+ const t = typeof value;
20
+
21
+ if (t === 'number') {
22
+ if (!Number.isFinite(value)) {
23
+ warnings.push(`${path || '<root>'}: ${value} (non-finite number)`);
24
+ return undefined;
25
+ }
26
+ return value;
27
+ }
28
+ if (t === 'string' || t === 'boolean') return value;
29
+ if (t === 'undefined' || t === 'function' || t === 'symbol' || t === 'bigint') {
30
+ warnings.push(`${path || '<root>'}: ${t}`);
31
+ return undefined;
32
+ }
33
+
34
+ if (t === 'object') {
35
+ if (seen.has(value)) {
36
+ warnings.push(`${path || '<root>'}: circular reference`);
37
+ return undefined;
38
+ }
39
+ seen.add(value);
40
+
41
+ if (Array.isArray(value)) {
42
+ const out = [];
43
+ for (let i = 0; i < value.length; i++) {
44
+ const cleaned = sanitizeForStorage(value[i], `${path}[${i}]`, seen, warnings);
45
+ out.push(cleaned === undefined ? null : cleaned);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ const out = {};
51
+ for (const k of Object.keys(value)) {
52
+ const cleaned = sanitizeForStorage(value[k], path ? `${path}.${k}` : k, seen, warnings);
53
+ if (cleaned !== undefined) out[k] = cleaned;
54
+ }
55
+ return out;
56
+ }
57
+
58
+ warnings.push(`${path || '<root>'}: unsupported type ${t}`);
59
+ return undefined;
60
+ }
61
+
3
62
  /**
4
63
  * Stores content in the specified holon and lens.
5
64
  * If the target path already contains a hologram, the put operation will be
@@ -142,11 +201,21 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
142
201
 
143
202
  return new Promise((resolve, reject) => {
144
203
  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;
204
+ // Sanitize before serialization so undefined/NaN/Infinity etc.
205
+ // can never produce a malformed payload like `"initiated":,`
206
+ // (which is what causes per-character parse warnings on read).
207
+ const sanitizeWarnings = [];
208
+ let dataToStore = sanitizeForStorage(data, '', new WeakSet(), sanitizeWarnings) || {};
209
+ if (sanitizeWarnings.length > 0) {
210
+ console.warn(
211
+ `holosphere.put: sanitized ${sanitizeWarnings.length} field(s) at ${targetHolon}/${targetLens}/${targetKey} (id=${data.id}):`,
212
+ sanitizeWarnings
213
+ );
149
214
  }
215
+ // Strip read-side envelopes that must never be persisted
216
+ // (they're attached at resolution time).
217
+ if (dataToStore._meta !== undefined) delete dataToStore._meta;
218
+ if (dataToStore._hologram !== undefined) delete dataToStore._hologram;
150
219
  const payload = JSON.stringify(dataToStore); // The data being stored
151
220
 
152
221
  const putCallback = async (ack) => {
@@ -514,8 +583,12 @@ export async function getAll(holoInstance, holon, lens, password = null) {
514
583
  user.get('private').get(lens) :
515
584
  holoInstance.gun.get(holoInstance.appname).get(holon).get(lens);
516
585
 
517
- // PASS 1: Get shallow node to determine expected item count
518
- dataPath.once((data) => {
586
+ // PASS 1: Get shallow node to determine expected item count.
587
+ // Retry once if empty — Gun's .once() reads from local cache, which
588
+ // may be cold immediately after startup before peers have synced.
589
+ const shallowOnce = () => new Promise((res) => dataPath.once((d) => res(d)));
590
+
591
+ const processShallow = (data) => {
519
592
  if (!data) {
520
593
  resolve([]);
521
594
  return;
@@ -537,81 +610,90 @@ export async function getAll(holoInstance, holon, lens, password = null) {
537
610
  return;
538
611
  }
539
612
 
540
- // PASS 2: Use map().once() to iterate and get full item data
541
- let receivedCount = 0;
542
-
543
- dataPath.map().once(async (itemData, key) => {
544
- if (!itemData || key === '_') {
545
- receivedCount++;
546
- if (receivedCount >= expectedCount) {
547
- await Promise.all(pendingProcessing);
548
- resolve(Array.from(output.values()));
549
- }
550
- return;
551
- }
613
+ // PASS 2: iterate explicitly over the filtered keys.
614
+ // Using dataPath.map().once() here is unsafe when the parent
615
+ // node has tombstoned siblings (null values): map() fires for
616
+ // every child, and a null sibling's callback can satisfy
617
+ // receivedCount before real items are processed, resolving [].
618
+ const processItem = async (itemData, key) => {
619
+ if (!itemData) return;
620
+ try {
621
+ const parsed = await holoInstance.parse(itemData);
622
+ if (!parsed || !parsed.id) return;
552
623
 
553
- const processingPromise = (async () => {
554
- try {
555
- const parsed = await holoInstance.parse(itemData);
556
- if (!parsed || !parsed.id) return;
557
-
558
- if (holoInstance.isHologram(parsed)) {
559
- try {
560
- const resolved = await holoInstance.resolveHologram(parsed, {
561
- followHolograms: true,
562
- maxDepth: 10,
563
- currentDepth: 0
564
- });
624
+ if (holoInstance.isHologram(parsed)) {
625
+ try {
626
+ const resolved = await holoInstance.resolveHologram(parsed, {
627
+ followHolograms: true,
628
+ maxDepth: 10,
629
+ currentDepth: 0
630
+ });
565
631
 
566
- if (resolved === null) {
567
- console.warn(`Broken hologram detected in getAll for key ${key}. Removing it...`);
568
- try {
569
- await holoInstance.delete(holon, lens, key, password);
570
- } catch (cleanupError) {
571
- console.error(`Failed to remove broken hologram at ${holon}/${lens}/${key}:`, cleanupError);
572
- }
573
- return;
632
+ if (resolved === null) {
633
+ console.warn(`Broken hologram detected in getAll for key ${key}. Removing it...`);
634
+ try {
635
+ await holoInstance.delete(holon, lens, key, password);
636
+ } catch (cleanupError) {
637
+ console.error(`Failed to remove broken hologram at ${holon}/${lens}/${key}:`, cleanupError);
574
638
  }
639
+ return;
640
+ }
575
641
 
576
- if (resolved && resolved !== parsed) {
577
- if (schema) {
578
- const valid = holoInstance.validator.validate(schema, resolved);
579
- if (valid || !holoInstance.strict) {
580
- output.set(resolved.id, resolved);
581
- }
582
- } else {
642
+ if (resolved && resolved !== parsed) {
643
+ if (schema) {
644
+ const valid = holoInstance.validator.validate(schema, resolved);
645
+ if (valid || !holoInstance.strict) {
583
646
  output.set(resolved.id, resolved);
584
647
  }
585
- return;
648
+ } else {
649
+ output.set(resolved.id, resolved);
586
650
  }
587
- } catch (hologramError) {
588
- console.error(`Error resolving hologram for key ${key}:`, hologramError);
589
651
  return;
590
652
  }
653
+ } catch (hologramError) {
654
+ console.error(`Error resolving hologram for key ${key}:`, hologramError);
655
+ return;
591
656
  }
657
+ }
592
658
 
593
- if (schema) {
594
- const valid = holoInstance.validator.validate(schema, parsed);
595
- if (valid || !holoInstance.strict) {
596
- output.set(parsed.id, parsed);
597
- }
598
- } else {
659
+ if (schema) {
660
+ const valid = holoInstance.validator.validate(schema, parsed);
661
+ if (valid || !holoInstance.strict) {
599
662
  output.set(parsed.id, parsed);
600
663
  }
601
- } catch (error) {
602
- console.error('Error processing data:', error);
664
+ } else {
665
+ output.set(parsed.id, parsed);
603
666
  }
604
- })();
605
-
606
- pendingProcessing.push(processingPromise);
607
- receivedCount++;
667
+ } catch (error) {
668
+ console.error('Error processing data:', error);
669
+ }
670
+ };
608
671
 
609
- if (receivedCount >= expectedCount) {
610
- await Promise.all(pendingProcessing);
611
- resolve(Array.from(output.values()));
672
+ Promise.all(keys.map((key) => {
673
+ const inline = data[key];
674
+ // If shallow already inlined the leaf (holosphere stores
675
+ // payloads as JSON strings), process it directly. This
676
+ // avoids a redundant round-trip and the map() race.
677
+ if (typeof inline !== 'object' || inline === null) {
678
+ return processItem(inline, key);
612
679
  }
613
- });
614
- });
680
+ // Otherwise fetch the leaf — sub-graph reference path.
681
+ return new Promise((resolveItem) => {
682
+ dataPath.get(key).once((itemData) => {
683
+ processItem(itemData, key).then(resolveItem, resolveItem);
684
+ });
685
+ });
686
+ })).then(() => resolve(Array.from(output.values())));
687
+ };
688
+
689
+ (async () => {
690
+ let data = await shallowOnce();
691
+ if (!data) {
692
+ await new Promise(r => setTimeout(r, 1500));
693
+ data = await shallowOnce();
694
+ }
695
+ processShallow(data);
696
+ })();
615
697
  });
616
698
  } catch (error) {
617
699
  console.error('Error in getAll:', error);