);
}
/* ── the switch flow (SPEC 4.4) ── */
const skSleep = (ms) => new Promise((r) => setTimeout(r, ms));
const skUi = { set: null }; // the layer's state setter
const skShow = (v) => { if (skUi.set) skUi.set(v); };
const SK_NOT_SENT = "You have work that is not on the server yet. Wait until the connection is back, then switch.";
const SK_OTHER_NOT_SENT = (names) => "Another DMS window on this computer (in " + names + ") has work that is not on the server yet. Wait until the connection is back in that window, then switch.";
/* the OTHER DMS windows of this PC (SPEC 4.4 step 2 / D12 across windows; RC35 attacker finding): a switch changes this PC's sign-in, and a
window without a sign-in of its own follows it on its next reload - so the switch also waits for what the other windows (in any store
but the one switched to) have not sent yet. Every DMS window (K2BStoreLayer) answers "pending?" on the k2b-store channel with its own
count and starts sending its work at once; still unsent after 5 s -> the switch is refused. A window left in the old store does not
reload into the new one while it has unsent work (the "Reload into" button waits; a plain reload or close asks the browser's "Leave?"). */
const SK_TAB = "t" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
let skLeaving = false; // this window reloads on purpose (switch / reload banner / home store): no "Leave?" question
let skMovedAway = false; // another window of this PC switched it to another store (the "This window is in" overlay)
function skOwnWork() {
const D = window.DMS, st = sk$(); const SW = (D && D.storeSwitch) || {};
if (!D || !st || !SW.pendingWork || !(D.getAuthToken && D.getAuthToken())) return null; // signed out: this window sends nothing
let p; try { p = SW.pendingWork() || {}; } catch (e) { p = { changed: 1, flushing: true }; }
const booting = !!(D.connState && D.connState() === "connecting"); // its first server answer is not in yet: its counts are not final
return { store: String(st.pageId() || ""), dba: st.branding().dba, n: (p.changed | 0) + (p.deletes | 0), flushing: !!p.flushing, inflight: SW.inflight ? (SW.inflight() | 0) : 0, booting };
}
const skHasWork = (a) => !!(a && (a.n > 0 || a.flushing || a.inflight > 0));
async function skOtherWindowsWork(to, ms) {
let bc = null; try { bc = new BroadcastChannel("k2b-store"); } catch (e) { return []; } // a browser without the channel: nothing to ask
const ask = SK_TAB + "-" + Date.now().toString(36), seen = new Map();
bc.onmessage = (e) => { const m = e && e.data; if (m && m.type === "pending" && m.ask === ask && m.tab && m.tab !== SK_TAB) seen.set(m.tab, m); };
const busy = () => Array.from(seen.values()).filter((a) => String(a.store) !== String(to) && skHasWork(a));
const post = () => { try { bc.postMessage({ type: "pending?", ask, tab: SK_TAB, to: String(to) }); } catch (e) {} };
const deadline = Date.now() + (ms || 5000);
post(); await skSleep(400);
while (busy().length && Date.now() < deadline) { await skSleep(500); post(); await skSleep(150); }
try { bc.close(); } catch (e) {}
return busy();
}
async function skSwitch(to) {
const st = sk$(); const D = window.DMS;
if (!st || !D) return { ok: false, reason: "not-ready" };
if (!to || to === st.pageId()) return { ok: false, reason: "same" };
const name = ((st.list().find((x) => x.id === to)) || {}).dba || ("store " + to);
// 1. screens with unsaved changes: ask first (Save and switch / Close without saving / Stay here)
const dirty = skUnsaved.dirty();
if (dirty.length) {
const choice = await new Promise((res) => skShow({ kind: "unsaved", to, name, items: dirty.map((x) => ({ id: x.id, label: x.label, canSave: !!x.spec.save })), done: res }));
skShow(null);
if (choice === "save") {
for (const x of dirty) { try { const r = await x.spec.save(); if (r === false || (r && r.ok === false)) { skShow({ kind: "msg", text: "“" + x.label + "” could not be saved. Nothing was switched." }); return { ok: false, reason: "save-failed" }; } } catch (e) { skShow({ kind: "msg", text: "“" + x.label + "” could not be saved. Nothing was switched." }); return { ok: false, reason: "save-failed" }; } }
if (skUnsaved.dirty().length) { skShow({ kind: "msg", text: "Something is still not saved. Nothing was switched." }); return { ok: false, reason: "still-dirty" }; }
} else if (choice === "discard") {
for (const x of dirty) { try { x.spec.discard && x.spec.discard(); } catch (e) { /* closed anyway */ } }
} else return { ok: false, reason: "stay" };
}
// 2. what this page has not sent to the server: send it now (5 s); writes in flight: wait (10 s); offline / still unsent -> refused
skShow({ kind: "busy", text: "Getting ready to switch to " + name + "…" });
const SW = D.storeSwitch || {};
let pend = SW.pendingWork ? SW.pendingWork() : { changed: 0, deletes: 0, flushing: false };
if ((pend.changed || pend.deletes || pend.flushing) && SW.flushPending) pend = await SW.flushPending(5000);
const until = Date.now() + 10000;
while (SW.inflight && SW.inflight() > 0 && Date.now() < until) await skSleep(100);
const online = !D.isOnline || D.isOnline();
if (!online || pend.changed || pend.deletes || pend.flushing || (SW.inflight && SW.inflight() > 0)) { skShow({ kind: "msg", text: SK_NOT_SENT, refused: true }); return { ok: false, reason: "not-sent" }; }
// 2b. the other DMS windows of this PC: their unsent work first (they send it now; still unsent after 5 s -> refused)
const others = await skOtherWindowsWork(to, 5000);
if (others.length) {
const names = Array.from(new Set(others.map((a) => a.dba || ("store " + a.store)))).join(", ");
skShow({ kind: "msg", text: SK_OTHER_NOT_SENT(names), refused: true, other: true }); return { ok: false, reason: "not-sent-other" };
}
// 3. a check number is reserved only inside a check command (writeCheck reserves, posts or gives it back) - step 2 waited for those
// 3b. the server issues the session for the other store
let ans = null;
try {
const r = await fetch(location.origin + "/api/session/store", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + D.getAuthToken(), "X-K2B-Protocol": String(D.PROTOCOL), "X-K2B-Store": st.pageId() || "" }, body: JSON.stringify({ store: to }), credentials: "omit" });
ans = await r.json().catch(() => null);
if (!r.ok || !ans || !ans.token) { skShow({ kind: "msg", text: (ans && ans.error) || "The switch did not work. Nothing was changed." }); return { ok: false, reason: (ans && ans.code) || "refused" }; }
} catch (e) { skShow({ kind: "msg", text: "The server could not be reached. Nothing was changed." }); return { ok: false, reason: "network" }; }
// 5. this PC remembers the store; this tab's per-session items go; the other tabs are told; the live link closes
const from = st.pageId();
D.setAuthToken(ans.token);
st._setLast(to);
try { sessionStorage.removeItem("dkf_view_deal"); sessionStorage.removeItem("jb_helper_builtin_v1"); sessionStorage.removeItem("k2b-store-upgrade"); } catch (e) {}
try { window.jbHelperReset && window.jbHelperReset(); } catch (e) {}
try { SW.closeLive && SW.closeLive(); } catch (e) {}
try { const bc = new BroadcastChannel("k2b-store"); bc.postMessage({ type: "switched", from, to, at: Date.now() }); bc.close(); } catch (e) {}
try { localStorage.setItem("k2b-store-switch", JSON.stringify({ from, to, at: Date.now() })); } catch (e) {}
// 6. the whole DMS starts again in the other store
skShow({ kind: "busy", text: "Opening " + name + "…" });
skLeaving = true;
try { location.reload(); } catch (e) {}
return { ok: true };
}
window.k2bStoreSwitchFlow = skSwitch;
/* ── answers that say this window is in the wrong store (DMSCore apiFetch calls this) ── */
let skRefusedShown = false;
window.k2bStoreRefusal = (body, status) => {
const st = sk$(); if (!st || !body) return;
if (body.code === "store_switched") {
if (skRefusedShown) return; skRefusedShown = true;
const nm = ((st.list().find((x) => x.id === String(body.store))) || {}).dba || ("store " + body.store);
skShow({ kind: "switched", text: "This window was showing another store - reloading into " + nm + "." });
setTimeout(() => { skLeaving = true; try { location.reload(); } catch (e) {} }, 1500);
return;
}
if (body.code === "store_denied" || body.code === "store_off") {
if (skRefusedShown) return; skRefusedShown = true;
const nm = st.pageId() === String(body.store) ? st.branding().dba : ("store " + body.store);
skShow({ kind: "refused", name: nm, home: body.home ? String(body.home) : null, off: body.code === "store_off" });
}
};
async function skGoHome(home) {
const D = window.DMS;
try {
const r = await fetch(location.origin + "/api/session/store", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + D.getAuthToken(), "X-K2B-Protocol": String(D.PROTOCOL) }, body: JSON.stringify({ store: home }), credentials: "omit" });
const a = await r.json().catch(() => null);
if (r.ok && a && a.token) { D.setAuthToken(a.token); sk$()._setLast(home); skLeaving = true; location.reload(); return; }
} catch (e) { /* signed out below */ }
try { await D.signOut(); } catch (e) {}
location.reload();
}
/* ── the layer: every dialog of the switch ── */
function K2BStoreLayer() {
const st = useK2BStore();
const [ui, setUi] = React.useState(null);
const [moved, setMoved] = React.useState(null);
React.useEffect(() => { skUi.set = setUi; return () => { if (skUi.set === setUi) skUi.set = null; }; }, []);
React.useEffect(() => { // another tab / window of this PC switched store
const here = () => (sk$() && sk$().pageId()) || null;
let bc = null, sending = false;
const answer = (m) => { // another window asks before it switches: what this window has not sent yet (and send it now)
const a = skOwnWork(); if (!a || !bc) return;
const post = (x) => { try { bc.postMessage(Object.assign({ type: "pending", ask: m.ask, tab: SK_TAB }, x)); } catch (e) {} };
post(a);
const SW = (window.DMS && window.DMS.storeSwitch) || {};
if (skHasWork(a) && !a.booting && !sending && SW.flushPending) { // a window still loading sends its work itself once loaded
sending = true;
Promise.resolve().then(() => SW.flushPending(4000)).catch(() => null).then(() => { sending = false; const b = skOwnWork(); if (b) post(b); });
}
};
const on = (m) => {
if (!m) return;
if (m.type === "pending?") { if (m.tab !== SK_TAB) answer(m); return; }
if (m.type !== "switched" || !m.to || !here()) return;
skMovedAway = String(m.to) !== here();
if (skMovedAway) setMoved({ from: here(), to: String(m.to) });
};
try { bc = new BroadcastChannel("k2b-store"); bc.onmessage = (e) => on(e.data); } catch (e) { bc = null; }
const ls = (e) => { if (e.key === "k2b-store-switch" && e.newValue) { try { on(Object.assign({ type: "switched" }, JSON.parse(e.newValue))); } catch (er) {} } };
window.addEventListener("storage", ls);
// a reload / close of a window the PC switched away from, with work not on the server: the browser asks "Leave?" (its next load would
// open the other store, and this store's unsent work would wait in this PC's copy until someone opens this store here again)
const leave = (e) => {
if (skLeaving || !skMovedAway) return undefined;
const s = sk$(); if (!s || !s.pageId()) return undefined;
let t = null; try { t = sessionStorage.getItem("jbrews-auth-token") || localStorage.getItem("jbrews-auth-token"); } catch (er) { return undefined; }
const lands = t ? s.tokenStore(t) : null;
if (!lands || lands === s.pageId() || !skHasWork(skOwnWork())) return undefined;
e.preventDefault(); e.returnValue = "unsent"; return "unsent";
};
window.addEventListener("beforeunload", leave);
return () => { try { bc && bc.close(); } catch (e) {} window.removeEventListener("storage", ls); window.removeEventListener("beforeunload", leave); };
}, []);
const nameOf = (id) => { const s = st && st.list().find((x) => x.id === id); return s ? s.dba : (st && st.pageId() === id ? st.branding().dba : "store " + id); };
const isPop = /[?&]popout=/.test(location.search);
const box = (attrs, children) => (