mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix(server-storage): a duplicate leaves a running tab's kept page loads to it
A duplicated tab copies the tab's session storage, so after a reload that kept older page loads its pointer lists them too, and the duplicate took them over at once, although the tab still running owns those edits and may have newer ones queued; its replay could then write older watch progress over newer. When the previous page load still runs, the kept ones now wait with it and `touch` adopts them, oldest first, once it no longer looks live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3a493fa7f
commit
c65502a65d
3 files changed
+57
-13
No files matched your search
@@ -237,7 +237,8 @@ settings and guards read them in every build.
|
||||
writes over and sends them first (the old copy goes only once the new one
|
||||
is saved, at the latest with this page's next saved copy; until then the
|
||||
tab's pointer names the kept page loads too, so another reload finds
|
||||
them even without their registry records). A page marks
|
||||
them even without their registry records; a duplicate of a tab that
|
||||
still runs leaves them, like that tab's page load, to it). A page marks
|
||||
its record closed on `pagehide`, and it stays closed when a page-hide
|
||||
write failing afterwards rewrites it (not into the back-forward cache, where
|
||||
its record goes stale, as in a background tab the browser suspends;
|
||||
|
||||
@@ -539,4 +539,46 @@ describe('ServerUserDataStorage taking over other page loads', () => {
|
||||
|
||||
expect(server.state['xtream-favorites']['p1']).toEqual(['S', 'X']);
|
||||
});
|
||||
|
||||
it("leaves a running tab's kept page loads to it in a duplicate", async () => {
|
||||
const server = fakeServer({ 'xtream-favorites': { p1: ['S'] } });
|
||||
const local = memoryStorage();
|
||||
const tabStore = memoryStorage();
|
||||
const api = {
|
||||
...server.api,
|
||||
sendJson: jest.fn(() =>
|
||||
Promise.reject(new ServerApiError(0, 'network', 'offline'))
|
||||
),
|
||||
} as never;
|
||||
const page = (store: ReturnType<typeof memoryStorage>) =>
|
||||
new ServerUserDataStorage(api, local, jest.fn(), 'default', store);
|
||||
const first = page(tabStore);
|
||||
await first.hydrate();
|
||||
first.setItem('xtream-favorites', JSON.stringify({ p1: ['S', 'X'] }));
|
||||
await first.flush();
|
||||
first.close();
|
||||
// The reload cannot save its copy: it keeps the first one's.
|
||||
const setItem = local.setItem;
|
||||
local.setItem = (key: string, value: string) => {
|
||||
if (key.startsWith('iptvnator-server:unsent-writes:')) {
|
||||
throw new DOMException('full', 'QuotaExceededError');
|
||||
}
|
||||
setItem(key, value);
|
||||
};
|
||||
const running = page(tabStore);
|
||||
expect(running.hasUnsentWrites()).toBe(true);
|
||||
|
||||
// A duplicate copies the tab's session storage, pointer and all.
|
||||
const copied = memoryStorage();
|
||||
for (const key of tabStore.keys()) {
|
||||
copied.setItem(key, tabStore.getItem(key) ?? '');
|
||||
}
|
||||
const duplicate = page(copied);
|
||||
|
||||
// The running tab owns those writes and may hold newer ones.
|
||||
expect(duplicate.hasUnsentWrites()).toBe(false);
|
||||
running.close();
|
||||
duplicate.touch();
|
||||
expect(duplicate.hasUnsentWrites()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -61,7 +61,7 @@ export class ServerUserDataStorage implements UserDataStorage {
|
||||
private readonly deferred = new Map<string, DeferredEdit>();
|
||||
private hydrated = false;
|
||||
/** The previous page load in this tab, while it still looks live. */
|
||||
private awaited: string | null = null;
|
||||
private awaited: string[] = [];
|
||||
/** A record stands for this page: a stale one is adopted elsewhere. */
|
||||
private recorded = false;
|
||||
/** Playlists deleted here, in order: a read sent earlier skips them. */
|
||||
@@ -108,14 +108,14 @@ export class ServerUserDataStorage implements UserDataStorage {
|
||||
claimCache(local, instanceId);
|
||||
const [previous, ...kept] = swapPage(tabStore, instanceId, this.tabId);
|
||||
// Older page loads whose copies a reload kept come first.
|
||||
kept.forEach((page) => this.takeOver(page));
|
||||
// A duplicated tab copies the page load of a tab that still runs;
|
||||
// so does a reload whose close flag could not be saved: `touch`
|
||||
// adopts it once it no longer looks live.
|
||||
const pages = previous ? [...kept, previous] : kept;
|
||||
// A duplicated tab copies the page load of a tab that still runs,
|
||||
// and the ones it keeps; so does a reload whose close flag could not
|
||||
// be saved: `touch` adopts them once it no longer looks live.
|
||||
if (previous && pageIsLive(local, instanceId, previous)) {
|
||||
this.awaited = previous;
|
||||
} else if (previous) {
|
||||
this.takeOver(previous);
|
||||
this.awaited = pages;
|
||||
} else {
|
||||
pages.forEach((page) => this.takeOver(page));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,11 +173,12 @@ export class ServerUserDataStorage implements UserDataStorage {
|
||||
|
||||
/** Tells other tabs this one is alive and still sending its writes. */
|
||||
touch(): void {
|
||||
const page = this.awaited;
|
||||
if (page && !pageIsLive(this.local, this.instanceId, page)) {
|
||||
const pages = this.awaited;
|
||||
const owner = pages[pages.length - 1];
|
||||
if (owner && !pageIsLive(this.local, this.instanceId, owner)) {
|
||||
// This page may have read and edited since: adopt, not restore.
|
||||
this.awaited = null;
|
||||
this.adoptPage(page);
|
||||
this.awaited = [];
|
||||
pages.forEach((page) => this.adoptPage(page));
|
||||
}
|
||||
// A tab with nothing to sync never re-reads, which is where closed
|
||||
// tabs' saved writes are otherwise picked up.
|
||||
|
||||
Reference in new issue
Block a user