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 +136 -21
- package/global.js +44 -2
- package/hologram.js +7 -1
- package/holosphere-bundle.esm.js +127 -20
- package/holosphere-bundle.js +127 -20
- package/holosphere-bundle.min.js +8 -8
- package/holosphere.d.ts +29 -2
- package/holosphere.js +4 -4
- package/package.json +1 -1
- package/utils.js +16 -0
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
|
|
28
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
393
|
+
const hologramSouls = Object.keys(hologramsSet).filter(k =>
|
|
394
|
+
k !== '_' && hologramsSet[k] === true && !cascadeVisited.has(k)
|
|
306
395
|
);
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/holosphere-bundle.esm.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HoloSphere ESM Bundle v1.3.0-
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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);
|