holosphere 1.3.0-alpha3 → 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,15 +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, stripping read-side envelopes that
146
- // must never be persisted (they're attached at resolution time).
147
- let dataToStore = { ...data };
148
- if (dataToStore._meta !== undefined) {
149
- delete dataToStore._meta;
150
- }
151
- if (dataToStore._hologram !== undefined) {
152
- delete dataToStore._hologram;
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
+ );
153
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;
154
219
  const payload = JSON.stringify(dataToStore); // The data being stored
155
220
 
156
221
  const putCallback = async (ack) => {
package/federation.js CHANGED
@@ -6,6 +6,35 @@
6
6
  import * as h3 from 'h3-js';
7
7
  import { attachHologramMeta } from './hologram.js';
8
8
 
9
+ /**
10
+ * Look up a holon's display name from its `settings` lens.
11
+ *
12
+ * Returns the `name` field of the holon's settings document, or null if
13
+ * settings are missing or unnamed. Callers should fall back to the bare
14
+ * holon id when this returns null.
15
+ *
16
+ * @param {HoloSphere} holosphere
17
+ * @param {string} space - holon id
18
+ * @returns {Promise<string|null>}
19
+ */
20
+ async function getHolonName(holosphere, space) {
21
+ if (!holosphere || !space) return null;
22
+ try {
23
+ const settings = await holosphere.get(space, 'settings', space);
24
+ if (!settings) return null;
25
+ if (Array.isArray(settings)) {
26
+ const found = settings.find(s => s && typeof s.name === 'string' && s.name.trim() !== '');
27
+ return found ? found.name : null;
28
+ }
29
+ if (typeof settings.name === 'string' && settings.name.trim() !== '') {
30
+ return settings.name;
31
+ }
32
+ return null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
9
38
  /**
10
39
  * Creates a directional federation relationship between two spaces.
11
40
  *
@@ -149,7 +178,18 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
149
178
  const subscriptions = [];
150
179
  let lastNotificationTime = {};
151
180
 
181
+ // Cache partner display names once at setup. Each callback firing for
182
+ // a given partner uses the same name without re-reading settings.
183
+ const partnerNames = new Map();
184
+
152
185
  if (fedInfo.inbound && fedInfo.inbound.length > 0) {
186
+ await Promise.all(
187
+ fedInfo.inbound.map(async space => {
188
+ const name = await getHolonName(holosphere, space);
189
+ partnerNames.set(space, name);
190
+ })
191
+ );
192
+
153
193
  for (const federatedSpace of fedInfo.inbound) {
154
194
  // For each lens specified (or all if '*')
155
195
  for (const lens of lenses) {
@@ -158,27 +198,34 @@ export async function subscribeFederation(holosphere, spaceId, password = null,
158
198
  try {
159
199
  // Skip if data is missing or not from federated space
160
200
  if (!data || !data.id) return;
161
-
201
+
162
202
  // Apply throttling if configured
163
203
  const now = Date.now();
164
204
  const key = `${federatedSpace}_${lens}_${data.id}`;
165
-
205
+
166
206
  if (throttle > 0) {
167
- if (lastNotificationTime[key] &&
207
+ if (lastNotificationTime[key] &&
168
208
  (now - lastNotificationTime[key]) < throttle) {
169
209
  return; // Skip this notification (throttled)
170
210
  }
171
211
  lastNotificationTime[key] = now;
172
212
  }
173
-
174
- // Add federation metadata if not present
175
- if (!data.federation) {
176
- data.federation = {
213
+
214
+ // Add federation metadata if not present.
215
+ // Use the canonical `_federation` envelope so it
216
+ // matches what propagate() and getFederated()
217
+ // produce, and what consumers (UI badges, etc.)
218
+ // already check for.
219
+ if (!data._federation) {
220
+ const partnerName = partnerNames.get(federatedSpace);
221
+ data._federation = {
177
222
  origin: federatedSpace,
178
- timestamp: now
223
+ sourceLens: lens,
224
+ timestamp: now,
225
+ ...(partnerName ? { originName: partnerName } : {})
179
226
  };
180
227
  }
181
-
228
+
182
229
  // Execute callback with the data
183
230
  await callback(data, federatedSpace, lens);
184
231
  } catch (error) {
@@ -476,10 +523,37 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
476
523
  spacesToQuery = spacesToQuery.concat(federatedSpaces);
477
524
  }
478
525
 
526
+ // Resolve display names for federated partner spaces in parallel, so
527
+ // every tagged item can carry the holon's name. Compute once per call.
528
+ const remoteSpaces = spacesToQuery.filter(s => s !== holon);
529
+ const spaceNames = new Map();
530
+ await Promise.all(
531
+ remoteSpaces.map(async space => {
532
+ const name = await getHolonName(holosphere, space);
533
+ spaceNames.set(space, name);
534
+ })
535
+ );
536
+
537
+ // Tag items pulled from a federated partner with the space they came
538
+ // from (and its resolved display name, if any). Local items are left
539
+ // untouched so consumers can distinguish own vs. external by absence
540
+ // or presence of `_federation`.
541
+ const tagWithSource = (item, space) => {
542
+ if (!item || space === holon) return item;
543
+ const originName = spaceNames.get(space);
544
+ const fed = {
545
+ ...(item._federation || {}),
546
+ origin: space,
547
+ sourceLens: lens
548
+ };
549
+ if (originName) fed.originName = originName;
550
+ return { ...item, _federation: fed };
551
+ };
552
+
479
553
  // Fetch data from all relevant spaces
480
554
  for (const currentSpace of spacesToQuery) {
481
555
  if (queryIds && Array.isArray(queryIds)) {
482
- // --- Fetch specific IDs using holosphere.get ---
556
+ // --- Fetch specific IDs using holosphere.get ---
483
557
  console.log(`Fetching specific IDs from ${currentSpace}: ${queryIds.join(', ')}`);
484
558
  for (const itemId of queryIds) {
485
559
  if (fetchedItems.has(itemId)) continue; // Skip if already fetched
@@ -487,7 +561,7 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
487
561
  holosphere.get(currentSpace, lens, itemId)
488
562
  .then(item => {
489
563
  if (item) {
490
- fetchedItems.set(itemId, item);
564
+ fetchedItems.set(itemId, tagWithSource(item, currentSpace));
491
565
  }
492
566
  })
493
567
  .catch(err => console.warn(`Error fetching item ${itemId} from ${currentSpace}: ${err.message}`))
@@ -504,7 +578,7 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
504
578
  .then(items => {
505
579
  for (const item of items) {
506
580
  if (item && item[idField] && !fetchedItems.has(item[idField])) {
507
- fetchedItems.set(item[idField], item);
581
+ fetchedItems.set(item[idField], tagWithSource(item, currentSpace));
508
582
  }
509
583
  }
510
584
  })
@@ -554,7 +628,26 @@ export async function getFederated(holosphere, holon, lens, options = {}) {
554
628
  if (originalData) {
555
629
  // Replace the reference with the resolved data, attaching
556
630
  // the canonical _hologram envelope (single source of truth).
557
- result[i] = attachHologramMeta(originalData, item.soul);
631
+ const withMeta = attachHologramMeta(originalData, item.soul);
632
+ // Stamp the source holon's display name so consumers
633
+ // don't need a second round-trip to render it. Use
634
+ // the per-call cache when possible to avoid duplicate
635
+ // settings reads across many holograms from the same
636
+ // source.
637
+ if (withMeta._hologram?.sourceHolon) {
638
+ let sourceHolonName = spaceNames.get(withMeta._hologram.sourceHolon);
639
+ if (sourceHolonName === undefined) {
640
+ sourceHolonName = await getHolonName(holosphere, withMeta._hologram.sourceHolon);
641
+ spaceNames.set(withMeta._hologram.sourceHolon, sourceHolonName);
642
+ }
643
+ if (sourceHolonName) {
644
+ withMeta._hologram = {
645
+ ...withMeta._hologram,
646
+ sourceHolonName
647
+ };
648
+ }
649
+ }
650
+ result[i] = withMeta;
558
651
  } else {
559
652
  // Original data not found — keep the id so callers can
560
653
  // identify the broken reference, and surface the error.
@@ -732,6 +825,11 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
732
825
  // Check if data is already a hologram
733
826
  const isAlreadyHologram = holosphere.isHologram(data);
734
827
 
828
+ // Resolve our own holon's name once so every propagated
829
+ // payload carries it. Falls back to undefined (the field
830
+ // is omitted) so consumers can use the bare holon id.
831
+ const ownName = await getHolonName(holosphere, holon);
832
+
735
833
  // For each target space, propagate the data
736
834
  const propagatePromises = spaces.map(async (targetSpace) => {
737
835
  try {
@@ -740,7 +838,8 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
740
838
  origin: holon, // The space from which this data is being propagated
741
839
  sourceLens: lens, // The lens from which this data is being propagated
742
840
  propagatedAt: Date.now(),
743
- originalId: data.id
841
+ originalId: data.id,
842
+ ...(ownName ? { originName: ownName } : {})
744
843
  };
745
844
 
746
845
  if (useHolograms && !isAlreadyHologram) {
@@ -847,7 +946,11 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
847
946
 
848
947
  // Check if data is already a hologram (reuse from federation section)
849
948
  const isAlreadyHologram = holosphere.isHologram(data);
850
-
949
+
950
+ // Resolve our own holon's name once for parent propagation
951
+ // (same as the federation block above).
952
+ const ownNameParent = await getHolonName(holosphere, holon);
953
+
851
954
  // Propagate to each parent hexagon
852
955
  const parentPropagatePromises = parentHexagons.map(async (parentHexagon) => {
853
956
  try {
@@ -858,7 +961,8 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
858
961
  propagatedAt: Date.now(),
859
962
  originalId: data.id,
860
963
  propagationType: 'parent', // Indicate this is parent propagation
861
- parentLevel: holonResolution - h3.getResolution(parentHexagon) // How many levels up
964
+ parentLevel: holonResolution - h3.getResolution(parentHexagon), // How many levels up
965
+ ...(ownNameParent ? { originName: ownNameParent } : {})
862
966
  };
863
967
 
864
968
  if (useHolograms && !isAlreadyHologram) {
@@ -1,9 +1,9 @@
1
1
  /**
2
- * HoloSphere ESM Bundle v1.3.0-alpha3
2
+ * HoloSphere ESM Bundle v1.3.0-alpha4
3
3
  * ES6 Module version with all dependencies bundled
4
4
  *
5
5
  * Usage:
6
- * import HoloSphere from 'https://unpkg.com/holosphere@1.3.0-alpha3/holosphere-bundle.esm.js';
6
+ * import HoloSphere from 'https://unpkg.com/holosphere@1.3.0-alpha4/holosphere-bundle.esm.js';
7
7
  * const hs = new HoloSphere('myapp');
8
8
  */
9
9
  var __create = Object.create;
@@ -24322,6 +24322,23 @@ async function resolveHologram(holoInstance, hologram, options = {}) {
24322
24322
  }
24323
24323
 
24324
24324
  // federation.js
24325
+ async function getHolonName(holosphere, space) {
24326
+ if (!holosphere || !space) return null;
24327
+ try {
24328
+ const settings = await holosphere.get(space, "settings", space);
24329
+ if (!settings) return null;
24330
+ if (Array.isArray(settings)) {
24331
+ const found = settings.find((s) => s && typeof s.name === "string" && s.name.trim() !== "");
24332
+ return found ? found.name : null;
24333
+ }
24334
+ if (typeof settings.name === "string" && settings.name.trim() !== "") {
24335
+ return settings.name;
24336
+ }
24337
+ return null;
24338
+ } catch {
24339
+ return null;
24340
+ }
24341
+ }
24325
24342
  async function federate(holosphere, spaceId1, spaceId2, password1 = null, password2 = null, bidirectional = true, lensConfig = {}) {
24326
24343
  if (!spaceId1 || !spaceId2) {
24327
24344
  throw new Error("federate: Missing required space IDs");
@@ -24424,7 +24441,14 @@ async function subscribeFederation(holosphere, spaceId, password = null, callbac
24424
24441
  }
24425
24442
  const subscriptions2 = [];
24426
24443
  let lastNotificationTime = {};
24444
+ const partnerNames = /* @__PURE__ */ new Map();
24427
24445
  if (fedInfo.inbound && fedInfo.inbound.length > 0) {
24446
+ await Promise.all(
24447
+ fedInfo.inbound.map(async (space) => {
24448
+ const name = await getHolonName(holosphere, space);
24449
+ partnerNames.set(space, name);
24450
+ })
24451
+ );
24428
24452
  for (const federatedSpace of fedInfo.inbound) {
24429
24453
  for (const lens of lenses) {
24430
24454
  try {
@@ -24439,10 +24463,13 @@ async function subscribeFederation(holosphere, spaceId, password = null, callbac
24439
24463
  }
24440
24464
  lastNotificationTime[key] = now;
24441
24465
  }
24442
- if (!data.federation) {
24443
- data.federation = {
24466
+ if (!data._federation) {
24467
+ const partnerName = partnerNames.get(federatedSpace);
24468
+ data._federation = {
24444
24469
  origin: federatedSpace,
24445
- timestamp: now
24470
+ sourceLens: lens,
24471
+ timestamp: now,
24472
+ ...partnerName ? { originName: partnerName } : {}
24446
24473
  };
24447
24474
  }
24448
24475
  await callback(data, federatedSpace, lens);
@@ -24635,6 +24662,25 @@ async function getFederated(holosphere, holon, lens, options = {}) {
24635
24662
  const federatedSpaces = maxFederatedSpaces === -1 ? fedInfo.inbound : fedInfo.inbound.slice(0, maxFederatedSpaces);
24636
24663
  spacesToQuery = spacesToQuery.concat(federatedSpaces);
24637
24664
  }
24665
+ const remoteSpaces = spacesToQuery.filter((s) => s !== holon);
24666
+ const spaceNames = /* @__PURE__ */ new Map();
24667
+ await Promise.all(
24668
+ remoteSpaces.map(async (space) => {
24669
+ const name = await getHolonName(holosphere, space);
24670
+ spaceNames.set(space, name);
24671
+ })
24672
+ );
24673
+ const tagWithSource = (item, space) => {
24674
+ if (!item || space === holon) return item;
24675
+ const originName = spaceNames.get(space);
24676
+ const fed = {
24677
+ ...item._federation || {},
24678
+ origin: space,
24679
+ sourceLens: lens
24680
+ };
24681
+ if (originName) fed.originName = originName;
24682
+ return { ...item, _federation: fed };
24683
+ };
24638
24684
  for (const currentSpace of spacesToQuery) {
24639
24685
  if (queryIds && Array.isArray(queryIds)) {
24640
24686
  console.log(`Fetching specific IDs from ${currentSpace}: ${queryIds.join(", ")}`);
@@ -24643,7 +24689,7 @@ async function getFederated(holosphere, holon, lens, options = {}) {
24643
24689
  fetchPromises.push(
24644
24690
  holosphere.get(currentSpace, lens, itemId).then((item) => {
24645
24691
  if (item) {
24646
- fetchedItems.set(itemId, item);
24692
+ fetchedItems.set(itemId, tagWithSource(item, currentSpace));
24647
24693
  }
24648
24694
  }).catch((err) => console.warn(`Error fetching item ${itemId} from ${currentSpace}: ${err.message}`))
24649
24695
  );
@@ -24657,7 +24703,7 @@ async function getFederated(holosphere, holon, lens, options = {}) {
24657
24703
  holosphere.getAll(currentSpace, lens).then((items) => {
24658
24704
  for (const item of items) {
24659
24705
  if (item && item[idField] && !fetchedItems.has(item[idField])) {
24660
- fetchedItems.set(item[idField], item);
24706
+ fetchedItems.set(item[idField], tagWithSource(item, currentSpace));
24661
24707
  }
24662
24708
  }
24663
24709
  }).catch((err) => console.warn(`Error fetching all items from ${currentSpace}: ${err.message}`))
@@ -24689,7 +24735,21 @@ async function getFederated(holosphere, holon, lens, options = {}) {
24689
24735
  );
24690
24736
  console.log(`Original data found via soul path:`, JSON.stringify(originalData));
24691
24737
  if (originalData) {
24692
- result[i] = attachHologramMeta(originalData, item.soul);
24738
+ const withMeta = attachHologramMeta(originalData, item.soul);
24739
+ if (withMeta._hologram?.sourceHolon) {
24740
+ let sourceHolonName = spaceNames.get(withMeta._hologram.sourceHolon);
24741
+ if (sourceHolonName === void 0) {
24742
+ sourceHolonName = await getHolonName(holosphere, withMeta._hologram.sourceHolon);
24743
+ spaceNames.set(withMeta._hologram.sourceHolon, sourceHolonName);
24744
+ }
24745
+ if (sourceHolonName) {
24746
+ withMeta._hologram = {
24747
+ ...withMeta._hologram,
24748
+ sourceHolonName
24749
+ };
24750
+ }
24751
+ }
24752
+ result[i] = withMeta;
24693
24753
  } else {
24694
24754
  result[i] = {
24695
24755
  id: item.id,
@@ -24813,6 +24873,7 @@ async function propagate(holosphere, holon, lens, data, options = {}) {
24813
24873
  });
24814
24874
  if (spaces.length > 0) {
24815
24875
  const isAlreadyHologram = holosphere.isHologram(data);
24876
+ const ownName = await getHolonName(holosphere, holon);
24816
24877
  const propagatePromises = spaces.map(async (targetSpace) => {
24817
24878
  try {
24818
24879
  let payloadToPut;
@@ -24822,7 +24883,8 @@ async function propagate(holosphere, holon, lens, data, options = {}) {
24822
24883
  sourceLens: lens,
24823
24884
  // The lens from which this data is being propagated
24824
24885
  propagatedAt: Date.now(),
24825
- originalId: data.id
24886
+ originalId: data.id,
24887
+ ...ownName ? { originName: ownName } : {}
24826
24888
  };
24827
24889
  if (useHolograms && !isAlreadyHologram) {
24828
24890
  const newHologram = holosphere.createHologram(holon, lens, data);
@@ -24907,6 +24969,7 @@ async function propagate(holosphere, holon, lens, data, options = {}) {
24907
24969
  if (parentHexagons.length > 0) {
24908
24970
  result.parentPropagation.messages.push(`Found ${parentHexagons.length} parent hexagons to propagate to: ${parentHexagons.join(", ")}`);
24909
24971
  const isAlreadyHologram = holosphere.isHologram(data);
24972
+ const ownNameParent = await getHolonName(holosphere, holon);
24910
24973
  const parentPropagatePromises = parentHexagons.map(async (parentHexagon) => {
24911
24974
  try {
24912
24975
  let payloadToPut;
@@ -24919,8 +24982,9 @@ async function propagate(holosphere, holon, lens, data, options = {}) {
24919
24982
  originalId: data.id,
24920
24983
  propagationType: "parent",
24921
24984
  // Indicate this is parent propagation
24922
- parentLevel: holonResolution - getResolution(parentHexagon)
24985
+ parentLevel: holonResolution - getResolution(parentHexagon),
24923
24986
  // How many levels up
24987
+ ...ownNameParent ? { originName: ownNameParent } : {}
24924
24988
  };
24925
24989
  if (useHolograms && !isAlreadyHologram) {
24926
24990
  const newHologram = holosphere.createHologram(holon, lens, data);
@@ -25188,6 +25252,45 @@ function clearSchemaCache(holoInstance, lens = null) {
25188
25252
  }
25189
25253
 
25190
25254
  // content.js
25255
+ function sanitizeForStorage(value, path = "", seen = /* @__PURE__ */ new WeakSet(), warnings = []) {
25256
+ if (value === null) return null;
25257
+ const t = typeof value;
25258
+ if (t === "number") {
25259
+ if (!Number.isFinite(value)) {
25260
+ warnings.push(`${path || "<root>"}: ${value} (non-finite number)`);
25261
+ return void 0;
25262
+ }
25263
+ return value;
25264
+ }
25265
+ if (t === "string" || t === "boolean") return value;
25266
+ if (t === "undefined" || t === "function" || t === "symbol" || t === "bigint") {
25267
+ warnings.push(`${path || "<root>"}: ${t}`);
25268
+ return void 0;
25269
+ }
25270
+ if (t === "object") {
25271
+ if (seen.has(value)) {
25272
+ warnings.push(`${path || "<root>"}: circular reference`);
25273
+ return void 0;
25274
+ }
25275
+ seen.add(value);
25276
+ if (Array.isArray(value)) {
25277
+ const out2 = [];
25278
+ for (let i = 0; i < value.length; i++) {
25279
+ const cleaned = sanitizeForStorage(value[i], `${path}[${i}]`, seen, warnings);
25280
+ out2.push(cleaned === void 0 ? null : cleaned);
25281
+ }
25282
+ return out2;
25283
+ }
25284
+ const out = {};
25285
+ for (const k of Object.keys(value)) {
25286
+ const cleaned = sanitizeForStorage(value[k], path ? `${path}.${k}` : k, seen, warnings);
25287
+ if (cleaned !== void 0) out[k] = cleaned;
25288
+ }
25289
+ return out;
25290
+ }
25291
+ warnings.push(`${path || "<root>"}: unsupported type ${t}`);
25292
+ return void 0;
25293
+ }
25191
25294
  async function put(holoInstance, holon, lens, data, password = null, options = {}) {
25192
25295
  if (!data) {
25193
25296
  throw new Error("put: Missing required data parameter");
@@ -25285,13 +25388,16 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25285
25388
  }
25286
25389
  return new Promise((resolve, reject) => {
25287
25390
  try {
25288
- let dataToStore = { ...data };
25289
- if (dataToStore._meta !== void 0) {
25290
- delete dataToStore._meta;
25291
- }
25292
- if (dataToStore._hologram !== void 0) {
25293
- delete dataToStore._hologram;
25391
+ const sanitizeWarnings = [];
25392
+ let dataToStore = sanitizeForStorage(data, "", /* @__PURE__ */ new WeakSet(), sanitizeWarnings) || {};
25393
+ if (sanitizeWarnings.length > 0) {
25394
+ console.warn(
25395
+ `holosphere.put: sanitized ${sanitizeWarnings.length} field(s) at ${targetHolon}/${targetLens}/${targetKey} (id=${data.id}):`,
25396
+ sanitizeWarnings
25397
+ );
25294
25398
  }
25399
+ if (dataToStore._meta !== void 0) delete dataToStore._meta;
25400
+ if (dataToStore._hologram !== void 0) delete dataToStore._hologram;
25295
25401
  const payload = JSON.stringify(dataToStore);
25296
25402
  const putCallback = async (ack) => {
25297
25403
  if (ack.err) {