Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cc8faf8cb |
@@ -461,6 +461,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// The one time the browser refuses to save, say so out loud as well as leaving the
|
// The one time the browser refuses to save, say so out loud as well as leaving the
|
||||||
// standing warning in the Session panel. Set before the first render so a failure
|
// standing warning in the Session panel. Set before the first render so a failure
|
||||||
// during start-up is caught too.
|
// during start-up is caught too.
|
||||||
|
// Another tab writing to the same table. Said once, with the way out attached —
|
||||||
|
// reloading here adopts whatever that tab has done, which is the only resolution
|
||||||
|
// that doesn't throw somebody's work away.
|
||||||
|
HoldemState.onOtherTab = () => {
|
||||||
|
Toast.show(
|
||||||
|
"This table is open in another tab. Whichever acts last wins.",
|
||||||
|
{ actionLabel: "Reload", onAction: () => window.location.reload(), duration: 20000 });
|
||||||
|
Render.renderStorageWarning();
|
||||||
|
};
|
||||||
|
HoldemState.watchOtherTabs();
|
||||||
|
|
||||||
HoldemState.onSaveFailed = () => {
|
HoldemState.onSaveFailed = () => {
|
||||||
Toast.show(
|
Toast.show(
|
||||||
"This table isn't being saved — refreshing would lose it. Keep this tab open.",
|
"This table isn't being saved — refreshing would lose it. Keep this tab open.",
|
||||||
|
|||||||
@@ -761,13 +761,20 @@ const Render = {
|
|||||||
renderStorageWarning() {
|
renderStorageWarning() {
|
||||||
const host = document.getElementById("storageWarning");
|
const host = document.getElementById("storageWarning");
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
host.hidden = !HoldemState.saveFailed && !HoldemState.loadFailed;
|
host.hidden = !HoldemState.saveFailed && !HoldemState.loadFailed && !HoldemState.otherTabActive;
|
||||||
if (host.hidden) return;
|
if (host.hidden) return;
|
||||||
|
|
||||||
// Both can be true at once — a browser having trouble with storage tends to
|
// Any of these can be true at once — a browser having trouble with storage
|
||||||
// have it in both directions — and the one that explains an empty table goes
|
// tends to have it in both directions — and the one that explains an empty
|
||||||
// first, because that's the question the dealer is standing there asking.
|
// table goes first, because that's the question the dealer is standing there
|
||||||
|
// asking.
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
if (HoldemState.otherTabActive) {
|
||||||
|
lines.push(
|
||||||
|
"This table is open in another tab, and both are writing to it — " +
|
||||||
|
"whichever acts last wins, so work done in the other one can vanish. " +
|
||||||
|
"Close one, then reload this tab to pick up the table as it stands.");
|
||||||
|
}
|
||||||
if (HoldemState.loadFailed) {
|
if (HoldemState.loadFailed) {
|
||||||
lines.push(
|
lines.push(
|
||||||
"The saved table couldn't be read, so this one started empty. " +
|
"The saved table couldn't be read, so this one started empty. " +
|
||||||
|
|||||||
@@ -84,6 +84,30 @@ const HoldemState = {
|
|||||||
// Where an unreadable save is kept instead of being written over.
|
// Where an unreadable save is kept instead of being written over.
|
||||||
RECOVERY_KEY: "casino.holdem.state.v1.unreadable",
|
RECOVERY_KEY: "casino.holdem.state.v1.unreadable",
|
||||||
|
|
||||||
|
// True once another tab has written to this table. Two tabs each hold their own
|
||||||
|
// copy in memory and each save writes the whole thing, so the second one to act
|
||||||
|
// silently throws the first one's work away — measured: tab A deals and takes a
|
||||||
|
// call, tab B renames a seat, and the deal, the blinds and the call are gone with
|
||||||
|
// nothing said in either tab.
|
||||||
|
//
|
||||||
|
// Watched rather than merged. Replacing the table under a dealer's hands mid-hand
|
||||||
|
// would be its own hazard; knowing there are two of them is what they need.
|
||||||
|
otherTabActive: false,
|
||||||
|
onOtherTab: null,
|
||||||
|
|
||||||
|
// The browser fires this in every *other* tab, never the one that wrote, so our
|
||||||
|
// own saves can't trip it.
|
||||||
|
watchOtherTabs() {
|
||||||
|
if (typeof window === "undefined" || this.tabWatcher) return;
|
||||||
|
this.tabWatcher = event => {
|
||||||
|
if (event.key !== this.STORAGE_KEY) return;
|
||||||
|
if (this.otherTabActive) return;
|
||||||
|
this.otherTabActive = true;
|
||||||
|
if (typeof this.onOtherTab === "function") this.onOtherTab();
|
||||||
|
};
|
||||||
|
window.addEventListener("storage", this.tabWatcher);
|
||||||
|
},
|
||||||
|
|
||||||
// True when there was a saved table and it couldn't be read. Same pair as
|
// True when there was a saved table and it couldn't be read. Same pair as
|
||||||
// saveFailed above, for the same reason: the UI has to be able to say so.
|
// saveFailed above, for the same reason: the UI has to be able to say so.
|
||||||
loadFailed: false,
|
loadFailed: false,
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Noticing that the same table is open twice.
|
||||||
|
//
|
||||||
|
// Two tabs each hold their own copy in memory and each save writes the whole thing,
|
||||||
|
// so the second one to act throws the first one's work away. This doesn't merge them
|
||||||
|
// — replacing a table under a dealer's hands mid-hand would be its own hazard — it
|
||||||
|
// notices, which is what lets them decide.
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { HoldemState } = require("./load");
|
||||||
|
|
||||||
|
function withWindow() {
|
||||||
|
const listeners = [];
|
||||||
|
globalThis.window = {
|
||||||
|
addEventListener(type, fn) { if (type === "storage") listeners.push(fn); },
|
||||||
|
removeEventListener(type, fn) {
|
||||||
|
const i = listeners.indexOf(fn);
|
||||||
|
if (i >= 0) listeners.splice(i, 1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
listeners,
|
||||||
|
fire(key) { listeners.forEach(fn => fn({ key })); },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
HoldemState.otherTabActive = false;
|
||||||
|
HoldemState.onOtherTab = null;
|
||||||
|
HoldemState.tabWatcher = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
delete globalThis.window;
|
||||||
|
HoldemState.otherTabActive = false;
|
||||||
|
HoldemState.onOtherTab = null;
|
||||||
|
HoldemState.tabWatcher = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a write to this table from elsewhere is noticed", () => {
|
||||||
|
const win = withWindow();
|
||||||
|
let told = 0;
|
||||||
|
HoldemState.onOtherTab = () => { told++; };
|
||||||
|
HoldemState.watchOtherTabs();
|
||||||
|
|
||||||
|
win.fire(HoldemState.STORAGE_KEY);
|
||||||
|
|
||||||
|
assert.equal(HoldemState.otherTabActive, true);
|
||||||
|
assert.equal(told, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("somebody else's storage key is not our business", () => {
|
||||||
|
const win = withWindow();
|
||||||
|
let told = 0;
|
||||||
|
HoldemState.onOtherTab = () => { told++; };
|
||||||
|
HoldemState.watchOtherTabs();
|
||||||
|
|
||||||
|
win.fire("some.other.app.state");
|
||||||
|
win.fire(HoldemState.RECOVERY_KEY);
|
||||||
|
|
||||||
|
assert.equal(HoldemState.otherTabActive, false);
|
||||||
|
assert.equal(told, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("told once, however many times the other tab acts", () => {
|
||||||
|
const win = withWindow();
|
||||||
|
let told = 0;
|
||||||
|
HoldemState.onOtherTab = () => { told++; };
|
||||||
|
HoldemState.watchOtherTabs();
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) win.fire(HoldemState.STORAGE_KEY);
|
||||||
|
|
||||||
|
assert.equal(told, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("watching twice doesn't listen twice", () => {
|
||||||
|
const win = withWindow();
|
||||||
|
HoldemState.watchOtherTabs();
|
||||||
|
HoldemState.watchOtherTabs();
|
||||||
|
HoldemState.watchOtherTabs();
|
||||||
|
|
||||||
|
assert.equal(win.listeners.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no listener set is not an error", () => {
|
||||||
|
const win = withWindow();
|
||||||
|
HoldemState.onOtherTab = null;
|
||||||
|
HoldemState.watchOtherTabs();
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => win.fire(HoldemState.STORAGE_KEY));
|
||||||
|
assert.equal(HoldemState.otherTabActive, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no window at all is not an error", () => {
|
||||||
|
// The same modules are loaded in Node by the test harness.
|
||||||
|
delete globalThis.window;
|
||||||
|
HoldemState.tabWatcher = null;
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => HoldemState.watchOtherTabs());
|
||||||
|
assert.equal(HoldemState.otherTabActive, false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user