@geravant/sinain 1.22.4 → 1.22.6
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/package.json
CHANGED
package/sinain-core/src/index.ts
CHANGED
|
@@ -264,7 +264,13 @@ async function exportConceptBundle(
|
|
|
264
264
|
{ timeout: 30_000, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
|
|
265
265
|
const parsed = JSON.parse(stdout);
|
|
266
266
|
// If the export found at least one entity (the root), return it.
|
|
267
|
-
|
|
267
|
+
// BFS always includes the root in `visited`, so entities >= 1 always —
|
|
268
|
+
// not a useful guard. Check for actual triples instead, otherwise we
|
|
269
|
+
// accept a bundle that has just the root with no data and never try
|
|
270
|
+
// the next DB. This was the bug that produced empty parloa bundles
|
|
271
|
+
// when entity:parloa lived in the workspace DB but local was checked
|
|
272
|
+
// first and returned an empty (root-only) bundle.
|
|
273
|
+
if (parsed.stats && parsed.stats.triples > 0) return parsed;
|
|
268
274
|
} catch (e) {
|
|
269
275
|
// try next DB
|
|
270
276
|
}
|
|
@@ -493,10 +493,57 @@ function ensurePeerJsLoaded() {
|
|
|
493
493
|
|
|
494
494
|
function newPeer(idOrUndef) {
|
|
495
495
|
// Honor SHARE_PEERJS_HOST env-injected override. Empty string = peerjs.com default.
|
|
496
|
-
|
|
496
|
+
// debug: 3 enables verbose peerjs logging in console — critical for diagnosing
|
|
497
|
+
// WebRTC handshake failures (NAT, ICE state transitions, peer-unavailable, etc.).
|
|
498
|
+
const opts = SHARE_PEERJS_HOST ? { host: SHARE_PEERJS_HOST, debug: 3 } : { debug: 3 };
|
|
497
499
|
return idOrUndef ? new window.Peer(idOrUndef, opts) : new window.Peer(opts);
|
|
498
500
|
}
|
|
499
501
|
|
|
502
|
+
// Attach detailed instrumentation to a peer + its underlying RTCPeerConnections.
|
|
503
|
+
// Logs to console (sinain-share namespace) so diagnostics are visible without
|
|
504
|
+
// any UI changes. Tracks the four state machines that matter for WebRTC: ICE
|
|
505
|
+
// gathering, ICE connection, peerconnection, and signaling.
|
|
506
|
+
function instrumentPeer(peer, label) {
|
|
507
|
+
const tag = "[sinain-share:" + label + "]";
|
|
508
|
+
console.log(tag, "instrumented peer", peer.id || "(no-id-yet)");
|
|
509
|
+
peer.on("open", id => console.log(tag, "peer.open id=" + id));
|
|
510
|
+
peer.on("error", e => console.warn(tag, "peer.error type=" + (e && e.type) + " msg=" + (e && e.message)));
|
|
511
|
+
peer.on("disconnected", () => console.warn(tag, "peer.disconnected (lost broker connection)"));
|
|
512
|
+
peer.on("close", () => console.log(tag, "peer.close (destroyed)"));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function instrumentConnection(conn, label) {
|
|
516
|
+
const tag = "[sinain-share:" + label + ":" + (conn.peer || "?") + "]";
|
|
517
|
+
console.log(tag, "new connection, reliable=" + conn.reliable);
|
|
518
|
+
conn.on("open", () => console.log(tag, "conn.open (DataChannel ready)"));
|
|
519
|
+
conn.on("error", e => console.warn(tag, "conn.error", e));
|
|
520
|
+
conn.on("close", () => console.log(tag, "conn.close"));
|
|
521
|
+
conn.on("iceStateChanged", s => console.log(tag, "conn.iceStateChanged →", s));
|
|
522
|
+
// Tap into the underlying RTCPeerConnection. peerjs exposes it as
|
|
523
|
+
// conn.peerConnection (it might not exist immediately — wait a tick).
|
|
524
|
+
setTimeout(() => {
|
|
525
|
+
const pc = conn.peerConnection;
|
|
526
|
+
if (!pc) { console.warn(tag, "no peerConnection exposed"); return; }
|
|
527
|
+
pc.addEventListener("iceconnectionstatechange",
|
|
528
|
+
() => console.log(tag, "iceConnectionState →", pc.iceConnectionState));
|
|
529
|
+
pc.addEventListener("connectionstatechange",
|
|
530
|
+
() => console.log(tag, "connectionState →", pc.connectionState));
|
|
531
|
+
pc.addEventListener("icegatheringstatechange",
|
|
532
|
+
() => console.log(tag, "iceGatheringState →", pc.iceGatheringState));
|
|
533
|
+
pc.addEventListener("signalingstatechange",
|
|
534
|
+
() => console.log(tag, "signalingState →", pc.signalingState));
|
|
535
|
+
pc.addEventListener("icecandidate", (e) => {
|
|
536
|
+
if (!e.candidate) { console.log(tag, "ICE gathering complete"); return; }
|
|
537
|
+
const c = e.candidate;
|
|
538
|
+
// Log candidate type (host = local LAN, srflx = STUN, relay = TURN, prflx = peer-reflexive).
|
|
539
|
+
// If we never see "relay" but only "host"/"srflx" and connection fails, NAT traversal
|
|
540
|
+
// requires TURN — which peerjs cloud doesn't provide.
|
|
541
|
+
const type = (c.candidate.match(/typ (\\S+)/) || [])[1] || "?";
|
|
542
|
+
console.log(tag, "iceCandidate type=" + type + " proto=" + c.protocol + " addr=" + (c.address || "?") + ":" + (c.port || "?"));
|
|
543
|
+
});
|
|
544
|
+
}, 100);
|
|
545
|
+
}
|
|
546
|
+
|
|
500
547
|
const ShareManager = (() => {
|
|
501
548
|
// share_token → live Peer instance (sender side only). Bundles are re-fetched
|
|
502
549
|
// on demand rather than kept in JS memory across resume.
|
|
@@ -543,6 +590,7 @@ const ShareManager = (() => {
|
|
|
543
590
|
// Peer mode
|
|
544
591
|
await ensurePeerJsLoaded();
|
|
545
592
|
const peer = newPeer(token);
|
|
593
|
+
instrumentPeer(peer, "sender:" + token.slice(0, 8));
|
|
546
594
|
await new Promise((res, rej) => {
|
|
547
595
|
peer.on("open", () => res());
|
|
548
596
|
peer.on("error", e => rej(e));
|
|
@@ -565,13 +613,17 @@ const ShareManager = (() => {
|
|
|
565
613
|
|
|
566
614
|
function attachSenderHandlers(peer, token, entity) {
|
|
567
615
|
peer.on("connection", (conn) => {
|
|
616
|
+
console.log("[sinain-share:sender:" + token.slice(0, 8) + "] inbound connection from", conn.peer);
|
|
617
|
+
instrumentConnection(conn, "sender");
|
|
568
618
|
patchStatus(token, "connecting");
|
|
569
619
|
conn.on("open", async () => {
|
|
570
620
|
try {
|
|
571
621
|
// Re-fetch bundle each time — keeps memory low and reflects latest state.
|
|
572
622
|
const bundle = await buildBundle(entity);
|
|
623
|
+
console.log("[sinain-share:sender:" + token.slice(0, 8) + "] sending bundle, " + bundle.length + " bytes");
|
|
573
624
|
conn.send({ type: "bundle", payload: bundle });
|
|
574
625
|
} catch (e) {
|
|
626
|
+
console.warn("[sinain-share:sender] buildBundle/send failed:", e);
|
|
575
627
|
conn.send({ type: "error", message: String(e).slice(0, 200) });
|
|
576
628
|
conn.close();
|
|
577
629
|
}
|
|
@@ -609,6 +661,7 @@ const ShareManager = (() => {
|
|
|
609
661
|
try {
|
|
610
662
|
await ensurePeerJsLoaded();
|
|
611
663
|
const peer = newPeer(share.share_token);
|
|
664
|
+
instrumentPeer(peer, "sender-resume:" + share.share_token.slice(0, 8));
|
|
612
665
|
await new Promise((res, rej) => {
|
|
613
666
|
peer.on("open", () => res());
|
|
614
667
|
peer.on("error", e => rej(e));
|
|
@@ -652,40 +705,55 @@ const ShareManager = (() => {
|
|
|
652
705
|
|
|
653
706
|
async function connectAsRecipient(token) {
|
|
654
707
|
showToast('<span class="spinner"></span> Connecting peer-to-peer…', 30_000);
|
|
708
|
+
console.log("[sinain-share:recipient:" + token.slice(0, 8) + "] connectAsRecipient start");
|
|
655
709
|
await ensurePeerJsLoaded();
|
|
656
710
|
const me = newPeer();
|
|
711
|
+
instrumentPeer(me, "recipient:" + token.slice(0, 8));
|
|
657
712
|
await new Promise((res, rej) => {
|
|
658
713
|
me.on("open", () => res());
|
|
659
714
|
me.on("error", e => rej(e));
|
|
660
715
|
setTimeout(() => rej(new Error("peerjs broker timeout")), 8000);
|
|
661
716
|
});
|
|
717
|
+
console.log("[sinain-share:recipient:" + token.slice(0, 8) + "] my peer registered, dialing", token);
|
|
662
718
|
return new Promise((resolve, reject) => {
|
|
663
719
|
const conn = me.connect(token, { reliable: true });
|
|
720
|
+
instrumentConnection(conn, "recipient");
|
|
664
721
|
const cleanup = () => { try { conn.close(); } catch {} try { me.destroy(); } catch {} };
|
|
665
722
|
const openTimeout = setTimeout(() => {
|
|
723
|
+
console.warn("[sinain-share:recipient:" + token.slice(0, 8) + "] 15s timeout — conn.open never fired. Almost certainly NAT traversal failed (no TURN). Look for ICE candidate types above — only host/srflx without 'relay' = TURN missing.");
|
|
666
724
|
cleanup();
|
|
667
725
|
reject(new Error("source offline or unreachable"));
|
|
668
726
|
}, 15_000);
|
|
669
|
-
conn.on("open", () =>
|
|
670
|
-
|
|
727
|
+
conn.on("open", () => {
|
|
728
|
+
clearTimeout(openTimeout);
|
|
729
|
+
console.log("[sinain-share:recipient:" + token.slice(0, 8) + "] DataChannel open — waiting for bundle");
|
|
730
|
+
});
|
|
731
|
+
conn.on("error", (e) => {
|
|
732
|
+
console.warn("[sinain-share:recipient:" + token.slice(0, 8) + "] conn.error", e);
|
|
733
|
+
cleanup(); reject(e);
|
|
734
|
+
});
|
|
671
735
|
conn.on("data", async (msg) => {
|
|
672
736
|
if (!msg) return;
|
|
673
737
|
if (msg.type === "error") {
|
|
738
|
+
console.warn("[sinain-share:recipient] source reported error:", msg.message);
|
|
674
739
|
cleanup();
|
|
675
740
|
reject(new Error("source error: " + msg.message));
|
|
676
741
|
return;
|
|
677
742
|
}
|
|
678
743
|
if (msg.type === "bundle") {
|
|
744
|
+
console.log("[sinain-share:recipient:" + token.slice(0, 8) + "] bundle received, " + (msg.payload && msg.payload.length) + " bytes — POSTing to /knowledge/concepts/import");
|
|
679
745
|
try {
|
|
680
746
|
const importR = await api("/knowledge/concepts/import?conflict=merge", {
|
|
681
747
|
method: "POST",
|
|
682
748
|
headers: {"Content-Type": "application/json"},
|
|
683
749
|
body: msg.payload,
|
|
684
750
|
});
|
|
751
|
+
console.log("[sinain-share:recipient] import response:", importR);
|
|
685
752
|
conn.send({ type: "ack" });
|
|
686
753
|
setTimeout(cleanup, 500);
|
|
687
754
|
resolve(importR);
|
|
688
755
|
} catch (e) {
|
|
756
|
+
console.warn("[sinain-share:recipient] import threw:", e);
|
|
689
757
|
cleanup();
|
|
690
758
|
reject(e);
|
|
691
759
|
}
|
|
@@ -2149,8 +2217,14 @@ export function createAppServer(deps: ServerDeps) {
|
|
|
2149
2217
|
|
|
2150
2218
|
// New "living Confluence" SPA — search-driven, LLM-rendered pages,
|
|
2151
2219
|
// bookmarks, retraction, concept transfer.
|
|
2220
|
+
// Cache-Control: no-cache forces browsers to revalidate the SPA HTML
|
|
2221
|
+
// on every navigation. Otherwise, after a sinain-core upgrade the
|
|
2222
|
+
// browser serves stale SPA from cache (bugfixes don't take effect
|
|
2223
|
+
// until the user hard-reloads). With revalidation on, ETag mismatches
|
|
2224
|
+
// are detected immediately and the new SPA is loaded.
|
|
2152
2225
|
if (req.method === "GET" && url.pathname === "/knowledge/ui") {
|
|
2153
2226
|
res.setHeader("Content-Type", "text/html");
|
|
2227
|
+
res.setHeader("Cache-Control", "no-cache, must-revalidate");
|
|
2154
2228
|
res.end(renderKnowledgeUiV2());
|
|
2155
2229
|
return;
|
|
2156
2230
|
}
|
|
@@ -2159,6 +2233,7 @@ export function createAppServer(deps: ServerDeps) {
|
|
|
2159
2233
|
// we just serve the same HTML; client-side router parses location.pathname.
|
|
2160
2234
|
if (req.method === "GET" && url.pathname.startsWith("/knowledge/ui/")) {
|
|
2161
2235
|
res.setHeader("Content-Type", "text/html");
|
|
2236
|
+
res.setHeader("Cache-Control", "no-cache, must-revalidate");
|
|
2162
2237
|
res.end(renderKnowledgeUiV2());
|
|
2163
2238
|
return;
|
|
2164
2239
|
}
|