holosphere 1.3.0-alpha5 → 1.3.0-alpha8

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
@@ -13,6 +13,24 @@
13
13
  */
14
14
  const READ_TIMEOUT_MS = 8000;
15
15
 
16
+ /**
17
+ * Default deadline (ms) for the put-path's Gun ack callback. Gun fires the
18
+ * ack only after the local commit + at least one peer ack chain — with no
19
+ * reachable peer (cold start, offline, partitioned mesh) it never fires
20
+ * and the consumer's `await holosphere.put(...)` hangs forever. UIs
21
+ * worked around this by racing every `put` against their own timeout;
22
+ * owning the deadline here makes every consumer local-first by default.
23
+ *
24
+ * On timeout the returned promise resolves with a `queued: true` sentinel
25
+ * (see `put` return shape) — Gun keeps the write in its local queue and
26
+ * the ack callback's side effects (subscriber notification, hologram
27
+ * cascade, federation propagation) run later when a peer reappears.
28
+ *
29
+ * Pick `WRITE_TIMEOUT_MS = 0` (or pass `{ timeout: 0 }`) to opt out of
30
+ * the fallback and keep the historical "wait for ack" behaviour.
31
+ */
32
+ const WRITE_TIMEOUT_MS = 5000;
33
+
16
34
  /**
17
35
  * `.once()` wrapped in a deadline. Resolves with the value when Gun fires
18
36
  * back, or `null` after `timeoutMs` if it hasn't. Pass `timeoutMs <= 0` to
@@ -20,18 +38,69 @@ const READ_TIMEOUT_MS = 8000;
20
38
  *
21
39
  * The first responder wins — both branches are idempotent so a late
22
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.
23
55
  */
24
- 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
+
25
68
  return new Promise((resolve) => {
26
69
  let done = false;
27
- const finish = (v) => { if (!done) { done = true; resolve(v); } };
28
- 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
+ });
29
76
  if (timeoutMs > 0) {
30
77
  setTimeout(() => finish(null), timeoutMs);
31
78
  }
32
79
  });
33
80
  }
34
81
 
82
+ /**
83
+ * Race a put-result promise against a deadline. If the underlying Gun
84
+ * ack never arrives, resolve with `queuedResult` so the caller stops
85
+ * waiting; Gun keeps the write in its local queue and replays it when
86
+ * a peer is available.
87
+ *
88
+ * The first responder wins — both branches are idempotent so the late
89
+ * ack-driven resolution after timeout is harmlessly ignored.
90
+ */
91
+ function withWriteTimeout(promise, timeoutMs, queuedResult) {
92
+ if (!timeoutMs || timeoutMs <= 0) return promise;
93
+ return new Promise((resolve, reject) => {
94
+ let done = false;
95
+ const finish = (fn, val) => { if (!done) { done = true; fn(val); } };
96
+ promise.then(
97
+ (v) => finish(resolve, v),
98
+ (e) => finish(reject, e)
99
+ );
100
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
101
+ });
102
+ }
103
+
35
104
  /**
36
105
  * Recursively sanitizes a value for storage in GunDB.
37
106
  *
@@ -118,7 +187,11 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
118
187
  throw new Error('put: Missing required holon or lens parameters:', holon, lens);
119
188
  }
120
189
 
121
- const { disableHologramRedirection = false } = options; // Extract new option
190
+ const {
191
+ disableHologramRedirection = false,
192
+ timeout: writeTimeoutOverride
193
+ } = options;
194
+ const writeTimeoutMs = writeTimeoutOverride !== undefined ? writeTimeoutOverride : WRITE_TIMEOUT_MS;
122
195
 
123
196
  let targetHolon = holon;
124
197
  let targetLens = lens;
@@ -231,7 +304,7 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
231
304
  });
232
305
  }
233
306
 
234
- return new Promise((resolve, reject) => {
307
+ const ackPromise = new Promise((resolve, reject) => {
235
308
  try {
236
309
  // Sanitize before serialization so undefined/NaN/Infinity etc.
237
310
  // can never produce a malformed payload like `"initiated":,`
@@ -290,22 +363,38 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
290
363
  }
291
364
  // --- End: Hologram Tracking Logic ---
292
365
 
293
- // --- Start: Active Hologram Update Logic (for actual data being stored) ---
366
+ // --- Start: Active Hologram Update Logic ---
367
+ //
368
+ // Walks this node's `_holograms` set and stamps every
369
+ // registered hologram with `updated: now` so consumers
370
+ // re-resolve and see the latest source data.
371
+ //
372
+ // Runs for BOTH original-data puts and hologram-update
373
+ // puts so updates cascade through multi-hop forwards
374
+ // (A → B → C → …) even when the second hop's
375
+ // cross-holon registration on the original source
376
+ // can't be relied on to make it across the Gun mesh.
377
+ // Each hop maintains its own local `_holograms` set
378
+ // tracking the next hops, and we walk that set on
379
+ // every put — cycle-protected via
380
+ // `options._cascadeVisited`.
294
381
  let updatedHolograms = [];
295
- if (!isHologram && !options.isHologramUpdate) {
382
+ const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
383
+ const cascadeVisited = new Set(options._cascadeVisited || []);
384
+ if (!cascadeVisited.has(currentDataSoul)) {
385
+ cascadeVisited.add(currentDataSoul);
296
386
  try {
297
- const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
298
387
  const currentNodeRef = holoInstance.getNodeRef(currentDataSoul);
299
-
300
- // Get the _holograms set for this data
388
+
389
+ // Get the _holograms set for this node
301
390
  await new Promise((resolveHologramUpdate) => {
302
391
  currentNodeRef.get('_holograms').once(async (hologramsSet) => {
303
392
  if (hologramsSet) {
304
- const hologramSouls = Object.keys(hologramsSet).filter(k =>
305
- k !== '_' && hologramsSet[k] === true // Only active holograms (deleted ones are null/removed)
393
+ const hologramSouls = Object.keys(hologramsSet).filter(k =>
394
+ k !== '_' && hologramsSet[k] === true && !cascadeVisited.has(k)
306
395
  );
307
-
308
- if (hologramSouls.length > 0) {
396
+
397
+ if (hologramSouls.length > 0) {
309
398
  // Update each active hologram with an 'updated' timestamp
310
399
  const updatePromises = hologramSouls.map(async (hologramSoul) => {
311
400
  try {
@@ -319,26 +408,31 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
319
408
  null,
320
409
  { resolveHolograms: false }
321
410
  );
322
-
411
+
323
412
  if (currentHologram) {
324
413
  // Update the hologram with an 'updated' timestamp
325
414
  const updatedHologram = {
326
415
  ...currentHologram,
327
416
  updated: Date.now()
328
417
  };
329
-
418
+
330
419
  await holoInstance.put(
331
420
  hologramSoulInfo.holon,
332
421
  hologramSoulInfo.lens,
333
422
  updatedHologram,
334
423
  null,
335
- {
424
+ {
336
425
  autoPropagate: false, // Don't auto-propagate hologram updates
337
426
  disableHologramRedirection: true, // Prevent redirection when updating holograms
338
- isHologramUpdate: true // Prevent recursive hologram updates
427
+ isHologramUpdate: true,
428
+ // Carry the visited set forward so the
429
+ // recursive put keeps cascading through
430
+ // this hop's `_holograms` set without
431
+ // looping back through us.
432
+ _cascadeVisited: cascadeVisited
339
433
  }
340
434
  );
341
-
435
+
342
436
  // Add to the list of updated holograms
343
437
  updatedHolograms.push({
344
438
  soul: hologramSoul,
@@ -354,7 +448,7 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
354
448
  console.warn(`Error updating hologram ${hologramSoul}:`, hologramUpdateError);
355
449
  }
356
450
  });
357
-
451
+
358
452
  await Promise.all(updatePromises);
359
453
  }
360
454
  }
@@ -424,6 +518,22 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
424
518
  reject(error);
425
519
  }
426
520
  });
521
+
522
+ // Bound the wait on Gun's put ack so an offline/partitioned mesh
523
+ // doesn't hang the caller forever. Gun keeps the local write and
524
+ // its eventual ack callback (subscriber notify, hologram cascade,
525
+ // propagation) still runs when a peer reappears — we just stop
526
+ // blocking the awaiting consumer in the meantime.
527
+ return withWriteTimeout(ackPromise, writeTimeoutMs, {
528
+ success: true,
529
+ queued: true,
530
+ isHologramAtPath: isHologram,
531
+ pathHolon: targetHolon,
532
+ pathLens: targetLens,
533
+ pathKey: targetKey,
534
+ propagationResult: null,
535
+ updatedHolograms: []
536
+ });
427
537
  } catch (error) {
428
538
  console.error('Error in put:', error);
429
539
  throw error;
@@ -454,6 +564,11 @@ export async function get(holoInstance, holon, lens, key, password = null, optio
454
564
  validationOptions = {},
455
565
  visited,
456
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,
457
572
  // `_deleted: true` is the soft-tombstone convention used by the bot,
458
573
  // the web dashboard, and the MCP council tools. Pre-this fix the
459
574
  // library was unaware of it and every caller filtered defensively.
@@ -580,7 +695,7 @@ export async function get(holoInstance, holon, lens, key, password = null, optio
580
695
  // `.once()` wrapped in a deadline — cold-path reads (peer offline,
581
696
  // never-written key) used to hang forever otherwise. After
582
697
  // `timeout` ms with no Gun response we treat it as "not found".
583
- onceWithTimeout(dataPath, timeout).then(handleData);
698
+ onceWithTimeout(dataPath, timeout, { awaitNetwork }).then(handleData);
584
699
  });
585
700
  } catch (error) {
586
701
  console.error('Error in get:', error);
package/global.js CHANGED
@@ -1,14 +1,37 @@
1
1
  // holo_global.js
2
2
 
3
+ /**
4
+ * Default deadline (ms) for the put-path's Gun ack callback. See
5
+ * `WRITE_TIMEOUT_MS` in content.js for the rationale — same hang, same
6
+ * fix, kept local here so global.js doesn't depend on content.js's
7
+ * internals.
8
+ */
9
+ const WRITE_TIMEOUT_MS = 5000;
10
+
11
+ function withWriteTimeout(promise, timeoutMs, queuedResult) {
12
+ if (!timeoutMs || timeoutMs <= 0) return promise;
13
+ return new Promise((resolve, reject) => {
14
+ let done = false;
15
+ const finish = (fn, val) => { if (!done) { done = true; fn(val); } };
16
+ promise.then(
17
+ (v) => finish(resolve, v),
18
+ (e) => finish(reject, e)
19
+ );
20
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
21
+ });
22
+ }
23
+
3
24
  /**
4
25
  * Stores data in a global (non-holon-specific) table.
5
26
  * @param {HoloSphere} holoInstance - The HoloSphere instance.
6
27
  * @param {string} tableName - The table name to store data in.
7
28
  * @param {object} data - The data to store. If it has an 'id' field, it will be used as the key.
8
29
  * @param {string} [password] - Optional password for private holon.
30
+ * @param {object} [options] - Additional options
31
+ * @param {number} [options.timeout=5000] - Ack deadline in ms; resolves anyway after this so an offline mesh can't hang the caller. Pass `0` to disable.
9
32
  * @returns {Promise<void>}
10
33
  */
11
- export async function putGlobal(holoInstance, tableName, data, password = null) {
34
+ export async function putGlobal(holoInstance, tableName, data, password = null, options = {}) {
12
35
  try {
13
36
  if (!tableName || !data) {
14
37
  throw new Error('Table name and data are required');
@@ -63,7 +86,9 @@ export async function putGlobal(holoInstance, tableName, data, password = null)
63
86
  });
64
87
  }
65
88
 
66
- return new Promise((resolve, reject) => {
89
+ const writeTimeoutMs = options.timeout !== undefined ? options.timeout : WRITE_TIMEOUT_MS;
90
+
91
+ const ackPromise = new Promise((resolve, reject) => {
67
92
  try {
68
93
  // Create a copy of data, stripping read-side envelopes that
69
94
  // must never be persisted (they're attached at resolution time).
@@ -143,6 +168,23 @@ export async function putGlobal(holoInstance, tableName, data, password = null)
143
168
  reject(error);
144
169
  }
145
170
  });
171
+
172
+ // Bound the wait on Gun's put ack so an offline mesh doesn't hang
173
+ // the caller forever. Gun keeps the write locally and replays it
174
+ // when a peer reappears. We tag the ack-arrived branch with a
175
+ // unique sentinel so we can warn (once) when the timeout fired
176
+ // and still keep the public Promise<void> contract.
177
+ const ACK_OK = Symbol('ackOk');
178
+ return withWriteTimeout(
179
+ ackPromise.then(() => ACK_OK),
180
+ writeTimeoutMs,
181
+ undefined
182
+ ).then((result) => {
183
+ if (result !== ACK_OK) {
184
+ console.warn(`putGlobal: no ack within ${writeTimeoutMs}ms for table=${tableName} — write queued locally, will replay on reconnect`);
185
+ }
186
+ return undefined;
187
+ });
146
188
  } catch (error) {
147
189
  console.error('Error in putGlobal:', error);
148
190
  throw error;
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-alpha5
2
+ * HoloSphere ESM Bundle v1.3.0-alpha8
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-alpha5/holosphere-bundle.esm.js';
6
+ * import HoloSphere from 'https://unpkg.com/holosphere@1.3.0-alpha8/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) {
@@ -25365,21 +25371,64 @@ function clearSchemaCache(holoInstance, lens = null) {
25365
25371
 
25366
25372
  // content.js
25367
25373
  var READ_TIMEOUT_MS = 8e3;
25368
- function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS) {
25374
+ var WRITE_TIMEOUT_MS = 5e3;
25375
+ function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS, { awaitNetwork = false } = {}) {
25376
+ if (!awaitNetwork) {
25377
+ return new Promise((resolve) => {
25378
+ let done = false;
25379
+ const finish = (v) => {
25380
+ if (!done) {
25381
+ done = true;
25382
+ resolve(v);
25383
+ }
25384
+ };
25385
+ node.once((data) => finish(data));
25386
+ if (timeoutMs > 0) {
25387
+ setTimeout(() => finish(null), timeoutMs);
25388
+ }
25389
+ });
25390
+ }
25369
25391
  return new Promise((resolve) => {
25370
25392
  let done = false;
25393
+ const detach = () => {
25394
+ try {
25395
+ if (node.off) node.off();
25396
+ } catch {
25397
+ }
25398
+ };
25371
25399
  const finish = (v) => {
25372
25400
  if (!done) {
25373
25401
  done = true;
25402
+ detach();
25374
25403
  resolve(v);
25375
25404
  }
25376
25405
  };
25377
- node.once((data) => finish(data));
25406
+ node.on((data) => {
25407
+ if (data === void 0) return;
25408
+ finish(data);
25409
+ });
25378
25410
  if (timeoutMs > 0) {
25379
25411
  setTimeout(() => finish(null), timeoutMs);
25380
25412
  }
25381
25413
  });
25382
25414
  }
25415
+ function withWriteTimeout(promise, timeoutMs, queuedResult) {
25416
+ if (!timeoutMs || timeoutMs <= 0) return promise;
25417
+ return new Promise((resolve, reject) => {
25418
+ let done = false;
25419
+ const finish = (fn, val) => {
25420
+ if (!done) {
25421
+ done = true;
25422
+ fn(val);
25423
+ }
25424
+ };
25425
+ promise.then(
25426
+ (v) => finish(resolve, v),
25427
+ (e) => finish(reject, e)
25428
+ );
25429
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
25430
+ });
25431
+ }
25383
25432
  function sanitizeForStorage(value, path = "", seen = /* @__PURE__ */ new WeakSet(), warnings = []) {
25384
25433
  if (value === null) return null;
25385
25434
  const t = typeof value;
@@ -25426,7 +25475,11 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25426
25475
  if (!holon || !lens) {
25427
25476
  throw new Error("put: Missing required holon or lens parameters:", holon, lens);
25428
25477
  }
25429
- const { disableHologramRedirection = false } = options;
25478
+ const {
25479
+ disableHologramRedirection = false,
25480
+ timeout: writeTimeoutOverride
25481
+ } = options;
25482
+ const writeTimeoutMs = writeTimeoutOverride !== void 0 ? writeTimeoutOverride : WRITE_TIMEOUT_MS;
25430
25483
  let targetHolon = holon;
25431
25484
  let targetLens = lens;
25432
25485
  let targetKey = data.id;
@@ -25514,7 +25567,7 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25514
25567
  });
25515
25568
  });
25516
25569
  }
25517
- return new Promise((resolve, reject) => {
25570
+ const ackPromise = new Promise((resolve, reject) => {
25518
25571
  try {
25519
25572
  const sanitizeWarnings = [];
25520
25573
  let dataToStore = sanitizeForStorage(data, "", /* @__PURE__ */ new WeakSet(), sanitizeWarnings) || {};
@@ -25547,16 +25600,17 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25547
25600
  }
25548
25601
  }
25549
25602
  let updatedHolograms = [];
25550
- if (!isHologram2 && !options.isHologramUpdate) {
25603
+ const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
25604
+ const cascadeVisited = new Set(options._cascadeVisited || []);
25605
+ if (!cascadeVisited.has(currentDataSoul)) {
25606
+ cascadeVisited.add(currentDataSoul);
25551
25607
  try {
25552
- const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
25553
25608
  const currentNodeRef = holoInstance.getNodeRef(currentDataSoul);
25554
25609
  await new Promise((resolveHologramUpdate) => {
25555
25610
  currentNodeRef.get("_holograms").once(async (hologramsSet) => {
25556
25611
  if (hologramsSet) {
25557
25612
  const hologramSouls = Object.keys(hologramsSet).filter(
25558
- (k) => k !== "_" && hologramsSet[k] === true
25559
- // Only active holograms (deleted ones are null/removed)
25613
+ (k) => k !== "_" && hologramsSet[k] === true && !cascadeVisited.has(k)
25560
25614
  );
25561
25615
  if (hologramSouls.length > 0) {
25562
25616
  const updatePromises = hologramSouls.map(async (hologramSoul) => {
@@ -25585,8 +25639,12 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25585
25639
  // Don't auto-propagate hologram updates
25586
25640
  disableHologramRedirection: true,
25587
25641
  // Prevent redirection when updating holograms
25588
- isHologramUpdate: true
25589
- // Prevent recursive hologram updates
25642
+ isHologramUpdate: true,
25643
+ // Carry the visited set forward so the
25644
+ // recursive put keeps cascading through
25645
+ // this hop's `_holograms` set without
25646
+ // looping back through us.
25647
+ _cascadeVisited: cascadeVisited
25590
25648
  }
25591
25649
  );
25592
25650
  updatedHolograms.push({
@@ -25664,6 +25722,16 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25664
25722
  reject(error);
25665
25723
  }
25666
25724
  });
25725
+ return withWriteTimeout(ackPromise, writeTimeoutMs, {
25726
+ success: true,
25727
+ queued: true,
25728
+ isHologramAtPath: isHologram2,
25729
+ pathHolon: targetHolon,
25730
+ pathLens: targetLens,
25731
+ pathKey: targetKey,
25732
+ propagationResult: null,
25733
+ updatedHolograms: []
25734
+ });
25667
25735
  } catch (error) {
25668
25736
  console.error("Error in put:", error);
25669
25737
  throw error;
@@ -25679,6 +25747,11 @@ async function get(holoInstance, holon, lens, key, password = null, options = {}
25679
25747
  validationOptions = {},
25680
25748
  visited,
25681
25749
  timeout = READ_TIMEOUT_MS,
25750
+ // Network-aware read: skip Gun's cold synchronous `undefined` and wait
25751
+ // (up to `timeout`) for a peer to answer. Used by hologram soul
25752
+ // resolution so cross-holon reads aren't misreported as missing. See
25753
+ // `onceWithTimeout`. Off by default to keep same-holon reads fast.
25754
+ awaitNetwork = false,
25682
25755
  // `_deleted: true` is the soft-tombstone convention used by the bot,
25683
25756
  // the web dashboard, and the MCP council tools. Pre-this fix the
25684
25757
  // library was unaware of it and every caller filtered defensively.
@@ -25764,7 +25837,7 @@ async function get(holoInstance, holon, lens, key, password = null, options = {}
25764
25837
  }
25765
25838
  };
25766
25839
  const dataPath = password ? user.get("private").get(lens).get(key) : holoInstance.gun.get(holoInstance.appname).get(holon).get(lens).get(key);
25767
- onceWithTimeout(dataPath, timeout).then(handleData);
25840
+ onceWithTimeout(dataPath, timeout, { awaitNetwork }).then(handleData);
25768
25841
  });
25769
25842
  } catch (error) {
25770
25843
  console.error("Error in get:", error);
@@ -26336,7 +26409,25 @@ async function deleteNode(holoInstance, holon, lens, key) {
26336
26409
  }
26337
26410
 
26338
26411
  // global.js
26339
- async function putGlobal(holoInstance, tableName, data, password = null) {
26412
+ var WRITE_TIMEOUT_MS2 = 5e3;
26413
+ function withWriteTimeout2(promise, timeoutMs, queuedResult) {
26414
+ if (!timeoutMs || timeoutMs <= 0) return promise;
26415
+ return new Promise((resolve, reject) => {
26416
+ let done = false;
26417
+ const finish = (fn, val) => {
26418
+ if (!done) {
26419
+ done = true;
26420
+ fn(val);
26421
+ }
26422
+ };
26423
+ promise.then(
26424
+ (v) => finish(resolve, v),
26425
+ (e) => finish(reject, e)
26426
+ );
26427
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
26428
+ });
26429
+ }
26430
+ async function putGlobal(holoInstance, tableName, data, password = null, options = {}) {
26340
26431
  try {
26341
26432
  if (!tableName || !data) {
26342
26433
  throw new Error("Table name and data are required");
@@ -26383,7 +26474,8 @@ async function putGlobal(holoInstance, tableName, data, password = null) {
26383
26474
  });
26384
26475
  });
26385
26476
  }
26386
- return new Promise((resolve, reject) => {
26477
+ const writeTimeoutMs = options.timeout !== void 0 ? options.timeout : WRITE_TIMEOUT_MS2;
26478
+ const ackPromise = new Promise((resolve, reject) => {
26387
26479
  try {
26388
26480
  let dataToStore = { ...data };
26389
26481
  if (dataToStore._meta !== void 0) {
@@ -26445,6 +26537,17 @@ async function putGlobal(holoInstance, tableName, data, password = null) {
26445
26537
  reject(error);
26446
26538
  }
26447
26539
  });
26540
+ const ACK_OK = Symbol("ackOk");
26541
+ return withWriteTimeout2(
26542
+ ackPromise.then(() => ACK_OK),
26543
+ writeTimeoutMs,
26544
+ void 0
26545
+ ).then((result) => {
26546
+ if (result !== ACK_OK) {
26547
+ console.warn(`putGlobal: no ack within ${writeTimeoutMs}ms for table=${tableName} \u2014 write queued locally, will replay on reconnect`);
26548
+ }
26549
+ return void 0;
26550
+ });
26448
26551
  } catch (error) {
26449
26552
  console.error("Error in putGlobal:", error);
26450
26553
  throw error;
@@ -27167,7 +27270,11 @@ function subscribe(holoInstance, holon, lens, callback) {
27167
27270
  try {
27168
27271
  let parsed = await holoInstance.parse(data);
27169
27272
  if (parsed && holoInstance.isHologram(parsed)) {
27273
+ const hologramSoul = parsed.soul;
27170
27274
  const resolved = await holoInstance.resolveHologram(parsed, { followHolograms: true });
27275
+ if (resolved === null) {
27276
+ console.warn(`Hologram at ${holon}/${lens}/${key} did not resolve (soul=${hologramSoul}); skipping.`);
27277
+ }
27171
27278
  if (resolved !== parsed) {
27172
27279
  parsed = resolved;
27173
27280
  }
@@ -30528,14 +30635,14 @@ var HoloSphere = class {
30528
30635
  return deleteNode(this, holon, lens, key);
30529
30636
  }
30530
30637
  // ================================ GLOBAL FUNCTIONS ================================
30531
- async putGlobal(tableName, data, password = null) {
30532
- return putGlobal(this, tableName, data, password);
30638
+ async putGlobal(tableName, data, password = null, options = {}) {
30639
+ return putGlobal(this, tableName, data, password, options);
30533
30640
  }
30534
30641
  /**
30535
30642
  * v2-compatible alias for putGlobal (no password param)
30536
30643
  */
30537
- async writeGlobal(tableName, data) {
30538
- return putGlobal(this, tableName, data, null);
30644
+ async writeGlobal(tableName, data, options = {}) {
30645
+ return putGlobal(this, tableName, data, null, options);
30539
30646
  }
30540
30647
  async getGlobal(tableName, key, password = null) {
30541
30648
  return getGlobal(this, tableName, key, password);