make localStorage pubsub more robust (fixes #5832)

Safari sometimes fires the StorageEvent in the same document as well.
Move lichess.StrongSocket.sri to lichess.sri and use the unique id to
filter events.
pull/5836/head
Niklas Fiekas 2019-12-29 11:42:30 +01:00
parent c2981fca90
commit c40772dee6
18 changed files with 57 additions and 52 deletions

View File

@ -13,7 +13,7 @@ $(function() {
...lichess.formAjax($form),
success: function() {
$form.find('.saved').fadeIn();
lichess.storage.set('reload-round-tabs', Math.random());
lichess.storage.fire('reload-round-tabs');
}
});
});

View File

@ -59,8 +59,6 @@ function localStorageInfo() {
'analyse.ceval.threads',
'analyse.ceval.hash-size',
'analyse.ceval.infinite',
'ceval.pool.start',
'ceval.fen',
'just-notified',
'push-subscribed',
'grid',

View File

@ -5,6 +5,7 @@ interface Lichess {
requestIdleCallback(f: () => void): void;
dispatchEvent(el: HTMLElement | Window, eventName: string): void;
hasTouchEvents: boolean;
sri: string;
isCol1(): boolean;
storage: LichessStorageHelper;
tempStorage: LichessStorageHelper; // TODO: unused
@ -47,7 +48,6 @@ interface Lichess {
// socket.js
StrongSocket: {
sri: string
(url: string, version: number, cfg: any): any;
}
@ -124,6 +124,7 @@ interface LichessStorageHelper {
makeBoolean(k: string): LichessBooleanStorage;
get(k: string): string | null;
set(k: string, v: string): void;
fire(k: string, v?: string): void;
remove(k: string): void;
}
@ -131,7 +132,8 @@ interface LichessStorage {
get(): string | null;
set(v: any): void;
remove(): void;
listen(f: (e: StorageEvent) => void): void;
listen(f: (e: LichessStorageEvent) => void): void;
fire(v?: string): void;
}
interface LichessBooleanStorage {
@ -140,6 +142,12 @@ interface LichessBooleanStorage {
toggle(): void;
}
interface LichessStorageEvent {
sri: string;
nonce: number;
value?: string;
}
interface Window {
lichess: Lichess

View File

@ -90,7 +90,7 @@ export function ctrl(send: SocketSend, chapters: Prop<StudyChapterMeta[]>, setTa
d.initial = vm.initial();
d.sticky = study.vm.mode.sticky;
if (!d.pgn) send("addChapter", d);
else importPgn(study.data.id, d, study.sri);
else importPgn(study.data.id, d);
close();
setTab();
},

View File

@ -58,7 +58,6 @@ export interface StudyCtrl {
onPremoveSet(): void;
redraw: Redraw;
trans: Trans;
sri: string;
}
export type Tab = 'intro' | 'members' | 'chapters';

View File

@ -31,8 +31,6 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
const send = ctrl.socket.send;
const redraw = ctrl.redraw;
const sri: string = li.StrongSocket ? li.StrongSocket.sri : '';
const vm: StudyVm = (() => {
const isManualChapter = data.chapter.id !== data.position.chapterId;
const sticked = data.features.sticky && !ctrl.initialPath && !isManualChapter && !practiceData;
@ -294,7 +292,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
return xhrReload();
}
data.position.path = position.path;
if (who && who.s === sri) return;
if (who && who.s === li.sri) return;
ctrl.userJump(position.path);
redraw();
},
@ -310,7 +308,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
if (sticky && !vm.mode.sticky) redraw();
return;
}
if (sticky && who && who.s === sri) {
if (sticky && who && who.s === li.sri) {
data.position.path = position.path + node.id;
return;
}
@ -331,7 +329,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
setMemberActive(who);
if (wrongChapter(d)) return;
// deleter already has it done
if (who && who.s === sri) return;
if (who && who.s === li.sri) return;
if (!ctrl.tree.pathExists(d.p.path)) return xhrReload();
ctrl.tree.deleteNodeAt(position.path);
if (vm.mode.sticky) ctrl.jump(ctrl.path);
@ -342,7 +340,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
if (who && who.s === sri) return;
if (who && who.s === li.sri) return;
if (!ctrl.tree.pathExists(d.p.path)) return xhrReload();
ctrl.tree.promoteAt(position.path, d.toMainline);
if (vm.mode.sticky) ctrl.jump(ctrl.path);
@ -362,7 +360,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
descChapter(d) {
setMemberActive(d.w);
if (d.w && d.w.s === sri) return;
if (d.w && d.w.s === li.sri) return;
if (data.chapter.id === d.chapterId) {
data.chapter.description = d.desc;
chapterDesc.set(d.desc);
@ -371,7 +369,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
descStudy(d) {
setMemberActive(d.w);
if (d.w && d.w.s === sri) return;
if (d.w && d.w.s === li.sri) return;
data.description = d.desc;
studyDesc.set(d.desc);
redraw();
@ -380,7 +378,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
setMemberActive(d.w);
if (d.s && !vm.mode.sticky) vm.behind++;
if (d.s) data.position = d.p;
else if (d.w && d.w.s === sri) {
else if (d.w && d.w.s === li.sri) {
vm.mode.write = true;
vm.chapterId = d.p.chapterId;
}
@ -404,7 +402,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
if (who && who.s === sri) return;
if (who && who.s === li.sri) return;
ctrl.tree.setShapes(d.s, ctrl.path);
if (ctrl.path === position.path) ctrl.withCg(cg => cg.setShapes(d.s));
redraw();
@ -465,7 +463,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
liking(d) {
data.likes = d.l.likes;
if (d.w && d.w.s === sri) data.liked = d.l.me;
if (d.w && d.w.s === li.sri) data.liked = d.l.me;
redraw();
},
following_onlines: members.inviteForm.setFollowings,
@ -606,6 +604,5 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
}
return !!relay && relay.socketHandler(t, d);
},
sri
};
};

View File

@ -44,10 +44,10 @@ export function practiceComplete(chapterId: string, nbMoves: number) {
});
}
export function importPgn(studyId: string, data: any, sri: string) {
export function importPgn(studyId: string, data: any) {
return $.ajax({
method: 'POST',
url: `/study/${studyId}/import-pgn?sri=${sri}`,
url: `/study/${studyId}/import-pgn?sri=${window.lichess.sri}`,
data: data,
headers
});

View File

@ -143,7 +143,7 @@ export default function(opts: CevalOpts): CevalCtrl {
opts.emit(ev, work);
if (ev.fen !== lastEmitFen) {
lastEmitFen = ev.fen;
li.storage.set('ceval.fen', ev.fen);
li.storage.fire('ceval.fen', ev.fen);
}
});
@ -222,7 +222,7 @@ export default function(opts: CevalOpts): CevalCtrl {
// ask other tabs if a game is in progress
if (enabled()) {
li.storage.set('ceval.fen', 'start:' + Math.random());
li.storage.fire('ceval.fen', 'start');
li.storage.make('round.ongoing').listen(_ => {
enabled(false);
opts.redraw();

View File

@ -14,8 +14,7 @@ export function isEvalBetter(a: Tree.ClientEval, b?: Tree.ClientEval): boolean {
// stop when another tab starts. Listen only once here,
// as the ctrl can be instanciated several times.
// gotta do the click on the toggle to have it visually change.
window.lichess.storage.make('ceval.pool.start').listen(() => {
console.log('received ceval.pool.start');
window.lichess.storage.make('ceval.pool.start').listen(_ => {
const toggle = document.getElementById('analyse-toggle-ceval');
if (toggle && (toggle as HTMLInputElement).checked) toggle.click();
});

View File

@ -141,8 +141,7 @@ export class Pool {
};
start = (work: Work) => {
console.log('sending ceval.pool.start');
window.lichess.storage.set('ceval.pool.start', Date.now().toString());
window.lichess.storage.fire('ceval.pool.start');
this.getWorker().then(function(worker) {
worker.start(work);
}).catch(function(error) {

View File

@ -20,7 +20,7 @@ module.exports = function(cfg, element) {
var onFirstConnect = function() {
var gameId = getParameterByName('hook_like');
if (!gameId) return;
$.post('/setup/hook/' + lichess.StrongSocket.sri + '/like/' + gameId);
$.post('/setup/hook/' + lichess.sri + '/like/' + gameId);
lobby.setTab('real_time');
history.replaceState(null, null, '/');
};
@ -266,7 +266,7 @@ module.exports = function(cfg, element) {
var poolMember = hookToPoolMember(color, $form.serializeArray());
$.modal.close();
var call = {
url: $form.attr('action').replace(/sri-placeholder/, lichess.StrongSocket.sri),
url: $form.attr('action').replace(/sri-placeholder/, lichess.sri),
data: $form.serialize() + "&color=" + color,
type: 'post'
};

View File

@ -53,7 +53,7 @@ export default class LobbyController {
this.trans = opts.trans;
this.poolInStorage = li.storage.make('lobby.pool-in');
this.poolInStorage.listen(() => { // when another tab joins a pool
this.poolInStorage.listen(_ => { // when another tab joins a pool
this.leavePool();
redraw();
});
@ -186,7 +186,7 @@ export default class LobbyController {
poolIn = () => {
if (!this.poolMember) return;
this.poolInStorage.set(li.StrongSocket.sri);
this.poolInStorage.fire();
this.socket.poolIn(this.poolMember);
};

View File

@ -14,7 +14,7 @@ export function sort(ctrl: LobbyController, hooks: Hook[]) {
}
export function init(hook: Hook) {
hook.action = hook.sri === window.lichess.StrongSocket.sri ? 'cancel' : 'join';
hook.action = hook.sri === window.lichess.sri ? 'cancel' : 'join';
hook.variant = hook.variant || 'standard';
}

View File

@ -19,7 +19,7 @@ export function nowPlaying() {
export function anonPoolSeek(pool) {
return $.ajax({
method: 'POST',
url: '/setup/hook/' + window.lichess.StrongSocket.sri,
url: '/setup/hook/' + window.lichess.sri,
data: {
variant: 1,
timeMode: 1,

View File

@ -12,7 +12,7 @@ export default function ctrl(opts: NotifyOpts, redraw: Redraw): Ctrl {
const readAllStorage = li.storage.make('notify-read-all');
readAllStorage.listen(() => {
readAllStorage.listen(_ => {
if (data) {
data.unread = 0;
opts.setCount(0);
@ -25,7 +25,7 @@ export default function ctrl(opts: NotifyOpts, redraw: Redraw): Ctrl {
if (data.pager.currentPage === 1 && data.unread && opts.isVisible()) {
opts.setNotified();
data.unread = 0;
readAllStorage.set('' + Math.random()); // tell other tabs
readAllStorage.fire();
}
initiating = false;
scrolling = false;

View File

@ -16,13 +16,11 @@ export function subscribe(ctrl: RoundController): void {
if (!ctrl.data.game.rated && ctrl.opts.userId) return;
// bots can cheat alright
if (ctrl.data.player.user && ctrl.data.player.user.title === 'BOT') return;
li.storage.make('ceval.fen').listen(ev => {
const v = ev.newValue;
if (!v) return;
else if (v.startsWith('start:')) return li.storage.set('round.ongoing', v);
li.storage.make('ceval.fen').listen(e => {
if (e.value === 'start') return li.storage.fire('round.ongoing');
const d = ctrl.data, step = lastStep(ctrl.data);
if (!found && step.ply > 14 && ctrl.isPlaying() &&
truncateFen(step.fen) === truncateFen(v)) {
e.value && truncateFen(step.fen) === truncateFen(e.value)) {
$.post('/jslog/' + d.game.id + d.player.id + '?n=ceval');
found = true;
}
@ -31,5 +29,5 @@ export function subscribe(ctrl: RoundController): void {
}
export function publish(d: RoundData, move: ApiMove) {
if (d.opponent.ai) li.storage.set('ceval.fen', move.fen);
if (d.opponent.ai) li.storage.fire('ceval.fen', move.fen);
}

View File

@ -269,13 +269,6 @@ lichess.StrongSocket = function(url, version, settings) {
};
};
try {
const data = window.crypto.getRandomValues(new Uint8Array(9));
lichess.StrongSocket.sri = btoa(String.fromCharCode(...data)).replace(/[/+]/g, '_');
} catch(_) {
lichess.StrongSocket.sri = Math.random().toString(36).slice(2, 12);
}
lichess.StrongSocket.defaults = {
events: {
fen: function(e) {
@ -291,7 +284,7 @@ lichess.StrongSocket.defaults = {
}
},
params: {
sri: lichess.StrongSocket.sri
sri: lichess.sri
},
options: {
name: "unnamed",

View File

@ -8,6 +8,13 @@ lichess.dispatchEvent = (el, eventName) => el.dispatchEvent(new Event(eventName)
lichess.hasTouchEvents = 'ontouchstart' in window;
try {
const data = window.crypto.getRandomValues(new Uint8Array(9));
lichess.sri = btoa(String.fromCharCode(...data)).replace(/[/+]/g, '_');
} catch(_) {
lichess.sri = Math.random().toString(36).slice(2, 12);
}
lichess.isCol1 = (() => {
let isCol1Cache = 'init'; // 'init' | 'rec' | boolean
return () => {
@ -28,15 +35,22 @@ lichess.isCol1 = (() => {
const api = {
get: k => storage.getItem(k),
set: (k, v) => storage.setItem(k, v),
fire: (k, v) => storage.setItem(k, JSON.stringify({sri: lichess.sri, nonce: Math.random(), value: v})),
remove: k => storage.removeItem(k),
make: k => ({
get: () => api.get(k),
set: v => api.set(k, v),
fire: v => api.fire(k, v),
remove: () => api.remove(k),
listen: f => window.addEventListener('storage', e => {
if (e.key === k &&
e.storageArea === storage &&
e.newValue !== null) f(e);
if (e.key !== k || e.storageArea !== storage || e.newValue === null) return;
let parsed;
try {
parsed = JSON.parse(e.newValue);
} catch(_) {
return;
}
if (parsed.sri && parsed.sri !== lichess.sri) f(parsed);
})
}),
makeBoolean: k => ({