From c40772dee6b59f451c0451ee87e41d07fb86bd7a Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sun, 29 Dec 2019 11:42:30 +0100 Subject: [PATCH] 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. --- public/javascripts/account.js | 2 +- public/oops/diagnostics.html | 2 -- ui/@types/lichess/index.d.ts | 12 ++++++++++-- ui/analyse/src/study/chapterNewForm.ts | 2 +- ui/analyse/src/study/interfaces.ts | 1 - ui/analyse/src/study/studyCtrl.ts | 21 +++++++++------------ ui/analyse/src/study/studyXhr.ts | 4 ++-- ui/ceval/src/ctrl.ts | 4 ++-- ui/ceval/src/main.ts | 3 +-- ui/ceval/src/pool.ts | 3 +-- ui/lobby/src/boot.js | 4 ++-- ui/lobby/src/ctrl.ts | 4 ++-- ui/lobby/src/hookRepo.ts | 2 +- ui/lobby/src/xhr.ts | 2 +- ui/notify/src/ctrl.ts | 4 ++-- ui/round/src/cevalSub.ts | 10 ++++------ ui/site/src/socket.js | 9 +-------- ui/site/src/standalones/util.js | 20 +++++++++++++++++--- 18 files changed, 57 insertions(+), 52 deletions(-) diff --git a/public/javascripts/account.js b/public/javascripts/account.js index a9f3cbf9b2..207cc8ff34 100644 --- a/public/javascripts/account.js +++ b/public/javascripts/account.js @@ -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'); } }); }); diff --git a/public/oops/diagnostics.html b/public/oops/diagnostics.html index 616908698e..a2c1428e69 100644 --- a/public/oops/diagnostics.html +++ b/public/oops/diagnostics.html @@ -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', diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts index 029c9c41da..fecf2eee68 100644 --- a/ui/@types/lichess/index.d.ts +++ b/ui/@types/lichess/index.d.ts @@ -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 diff --git a/ui/analyse/src/study/chapterNewForm.ts b/ui/analyse/src/study/chapterNewForm.ts index f906db04c0..117e8488dd 100644 --- a/ui/analyse/src/study/chapterNewForm.ts +++ b/ui/analyse/src/study/chapterNewForm.ts @@ -90,7 +90,7 @@ export function ctrl(send: SocketSend, chapters: Prop, 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(); }, diff --git a/ui/analyse/src/study/interfaces.ts b/ui/analyse/src/study/interfaces.ts index 83ad09ef29..bcd83653b8 100644 --- a/ui/analyse/src/study/interfaces.ts +++ b/ui/analyse/src/study/interfaces.ts @@ -58,7 +58,6 @@ export interface StudyCtrl { onPremoveSet(): void; redraw: Redraw; trans: Trans; - sri: string; } export type Tab = 'intro' | 'members' | 'chapters'; diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index 74a0192e3f..cebc2d7e58 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -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 }; }; diff --git a/ui/analyse/src/study/studyXhr.ts b/ui/analyse/src/study/studyXhr.ts index bbe0d56150..7f1d1f1cea 100644 --- a/ui/analyse/src/study/studyXhr.ts +++ b/ui/analyse/src/study/studyXhr.ts @@ -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 }); diff --git a/ui/ceval/src/ctrl.ts b/ui/ceval/src/ctrl.ts index 3ab27860d0..3189ef826c 100644 --- a/ui/ceval/src/ctrl.ts +++ b/ui/ceval/src/ctrl.ts @@ -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(); diff --git a/ui/ceval/src/main.ts b/ui/ceval/src/main.ts index 4e5a8d48b5..5ded677f53 100644 --- a/ui/ceval/src/main.ts +++ b/ui/ceval/src/main.ts @@ -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(); }); diff --git a/ui/ceval/src/pool.ts b/ui/ceval/src/pool.ts index 52252b2eb3..97a56fbb51 100644 --- a/ui/ceval/src/pool.ts +++ b/ui/ceval/src/pool.ts @@ -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) { diff --git a/ui/lobby/src/boot.js b/ui/lobby/src/boot.js index 5dc384997f..4013bcc13b 100644 --- a/ui/lobby/src/boot.js +++ b/ui/lobby/src/boot.js @@ -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' }; diff --git a/ui/lobby/src/ctrl.ts b/ui/lobby/src/ctrl.ts index ff31fb128f..40d32a7005 100644 --- a/ui/lobby/src/ctrl.ts +++ b/ui/lobby/src/ctrl.ts @@ -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); }; diff --git a/ui/lobby/src/hookRepo.ts b/ui/lobby/src/hookRepo.ts index 2e6b9ed79e..515d6c2eca 100644 --- a/ui/lobby/src/hookRepo.ts +++ b/ui/lobby/src/hookRepo.ts @@ -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'; } diff --git a/ui/lobby/src/xhr.ts b/ui/lobby/src/xhr.ts index 3682bfd5df..4d692819ba 100644 --- a/ui/lobby/src/xhr.ts +++ b/ui/lobby/src/xhr.ts @@ -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, diff --git a/ui/notify/src/ctrl.ts b/ui/notify/src/ctrl.ts index e89872faa7..fdc1f6a908 100644 --- a/ui/notify/src/ctrl.ts +++ b/ui/notify/src/ctrl.ts @@ -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; diff --git a/ui/round/src/cevalSub.ts b/ui/round/src/cevalSub.ts index cd726fe0f2..302b48a5ea 100644 --- a/ui/round/src/cevalSub.ts +++ b/ui/round/src/cevalSub.ts @@ -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); } diff --git a/ui/site/src/socket.js b/ui/site/src/socket.js index 83e5dbce31..d416506da0 100644 --- a/ui/site/src/socket.js +++ b/ui/site/src/socket.js @@ -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", diff --git a/ui/site/src/standalones/util.js b/ui/site/src/standalones/util.js index e8823635c0..ac21355864 100644 --- a/ui/site/src/standalones/util.js +++ b/ui/site/src/standalones/util.js @@ -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 => ({