holosphere 1.3.0-alpha7 → 1.3.0-alpha9

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
@@ -38,12 +38,41 @@ const WRITE_TIMEOUT_MS = 5000;
38
38
  *
39
39
  * The first responder wins — both branches are idempotent so a late
40
40
  * `.once()` callback after timeout is harmlessly ignored.
41
+ *
42
+ * Pass `{ awaitNetwork: true }` for cold reads that MUST reach the network —
43
+ * notably cross-holon hologram soul resolution. Gun's `.once()` fires
44
+ * synchronously with whatever is in the local graph and never re-fires; for a
45
+ * node we never subscribed to (another holon's lens), that first value is
46
+ * `undefined` even though a peer is about to deliver the real data a few ms
47
+ * later. Accepting that `undefined` as "not found" is exactly what produced
48
+ * the `Could not resolve hologram soul: … (target not present locally)` false
49
+ * negatives on freshly-loaded federated data. In `awaitNetwork` mode we use
50
+ * `.on()` instead, skip the cold `undefined`, and settle once the value
51
+ * syncs — while still honouring an explicit `null` (a real tombstone) and the
52
+ * deadline (for nodes no peer ever answers). The common, same-holon read path
53
+ * keeps the fast `.once()` behaviour so empty/missing local reads resolve
54
+ * immediately instead of blocking for the full deadline.
41
55
  */
42
- function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS) {
56
+ function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS, { awaitNetwork = false } = {}) {
57
+ if (!awaitNetwork) {
58
+ return new Promise((resolve) => {
59
+ let done = false;
60
+ const finish = (v) => { if (!done) { done = true; resolve(v); } };
61
+ node.once((data) => finish(data));
62
+ if (timeoutMs > 0) {
63
+ setTimeout(() => finish(null), timeoutMs);
64
+ }
65
+ });
66
+ }
67
+
43
68
  return new Promise((resolve) => {
44
69
  let done = false;
45
- const finish = (v) => { if (!done) { done = true; resolve(v); } };
46
- node.once((data) => finish(data));
70
+ const detach = () => { try { if (node.off) node.off(); } catch { /* ignore */ } };
71
+ const finish = (v) => { if (!done) { done = true; detach(); resolve(v); } };
72
+ node.on((data) => {
73
+ if (data === undefined) return; // cold/not-known yet — wait for a peer
74
+ finish(data); // defined value OR explicit null tombstone — definitive
75
+ });
47
76
  if (timeoutMs > 0) {
48
77
  setTimeout(() => finish(null), timeoutMs);
49
78
  }
@@ -535,6 +564,11 @@ export async function get(holoInstance, holon, lens, key, password = null, optio
535
564
  validationOptions = {},
536
565
  visited,
537
566
  timeout = READ_TIMEOUT_MS,
567
+ // Network-aware read: skip Gun's cold synchronous `undefined` and wait
568
+ // (up to `timeout`) for a peer to answer. Used by hologram soul
569
+ // resolution so cross-holon reads aren't misreported as missing. See
570
+ // `onceWithTimeout`. Off by default to keep same-holon reads fast.
571
+ awaitNetwork = false,
538
572
  // `_deleted: true` is the soft-tombstone convention used by the bot,
539
573
  // the web dashboard, and the MCP council tools. Pre-this fix the
540
574
  // library was unaware of it and every caller filtered defensively.
@@ -661,7 +695,7 @@ export async function get(holoInstance, holon, lens, key, password = null, optio
661
695
  // `.once()` wrapped in a deadline — cold-path reads (peer offline,
662
696
  // never-written key) used to hang forever otherwise. After
663
697
  // `timeout` ms with no Gun response we treat it as "not found".
664
- onceWithTimeout(dataPath, timeout).then(handleData);
698
+ onceWithTimeout(dataPath, timeout, { awaitNetwork }).then(handleData);
665
699
  });
666
700
  } catch (error) {
667
701
  console.error('Error in get:', error);
package/federation.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as h3 from 'h3-js';
7
- import { attachHologramMeta } from './hologram.js';
7
+ import { attachHologramMeta, parseSoulPath } from './hologram.js';
8
8
 
9
9
  /**
10
10
  * Look up a holon's display name from its `settings` lens.
@@ -887,11 +887,32 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
887
887
  };
888
888
  }
889
889
 
890
- // Defensive: even if the outbound list somehow
891
- // contains the source holon, never write a self-
892
- // hologram. The source already has the original.
893
- if (String(targetSpace) === String(holon)) {
890
+ // Never write a hologram to the holon its soul
891
+ // already points at that overwrites the original
892
+ // record with a pointer to itself (a circular
893
+ // hologram that can never resolve, so get() logs
894
+ // "CIRCULAR … Breaking loop" forever). Two cases:
895
+ // • target === source: the source already holds
896
+ // the original; don't self-hologram it.
897
+ // • forwarding a RECEIVED hologram back toward its
898
+ // birthplace via a third hop (A→B propagates,
899
+ // then B→A federation echoes the hologram — whose
900
+ // soul still points at A — home). The plain
901
+ // `target === holon` check misses this because
902
+ // here `holon` is B, not A.
903
+ const soulTargetHolon =
904
+ payloadToPut && typeof payloadToPut.soul === 'string'
905
+ ? parseSoulPath(payloadToPut.soul)?.holon
906
+ : null;
907
+ if (
908
+ String(targetSpace) === String(holon) ||
909
+ (soulTargetHolon != null &&
910
+ String(soulTargetHolon) === String(targetSpace))
911
+ ) {
894
912
  result.skipped++;
913
+ result.messages.push(
914
+ `Skipped self-referential propagation of ${data.id} to ${targetSpace} (hologram soul already points there).`
915
+ );
895
916
  return true;
896
917
  }
897
918
 
@@ -1018,10 +1039,31 @@ export async function propagate(holosphere, holon, lens, data, options = {}) {
1018
1039
  };
1019
1040
  }
1020
1041
 
1042
+ // Same self-reference guard as partner
1043
+ // propagation: never write a hologram to the
1044
+ // holon its soul points at (would create a
1045
+ // circular self-pointer that can never resolve).
1046
+ const soulTargetHolonParent =
1047
+ payloadToPut && typeof payloadToPut.soul === 'string'
1048
+ ? parseSoulPath(payloadToPut.soul)?.holon
1049
+ : null;
1050
+ if (
1051
+ String(parentHexagon) === String(holon) ||
1052
+ (soulTargetHolonParent != null &&
1053
+ String(soulTargetHolonParent) === String(parentHexagon))
1054
+ ) {
1055
+ result.parentPropagation.skipped =
1056
+ (result.parentPropagation.skipped || 0) + 1;
1057
+ result.parentPropagation.messages.push(
1058
+ `Skipped self-referential parent propagation of ${data.id} to ${parentHexagon} (hologram soul already points there).`
1059
+ );
1060
+ return true;
1061
+ }
1062
+
1021
1063
  // Store in the parent hexagon with redirection disabled and no further auto-propagation
1022
- await holosphere.put(parentHexagon, lens, payloadToPut, null, {
1023
- disableHologramRedirection: true,
1024
- autoPropagate: false
1064
+ await holosphere.put(parentHexagon, lens, payloadToPut, null, {
1065
+ disableHologramRedirection: true,
1066
+ autoPropagate: false
1025
1067
  });
1026
1068
 
1027
1069
  console.log(`[Federation] Successfully propagated to parent hexagon: ${parentHexagon}`);
package/hologram.js CHANGED
@@ -168,7 +168,13 @@ export async function resolveHologram(holoInstance, hologram, options = {}) {
168
168
  resolveHolograms: followHolograms,
169
169
  visited: nextVisited,
170
170
  maxDepth: maxDepth,
171
- currentDepth: currentDepth + 1
171
+ currentDepth: currentDepth + 1,
172
+ // The soul almost always points at a DIFFERENT holon's lens
173
+ // this peer never subscribed to, so the node is cold: Gun's
174
+ // `.once()` would fire `undefined` synchronously and we'd
175
+ // wrongly report "target not present locally". Wait for the
176
+ // network round-trip instead.
177
+ awaitNetwork: true
172
178
  }
173
179
  );
174
180
 
@@ -1,9 +1,9 @@
1
1
  /**
2
- * HoloSphere ESM Bundle v1.3.0-alpha7
2
+ * HoloSphere ESM Bundle v1.3.0-alpha9
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-alpha7/holosphere-bundle.esm.js';
6
+ * import HoloSphere from 'https://unpkg.com/holosphere@1.3.0-alpha9/holosphere-bundle.esm.js';
7
7
  * const hs = new HoloSphere('myapp');
8
8
  */
9
9
  var __create = Object.create;
@@ -24385,7 +24385,13 @@ async function resolveHologram(holoInstance, hologram, options = {}) {
24385
24385
  resolveHolograms: followHolograms,
24386
24386
  visited: nextVisited,
24387
24387
  maxDepth,
24388
- currentDepth: currentDepth + 1
24388
+ currentDepth: currentDepth + 1,
24389
+ // The soul almost always points at a DIFFERENT holon's lens
24390
+ // this peer never subscribed to, so the node is cold: Gun's
24391
+ // `.once()` would fire `undefined` synchronously and we'd
24392
+ // wrongly report "target not present locally". Wait for the
24393
+ // network round-trip instead.
24394
+ awaitNetwork: true
24389
24395
  }
24390
24396
  );
24391
24397
  if (originalData && !originalData._invalidHologram) {
@@ -25012,8 +25018,12 @@ async function propagate(holosphere, holon, lens, data, options = {}) {
25012
25018
  }
25013
25019
  };
25014
25020
  }
25015
- if (String(targetSpace) === String(holon)) {
25021
+ const soulTargetHolon = payloadToPut && typeof payloadToPut.soul === "string" ? parseSoulPath(payloadToPut.soul)?.holon : null;
25022
+ if (String(targetSpace) === String(holon) || soulTargetHolon != null && String(soulTargetHolon) === String(targetSpace)) {
25016
25023
  result.skipped++;
25024
+ result.messages.push(
25025
+ `Skipped self-referential propagation of ${data.id} to ${targetSpace} (hologram soul already points there).`
25026
+ );
25017
25027
  return true;
25018
25028
  }
25019
25029
  await holosphere.put(targetSpace, lens, payloadToPut, null, {
@@ -25116,6 +25126,14 @@ async function propagate(holosphere, holon, lens, data, options = {}) {
25116
25126
  }
25117
25127
  };
25118
25128
  }
25129
+ const soulTargetHolonParent = payloadToPut && typeof payloadToPut.soul === "string" ? parseSoulPath(payloadToPut.soul)?.holon : null;
25130
+ if (String(parentHexagon) === String(holon) || soulTargetHolonParent != null && String(soulTargetHolonParent) === String(parentHexagon)) {
25131
+ result.parentPropagation.skipped = (result.parentPropagation.skipped || 0) + 1;
25132
+ result.parentPropagation.messages.push(
25133
+ `Skipped self-referential parent propagation of ${data.id} to ${parentHexagon} (hologram soul already points there).`
25134
+ );
25135
+ return true;
25136
+ }
25119
25137
  await holosphere.put(parentHexagon, lens, payloadToPut, null, {
25120
25138
  disableHologramRedirection: true,
25121
25139
  autoPropagate: false
@@ -25366,16 +25384,41 @@ function clearSchemaCache(holoInstance, lens = null) {
25366
25384
  // content.js
25367
25385
  var READ_TIMEOUT_MS = 8e3;
25368
25386
  var WRITE_TIMEOUT_MS = 5e3;
25369
- function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS) {
25387
+ function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS, { awaitNetwork = false } = {}) {
25388
+ if (!awaitNetwork) {
25389
+ return new Promise((resolve) => {
25390
+ let done = false;
25391
+ const finish = (v) => {
25392
+ if (!done) {
25393
+ done = true;
25394
+ resolve(v);
25395
+ }
25396
+ };
25397
+ node.once((data) => finish(data));
25398
+ if (timeoutMs > 0) {
25399
+ setTimeout(() => finish(null), timeoutMs);
25400
+ }
25401
+ });
25402
+ }
25370
25403
  return new Promise((resolve) => {
25371
25404
  let done = false;
25405
+ const detach = () => {
25406
+ try {
25407
+ if (node.off) node.off();
25408
+ } catch {
25409
+ }
25410
+ };
25372
25411
  const finish = (v) => {
25373
25412
  if (!done) {
25374
25413
  done = true;
25414
+ detach();
25375
25415
  resolve(v);
25376
25416
  }
25377
25417
  };
25378
- node.once((data) => finish(data));
25418
+ node.on((data) => {
25419
+ if (data === void 0) return;
25420
+ finish(data);
25421
+ });
25379
25422
  if (timeoutMs > 0) {
25380
25423
  setTimeout(() => finish(null), timeoutMs);
25381
25424
  }
@@ -25716,6 +25759,11 @@ async function get(holoInstance, holon, lens, key, password = null, options = {}
25716
25759
  validationOptions = {},
25717
25760
  visited,
25718
25761
  timeout = READ_TIMEOUT_MS,
25762
+ // Network-aware read: skip Gun's cold synchronous `undefined` and wait
25763
+ // (up to `timeout`) for a peer to answer. Used by hologram soul
25764
+ // resolution so cross-holon reads aren't misreported as missing. See
25765
+ // `onceWithTimeout`. Off by default to keep same-holon reads fast.
25766
+ awaitNetwork = false,
25719
25767
  // `_deleted: true` is the soft-tombstone convention used by the bot,
25720
25768
  // the web dashboard, and the MCP council tools. Pre-this fix the
25721
25769
  // library was unaware of it and every caller filtered defensively.
@@ -25801,7 +25849,7 @@ async function get(holoInstance, holon, lens, key, password = null, options = {}
25801
25849
  }
25802
25850
  };
25803
25851
  const dataPath = password ? user.get("private").get(lens).get(key) : holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).get(key);
25804
- onceWithTimeout(dataPath, timeout).then(handleData);
25852
+ onceWithTimeout(dataPath, timeout, { awaitNetwork }).then(handleData);
25805
25853
  });
25806
25854
  } catch (error) {
25807
25855
  console.error("Error in get:", error);
@@ -27234,7 +27282,11 @@ function subscribe(holoInstance, holon, lens, callback) {
27234
27282
  try {
27235
27283
  let parsed = await holoInstance.parse(data);
27236
27284
  if (parsed && holoInstance.isHologram(parsed)) {
27285
+ const hologramSoul = parsed.soul;
27237
27286
  const resolved = await holoInstance.resolveHologram(parsed, { followHolograms: true });
27287
+ if (resolved === null) {
27288
+ console.warn(`Hologram at ${holon}/${lens}/${key} did not resolve (soul=${hologramSoul}); skipping.`);
27289
+ }
27238
27290
  if (resolved !== parsed) {
27239
27291
  parsed = resolved;
27240
27292
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HoloSphere Bundle v1.3.0-alpha7
2
+ * HoloSphere Bundle v1.3.0-alpha9
3
3
  * Holonic Geospatial Communication Infrastructure
4
4
  *
5
5
  * Includes:
@@ -9,7 +9,7 @@
9
9
  * - Ajv (JSON schema validation)
10
10
  *
11
11
  * Usage:
12
- * <script src="https://unpkg.com/holosphere@1.3.0-alpha7/holosphere-bundle.js"></script>
12
+ * <script src="https://unpkg.com/holosphere@1.3.0-alpha9/holosphere-bundle.js"></script>
13
13
  * <script>
14
14
  * const hs = new HoloSphere('myapp');
15
15
  * </script>
@@ -24411,7 +24411,13 @@ var HoloSphere = (() => {
24411
24411
  resolveHolograms: followHolograms,
24412
24412
  visited: nextVisited,
24413
24413
  maxDepth,
24414
- currentDepth: currentDepth + 1
24414
+ currentDepth: currentDepth + 1,
24415
+ // The soul almost always points at a DIFFERENT holon's lens
24416
+ // this peer never subscribed to, so the node is cold: Gun's
24417
+ // `.once()` would fire `undefined` synchronously and we'd
24418
+ // wrongly report "target not present locally". Wait for the
24419
+ // network round-trip instead.
24420
+ awaitNetwork: true
24415
24421
  }
24416
24422
  );
24417
24423
  if (originalData && !originalData._invalidHologram) {
@@ -25038,8 +25044,12 @@ var HoloSphere = (() => {
25038
25044
  }
25039
25045
  };
25040
25046
  }
25041
- if (String(targetSpace) === String(holon)) {
25047
+ const soulTargetHolon = payloadToPut && typeof payloadToPut.soul === "string" ? parseSoulPath(payloadToPut.soul)?.holon : null;
25048
+ if (String(targetSpace) === String(holon) || soulTargetHolon != null && String(soulTargetHolon) === String(targetSpace)) {
25042
25049
  result.skipped++;
25050
+ result.messages.push(
25051
+ `Skipped self-referential propagation of ${data.id} to ${targetSpace} (hologram soul already points there).`
25052
+ );
25043
25053
  return true;
25044
25054
  }
25045
25055
  await holosphere.put(targetSpace, lens, payloadToPut, null, {
@@ -25142,6 +25152,14 @@ var HoloSphere = (() => {
25142
25152
  }
25143
25153
  };
25144
25154
  }
25155
+ const soulTargetHolonParent = payloadToPut && typeof payloadToPut.soul === "string" ? parseSoulPath(payloadToPut.soul)?.holon : null;
25156
+ if (String(parentHexagon) === String(holon) || soulTargetHolonParent != null && String(soulTargetHolonParent) === String(parentHexagon)) {
25157
+ result.parentPropagation.skipped = (result.parentPropagation.skipped || 0) + 1;
25158
+ result.parentPropagation.messages.push(
25159
+ `Skipped self-referential parent propagation of ${data.id} to ${parentHexagon} (hologram soul already points there).`
25160
+ );
25161
+ return true;
25162
+ }
25145
25163
  await holosphere.put(parentHexagon, lens, payloadToPut, null, {
25146
25164
  disableHologramRedirection: true,
25147
25165
  autoPropagate: false
@@ -25392,16 +25410,41 @@ var HoloSphere = (() => {
25392
25410
  // content.js
25393
25411
  var READ_TIMEOUT_MS = 8e3;
25394
25412
  var WRITE_TIMEOUT_MS = 5e3;
25395
- function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS) {
25413
+ function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS, { awaitNetwork = false } = {}) {
25414
+ if (!awaitNetwork) {
25415
+ return new Promise((resolve) => {
25416
+ let done = false;
25417
+ const finish = (v) => {
25418
+ if (!done) {
25419
+ done = true;
25420
+ resolve(v);
25421
+ }
25422
+ };
25423
+ node.once((data) => finish(data));
25424
+ if (timeoutMs > 0) {
25425
+ setTimeout(() => finish(null), timeoutMs);
25426
+ }
25427
+ });
25428
+ }
25396
25429
  return new Promise((resolve) => {
25397
25430
  let done = false;
25431
+ const detach = () => {
25432
+ try {
25433
+ if (node.off) node.off();
25434
+ } catch {
25435
+ }
25436
+ };
25398
25437
  const finish = (v) => {
25399
25438
  if (!done) {
25400
25439
  done = true;
25440
+ detach();
25401
25441
  resolve(v);
25402
25442
  }
25403
25443
  };
25404
- node.once((data) => finish(data));
25444
+ node.on((data) => {
25445
+ if (data === void 0) return;
25446
+ finish(data);
25447
+ });
25405
25448
  if (timeoutMs > 0) {
25406
25449
  setTimeout(() => finish(null), timeoutMs);
25407
25450
  }
@@ -25742,6 +25785,11 @@ var HoloSphere = (() => {
25742
25785
  validationOptions = {},
25743
25786
  visited,
25744
25787
  timeout = READ_TIMEOUT_MS,
25788
+ // Network-aware read: skip Gun's cold synchronous `undefined` and wait
25789
+ // (up to `timeout`) for a peer to answer. Used by hologram soul
25790
+ // resolution so cross-holon reads aren't misreported as missing. See
25791
+ // `onceWithTimeout`. Off by default to keep same-holon reads fast.
25792
+ awaitNetwork = false,
25745
25793
  // `_deleted: true` is the soft-tombstone convention used by the bot,
25746
25794
  // the web dashboard, and the MCP council tools. Pre-this fix the
25747
25795
  // library was unaware of it and every caller filtered defensively.
@@ -25827,7 +25875,7 @@ var HoloSphere = (() => {
25827
25875
  }
25828
25876
  };
25829
25877
  const dataPath = password ? user.get("private").get(lens).get(key) : holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).get(key);
25830
- onceWithTimeout(dataPath, timeout).then(handleData);
25878
+ onceWithTimeout(dataPath, timeout, { awaitNetwork }).then(handleData);
25831
25879
  });
25832
25880
  } catch (error) {
25833
25881
  console.error("Error in get:", error);
@@ -27260,7 +27308,11 @@ var HoloSphere = (() => {
27260
27308
  try {
27261
27309
  let parsed = await holoInstance.parse(data);
27262
27310
  if (parsed && holoInstance.isHologram(parsed)) {
27311
+ const hologramSoul = parsed.soul;
27263
27312
  const resolved = await holoInstance.resolveHologram(parsed, { followHolograms: true });
27313
+ if (resolved === null) {
27314
+ console.warn(`Hologram at ${holon}/${lens}/${key} did not resolve (soul=${hologramSoul}); skipping.`);
27315
+ }
27264
27316
  if (resolved !== parsed) {
27265
27317
  parsed = resolved;
27266
27318
  }