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 +147 -65
- package/federation.js +319 -319
- package/global.js +58 -47
- package/hologram.js +38 -14
- package/holosphere-bundle.esm.js +525 -441
- package/holosphere-bundle.js +525 -441
- package/holosphere-bundle.min.js +8 -8
- package/holosphere.d.ts +50 -8
- package/holosphere.js +24 -19
- package/package.json +1 -1
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
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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:
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
}
|
|
602
|
-
|
|
664
|
+
} else {
|
|
665
|
+
output.set(parsed.id, parsed);
|
|
603
666
|
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error('Error processing data:', error);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
608
671
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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);
|