holosphere 1.3.0-alpha5 → 1.3.0-alpha7

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
@@ -32,6 +50,28 @@ function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS) {
32
50
  });
33
51
  }
34
52
 
53
+ /**
54
+ * Race a put-result promise against a deadline. If the underlying Gun
55
+ * ack never arrives, resolve with `queuedResult` so the caller stops
56
+ * waiting; Gun keeps the write in its local queue and replays it when
57
+ * a peer is available.
58
+ *
59
+ * The first responder wins — both branches are idempotent so the late
60
+ * ack-driven resolution after timeout is harmlessly ignored.
61
+ */
62
+ function withWriteTimeout(promise, timeoutMs, queuedResult) {
63
+ if (!timeoutMs || timeoutMs <= 0) return promise;
64
+ return new Promise((resolve, reject) => {
65
+ let done = false;
66
+ const finish = (fn, val) => { if (!done) { done = true; fn(val); } };
67
+ promise.then(
68
+ (v) => finish(resolve, v),
69
+ (e) => finish(reject, e)
70
+ );
71
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
72
+ });
73
+ }
74
+
35
75
  /**
36
76
  * Recursively sanitizes a value for storage in GunDB.
37
77
  *
@@ -118,7 +158,11 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
118
158
  throw new Error('put: Missing required holon or lens parameters:', holon, lens);
119
159
  }
120
160
 
121
- const { disableHologramRedirection = false } = options; // Extract new option
161
+ const {
162
+ disableHologramRedirection = false,
163
+ timeout: writeTimeoutOverride
164
+ } = options;
165
+ const writeTimeoutMs = writeTimeoutOverride !== undefined ? writeTimeoutOverride : WRITE_TIMEOUT_MS;
122
166
 
123
167
  let targetHolon = holon;
124
168
  let targetLens = lens;
@@ -231,7 +275,7 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
231
275
  });
232
276
  }
233
277
 
234
- return new Promise((resolve, reject) => {
278
+ const ackPromise = new Promise((resolve, reject) => {
235
279
  try {
236
280
  // Sanitize before serialization so undefined/NaN/Infinity etc.
237
281
  // can never produce a malformed payload like `"initiated":,`
@@ -290,22 +334,38 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
290
334
  }
291
335
  // --- End: Hologram Tracking Logic ---
292
336
 
293
- // --- Start: Active Hologram Update Logic (for actual data being stored) ---
337
+ // --- Start: Active Hologram Update Logic ---
338
+ //
339
+ // Walks this node's `_holograms` set and stamps every
340
+ // registered hologram with `updated: now` so consumers
341
+ // re-resolve and see the latest source data.
342
+ //
343
+ // Runs for BOTH original-data puts and hologram-update
344
+ // puts so updates cascade through multi-hop forwards
345
+ // (A → B → C → …) even when the second hop's
346
+ // cross-holon registration on the original source
347
+ // can't be relied on to make it across the Gun mesh.
348
+ // Each hop maintains its own local `_holograms` set
349
+ // tracking the next hops, and we walk that set on
350
+ // every put — cycle-protected via
351
+ // `options._cascadeVisited`.
294
352
  let updatedHolograms = [];
295
- if (!isHologram && !options.isHologramUpdate) {
353
+ const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
354
+ const cascadeVisited = new Set(options._cascadeVisited || []);
355
+ if (!cascadeVisited.has(currentDataSoul)) {
356
+ cascadeVisited.add(currentDataSoul);
296
357
  try {
297
- const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
298
358
  const currentNodeRef = holoInstance.getNodeRef(currentDataSoul);
299
-
300
- // Get the _holograms set for this data
359
+
360
+ // Get the _holograms set for this node
301
361
  await new Promise((resolveHologramUpdate) => {
302
362
  currentNodeRef.get('_holograms').once(async (hologramsSet) => {
303
363
  if (hologramsSet) {
304
- const hologramSouls = Object.keys(hologramsSet).filter(k =>
305
- k !== '_' && hologramsSet[k] === true // Only active holograms (deleted ones are null/removed)
364
+ const hologramSouls = Object.keys(hologramsSet).filter(k =>
365
+ k !== '_' && hologramsSet[k] === true && !cascadeVisited.has(k)
306
366
  );
307
-
308
- if (hologramSouls.length > 0) {
367
+
368
+ if (hologramSouls.length > 0) {
309
369
  // Update each active hologram with an 'updated' timestamp
310
370
  const updatePromises = hologramSouls.map(async (hologramSoul) => {
311
371
  try {
@@ -319,26 +379,31 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
319
379
  null,
320
380
  { resolveHolograms: false }
321
381
  );
322
-
382
+
323
383
  if (currentHologram) {
324
384
  // Update the hologram with an 'updated' timestamp
325
385
  const updatedHologram = {
326
386
  ...currentHologram,
327
387
  updated: Date.now()
328
388
  };
329
-
389
+
330
390
  await holoInstance.put(
331
391
  hologramSoulInfo.holon,
332
392
  hologramSoulInfo.lens,
333
393
  updatedHologram,
334
394
  null,
335
- {
395
+ {
336
396
  autoPropagate: false, // Don't auto-propagate hologram updates
337
397
  disableHologramRedirection: true, // Prevent redirection when updating holograms
338
- isHologramUpdate: true // Prevent recursive hologram updates
398
+ isHologramUpdate: true,
399
+ // Carry the visited set forward so the
400
+ // recursive put keeps cascading through
401
+ // this hop's `_holograms` set without
402
+ // looping back through us.
403
+ _cascadeVisited: cascadeVisited
339
404
  }
340
405
  );
341
-
406
+
342
407
  // Add to the list of updated holograms
343
408
  updatedHolograms.push({
344
409
  soul: hologramSoul,
@@ -354,7 +419,7 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
354
419
  console.warn(`Error updating hologram ${hologramSoul}:`, hologramUpdateError);
355
420
  }
356
421
  });
357
-
422
+
358
423
  await Promise.all(updatePromises);
359
424
  }
360
425
  }
@@ -424,6 +489,22 @@ export async function put(holoInstance, holon, lens, data, password = null, opti
424
489
  reject(error);
425
490
  }
426
491
  });
492
+
493
+ // Bound the wait on Gun's put ack so an offline/partitioned mesh
494
+ // doesn't hang the caller forever. Gun keeps the local write and
495
+ // its eventual ack callback (subscriber notify, hologram cascade,
496
+ // propagation) still runs when a peer reappears — we just stop
497
+ // blocking the awaiting consumer in the meantime.
498
+ return withWriteTimeout(ackPromise, writeTimeoutMs, {
499
+ success: true,
500
+ queued: true,
501
+ isHologramAtPath: isHologram,
502
+ pathHolon: targetHolon,
503
+ pathLens: targetLens,
504
+ pathKey: targetKey,
505
+ propagationResult: null,
506
+ updatedHolograms: []
507
+ });
427
508
  } catch (error) {
428
509
  console.error('Error in put:', error);
429
510
  throw 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;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * HoloSphere ESM Bundle v1.3.0-alpha5
2
+ * HoloSphere ESM Bundle v1.3.0-alpha7
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-alpha7/holosphere-bundle.esm.js';
7
7
  * const hs = new HoloSphere('myapp');
8
8
  */
9
9
  var __create = Object.create;
@@ -25365,6 +25365,7 @@ function clearSchemaCache(holoInstance, lens = null) {
25365
25365
 
25366
25366
  // content.js
25367
25367
  var READ_TIMEOUT_MS = 8e3;
25368
+ var WRITE_TIMEOUT_MS = 5e3;
25368
25369
  function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS) {
25369
25370
  return new Promise((resolve) => {
25370
25371
  let done = false;
@@ -25380,6 +25381,23 @@ function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS) {
25380
25381
  }
25381
25382
  });
25382
25383
  }
25384
+ function withWriteTimeout(promise, timeoutMs, queuedResult) {
25385
+ if (!timeoutMs || timeoutMs <= 0) return promise;
25386
+ return new Promise((resolve, reject) => {
25387
+ let done = false;
25388
+ const finish = (fn, val) => {
25389
+ if (!done) {
25390
+ done = true;
25391
+ fn(val);
25392
+ }
25393
+ };
25394
+ promise.then(
25395
+ (v) => finish(resolve, v),
25396
+ (e) => finish(reject, e)
25397
+ );
25398
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
25399
+ });
25400
+ }
25383
25401
  function sanitizeForStorage(value, path = "", seen = /* @__PURE__ */ new WeakSet(), warnings = []) {
25384
25402
  if (value === null) return null;
25385
25403
  const t = typeof value;
@@ -25426,7 +25444,11 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25426
25444
  if (!holon || !lens) {
25427
25445
  throw new Error("put: Missing required holon or lens parameters:", holon, lens);
25428
25446
  }
25429
- const { disableHologramRedirection = false } = options;
25447
+ const {
25448
+ disableHologramRedirection = false,
25449
+ timeout: writeTimeoutOverride
25450
+ } = options;
25451
+ const writeTimeoutMs = writeTimeoutOverride !== void 0 ? writeTimeoutOverride : WRITE_TIMEOUT_MS;
25430
25452
  let targetHolon = holon;
25431
25453
  let targetLens = lens;
25432
25454
  let targetKey = data.id;
@@ -25514,7 +25536,7 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25514
25536
  });
25515
25537
  });
25516
25538
  }
25517
- return new Promise((resolve, reject) => {
25539
+ const ackPromise = new Promise((resolve, reject) => {
25518
25540
  try {
25519
25541
  const sanitizeWarnings = [];
25520
25542
  let dataToStore = sanitizeForStorage(data, "", /* @__PURE__ */ new WeakSet(), sanitizeWarnings) || {};
@@ -25547,16 +25569,17 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25547
25569
  }
25548
25570
  }
25549
25571
  let updatedHolograms = [];
25550
- if (!isHologram2 && !options.isHologramUpdate) {
25572
+ const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
25573
+ const cascadeVisited = new Set(options._cascadeVisited || []);
25574
+ if (!cascadeVisited.has(currentDataSoul)) {
25575
+ cascadeVisited.add(currentDataSoul);
25551
25576
  try {
25552
- const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
25553
25577
  const currentNodeRef = holoInstance.getNodeRef(currentDataSoul);
25554
25578
  await new Promise((resolveHologramUpdate) => {
25555
25579
  currentNodeRef.get("_holograms").once(async (hologramsSet) => {
25556
25580
  if (hologramsSet) {
25557
25581
  const hologramSouls = Object.keys(hologramsSet).filter(
25558
- (k) => k !== "_" && hologramsSet[k] === true
25559
- // Only active holograms (deleted ones are null/removed)
25582
+ (k) => k !== "_" && hologramsSet[k] === true && !cascadeVisited.has(k)
25560
25583
  );
25561
25584
  if (hologramSouls.length > 0) {
25562
25585
  const updatePromises = hologramSouls.map(async (hologramSoul) => {
@@ -25585,8 +25608,12 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25585
25608
  // Don't auto-propagate hologram updates
25586
25609
  disableHologramRedirection: true,
25587
25610
  // Prevent redirection when updating holograms
25588
- isHologramUpdate: true
25589
- // Prevent recursive hologram updates
25611
+ isHologramUpdate: true,
25612
+ // Carry the visited set forward so the
25613
+ // recursive put keeps cascading through
25614
+ // this hop's `_holograms` set without
25615
+ // looping back through us.
25616
+ _cascadeVisited: cascadeVisited
25590
25617
  }
25591
25618
  );
25592
25619
  updatedHolograms.push({
@@ -25664,6 +25691,16 @@ async function put(holoInstance, holon, lens, data, password = null, options = {
25664
25691
  reject(error);
25665
25692
  }
25666
25693
  });
25694
+ return withWriteTimeout(ackPromise, writeTimeoutMs, {
25695
+ success: true,
25696
+ queued: true,
25697
+ isHologramAtPath: isHologram2,
25698
+ pathHolon: targetHolon,
25699
+ pathLens: targetLens,
25700
+ pathKey: targetKey,
25701
+ propagationResult: null,
25702
+ updatedHolograms: []
25703
+ });
25667
25704
  } catch (error) {
25668
25705
  console.error("Error in put:", error);
25669
25706
  throw error;
@@ -26336,7 +26373,25 @@ async function deleteNode(holoInstance, holon, lens, key) {
26336
26373
  }
26337
26374
 
26338
26375
  // global.js
26339
- async function putGlobal(holoInstance, tableName, data, password = null) {
26376
+ var WRITE_TIMEOUT_MS2 = 5e3;
26377
+ function withWriteTimeout2(promise, timeoutMs, queuedResult) {
26378
+ if (!timeoutMs || timeoutMs <= 0) return promise;
26379
+ return new Promise((resolve, reject) => {
26380
+ let done = false;
26381
+ const finish = (fn, val) => {
26382
+ if (!done) {
26383
+ done = true;
26384
+ fn(val);
26385
+ }
26386
+ };
26387
+ promise.then(
26388
+ (v) => finish(resolve, v),
26389
+ (e) => finish(reject, e)
26390
+ );
26391
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
26392
+ });
26393
+ }
26394
+ async function putGlobal(holoInstance, tableName, data, password = null, options = {}) {
26340
26395
  try {
26341
26396
  if (!tableName || !data) {
26342
26397
  throw new Error("Table name and data are required");
@@ -26383,7 +26438,8 @@ async function putGlobal(holoInstance, tableName, data, password = null) {
26383
26438
  });
26384
26439
  });
26385
26440
  }
26386
- return new Promise((resolve, reject) => {
26441
+ const writeTimeoutMs = options.timeout !== void 0 ? options.timeout : WRITE_TIMEOUT_MS2;
26442
+ const ackPromise = new Promise((resolve, reject) => {
26387
26443
  try {
26388
26444
  let dataToStore = { ...data };
26389
26445
  if (dataToStore._meta !== void 0) {
@@ -26445,6 +26501,17 @@ async function putGlobal(holoInstance, tableName, data, password = null) {
26445
26501
  reject(error);
26446
26502
  }
26447
26503
  });
26504
+ const ACK_OK = Symbol("ackOk");
26505
+ return withWriteTimeout2(
26506
+ ackPromise.then(() => ACK_OK),
26507
+ writeTimeoutMs,
26508
+ void 0
26509
+ ).then((result) => {
26510
+ if (result !== ACK_OK) {
26511
+ console.warn(`putGlobal: no ack within ${writeTimeoutMs}ms for table=${tableName} \u2014 write queued locally, will replay on reconnect`);
26512
+ }
26513
+ return void 0;
26514
+ });
26448
26515
  } catch (error) {
26449
26516
  console.error("Error in putGlobal:", error);
26450
26517
  throw error;
@@ -30528,14 +30595,14 @@ var HoloSphere = class {
30528
30595
  return deleteNode(this, holon, lens, key);
30529
30596
  }
30530
30597
  // ================================ GLOBAL FUNCTIONS ================================
30531
- async putGlobal(tableName, data, password = null) {
30532
- return putGlobal(this, tableName, data, password);
30598
+ async putGlobal(tableName, data, password = null, options = {}) {
30599
+ return putGlobal(this, tableName, data, password, options);
30533
30600
  }
30534
30601
  /**
30535
30602
  * v2-compatible alias for putGlobal (no password param)
30536
30603
  */
30537
- async writeGlobal(tableName, data) {
30538
- return putGlobal(this, tableName, data, null);
30604
+ async writeGlobal(tableName, data, options = {}) {
30605
+ return putGlobal(this, tableName, data, null, options);
30539
30606
  }
30540
30607
  async getGlobal(tableName, key, password = null) {
30541
30608
  return getGlobal(this, tableName, key, password);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HoloSphere Bundle v1.3.0-alpha5
2
+ * HoloSphere Bundle v1.3.0-alpha7
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-alpha5/holosphere-bundle.js"></script>
12
+ * <script src="https://unpkg.com/holosphere@1.3.0-alpha7/holosphere-bundle.js"></script>
13
13
  * <script>
14
14
  * const hs = new HoloSphere('myapp');
15
15
  * </script>
@@ -25391,6 +25391,7 @@ var HoloSphere = (() => {
25391
25391
 
25392
25392
  // content.js
25393
25393
  var READ_TIMEOUT_MS = 8e3;
25394
+ var WRITE_TIMEOUT_MS = 5e3;
25394
25395
  function onceWithTimeout(node, timeoutMs = READ_TIMEOUT_MS) {
25395
25396
  return new Promise((resolve) => {
25396
25397
  let done = false;
@@ -25406,6 +25407,23 @@ var HoloSphere = (() => {
25406
25407
  }
25407
25408
  });
25408
25409
  }
25410
+ function withWriteTimeout(promise, timeoutMs, queuedResult) {
25411
+ if (!timeoutMs || timeoutMs <= 0) return promise;
25412
+ return new Promise((resolve, reject) => {
25413
+ let done = false;
25414
+ const finish = (fn, val) => {
25415
+ if (!done) {
25416
+ done = true;
25417
+ fn(val);
25418
+ }
25419
+ };
25420
+ promise.then(
25421
+ (v) => finish(resolve, v),
25422
+ (e) => finish(reject, e)
25423
+ );
25424
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
25425
+ });
25426
+ }
25409
25427
  function sanitizeForStorage(value, path = "", seen = /* @__PURE__ */ new WeakSet(), warnings = []) {
25410
25428
  if (value === null) return null;
25411
25429
  const t = typeof value;
@@ -25452,7 +25470,11 @@ var HoloSphere = (() => {
25452
25470
  if (!holon || !lens) {
25453
25471
  throw new Error("put: Missing required holon or lens parameters:", holon, lens);
25454
25472
  }
25455
- const { disableHologramRedirection = false } = options;
25473
+ const {
25474
+ disableHologramRedirection = false,
25475
+ timeout: writeTimeoutOverride
25476
+ } = options;
25477
+ const writeTimeoutMs = writeTimeoutOverride !== void 0 ? writeTimeoutOverride : WRITE_TIMEOUT_MS;
25456
25478
  let targetHolon = holon;
25457
25479
  let targetLens = lens;
25458
25480
  let targetKey = data.id;
@@ -25540,7 +25562,7 @@ var HoloSphere = (() => {
25540
25562
  });
25541
25563
  });
25542
25564
  }
25543
- return new Promise((resolve, reject) => {
25565
+ const ackPromise = new Promise((resolve, reject) => {
25544
25566
  try {
25545
25567
  const sanitizeWarnings = [];
25546
25568
  let dataToStore = sanitizeForStorage(data, "", /* @__PURE__ */ new WeakSet(), sanitizeWarnings) || {};
@@ -25573,16 +25595,17 @@ var HoloSphere = (() => {
25573
25595
  }
25574
25596
  }
25575
25597
  let updatedHolograms = [];
25576
- if (!isHologram2 && !options.isHologramUpdate) {
25598
+ const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
25599
+ const cascadeVisited = new Set(options._cascadeVisited || []);
25600
+ if (!cascadeVisited.has(currentDataSoul)) {
25601
+ cascadeVisited.add(currentDataSoul);
25577
25602
  try {
25578
- const currentDataSoul = `${holoInstance.appname}/${targetHolon}/${targetLens}/${targetKey}`;
25579
25603
  const currentNodeRef = holoInstance.getNodeRef(currentDataSoul);
25580
25604
  await new Promise((resolveHologramUpdate) => {
25581
25605
  currentNodeRef.get("_holograms").once(async (hologramsSet) => {
25582
25606
  if (hologramsSet) {
25583
25607
  const hologramSouls = Object.keys(hologramsSet).filter(
25584
- (k) => k !== "_" && hologramsSet[k] === true
25585
- // Only active holograms (deleted ones are null/removed)
25608
+ (k) => k !== "_" && hologramsSet[k] === true && !cascadeVisited.has(k)
25586
25609
  );
25587
25610
  if (hologramSouls.length > 0) {
25588
25611
  const updatePromises = hologramSouls.map(async (hologramSoul) => {
@@ -25611,8 +25634,12 @@ var HoloSphere = (() => {
25611
25634
  // Don't auto-propagate hologram updates
25612
25635
  disableHologramRedirection: true,
25613
25636
  // Prevent redirection when updating holograms
25614
- isHologramUpdate: true
25615
- // Prevent recursive hologram updates
25637
+ isHologramUpdate: true,
25638
+ // Carry the visited set forward so the
25639
+ // recursive put keeps cascading through
25640
+ // this hop's `_holograms` set without
25641
+ // looping back through us.
25642
+ _cascadeVisited: cascadeVisited
25616
25643
  }
25617
25644
  );
25618
25645
  updatedHolograms.push({
@@ -25690,6 +25717,16 @@ var HoloSphere = (() => {
25690
25717
  reject(error);
25691
25718
  }
25692
25719
  });
25720
+ return withWriteTimeout(ackPromise, writeTimeoutMs, {
25721
+ success: true,
25722
+ queued: true,
25723
+ isHologramAtPath: isHologram2,
25724
+ pathHolon: targetHolon,
25725
+ pathLens: targetLens,
25726
+ pathKey: targetKey,
25727
+ propagationResult: null,
25728
+ updatedHolograms: []
25729
+ });
25693
25730
  } catch (error) {
25694
25731
  console.error("Error in put:", error);
25695
25732
  throw error;
@@ -26362,7 +26399,25 @@ var HoloSphere = (() => {
26362
26399
  }
26363
26400
 
26364
26401
  // global.js
26365
- async function putGlobal(holoInstance, tableName, data, password = null) {
26402
+ var WRITE_TIMEOUT_MS2 = 5e3;
26403
+ function withWriteTimeout2(promise, timeoutMs, queuedResult) {
26404
+ if (!timeoutMs || timeoutMs <= 0) return promise;
26405
+ return new Promise((resolve, reject) => {
26406
+ let done = false;
26407
+ const finish = (fn, val) => {
26408
+ if (!done) {
26409
+ done = true;
26410
+ fn(val);
26411
+ }
26412
+ };
26413
+ promise.then(
26414
+ (v) => finish(resolve, v),
26415
+ (e) => finish(reject, e)
26416
+ );
26417
+ setTimeout(() => finish(resolve, queuedResult), timeoutMs);
26418
+ });
26419
+ }
26420
+ async function putGlobal(holoInstance, tableName, data, password = null, options = {}) {
26366
26421
  try {
26367
26422
  if (!tableName || !data) {
26368
26423
  throw new Error("Table name and data are required");
@@ -26409,7 +26464,8 @@ var HoloSphere = (() => {
26409
26464
  });
26410
26465
  });
26411
26466
  }
26412
- return new Promise((resolve, reject) => {
26467
+ const writeTimeoutMs = options.timeout !== void 0 ? options.timeout : WRITE_TIMEOUT_MS2;
26468
+ const ackPromise = new Promise((resolve, reject) => {
26413
26469
  try {
26414
26470
  let dataToStore = { ...data };
26415
26471
  if (dataToStore._meta !== void 0) {
@@ -26471,6 +26527,17 @@ var HoloSphere = (() => {
26471
26527
  reject(error);
26472
26528
  }
26473
26529
  });
26530
+ const ACK_OK = Symbol("ackOk");
26531
+ return withWriteTimeout2(
26532
+ ackPromise.then(() => ACK_OK),
26533
+ writeTimeoutMs,
26534
+ void 0
26535
+ ).then((result) => {
26536
+ if (result !== ACK_OK) {
26537
+ console.warn(`putGlobal: no ack within ${writeTimeoutMs}ms for table=${tableName} \u2014 write queued locally, will replay on reconnect`);
26538
+ }
26539
+ return void 0;
26540
+ });
26474
26541
  } catch (error) {
26475
26542
  console.error("Error in putGlobal:", error);
26476
26543
  throw error;
@@ -30554,14 +30621,14 @@ var HoloSphere = (() => {
30554
30621
  return deleteNode(this, holon, lens, key);
30555
30622
  }
30556
30623
  // ================================ GLOBAL FUNCTIONS ================================
30557
- async putGlobal(tableName, data, password = null) {
30558
- return putGlobal(this, tableName, data, password);
30624
+ async putGlobal(tableName, data, password = null, options = {}) {
30625
+ return putGlobal(this, tableName, data, password, options);
30559
30626
  }
30560
30627
  /**
30561
30628
  * v2-compatible alias for putGlobal (no password param)
30562
30629
  */
30563
- async writeGlobal(tableName, data) {
30564
- return putGlobal(this, tableName, data, null);
30630
+ async writeGlobal(tableName, data, options = {}) {
30631
+ return putGlobal(this, tableName, data, null, options);
30565
30632
  }
30566
30633
  async getGlobal(tableName, key, password = null) {
30567
30634
  return getGlobal(this, tableName, key, password);