mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
fix(server-storage): take over a reload's writes once the previous page load stops looking live
When the close flag could not be saved (storage full), a reload took the previous page load for a running duplicated tab and left its saved writes unsent. The page now remembers that page load and its 30 s touch takes it over as soon as its record is closed, stale or gone, so the session's retry sends the writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
37c14d7d89
commit
d45210c4f7
3 files changed
+77
-21
No files matched your search
@@ -230,7 +230,9 @@ settings and guards read them in every build.
|
||||
is saved, at the latest with this page's next saved copy). A page marks
|
||||
its record closed on `pagehide` (not into the back-forward cache), and
|
||||
only a closed, stale or missing record is taken over: a duplicated tab,
|
||||
which copies `sessionStorage`, leaves the running tab its writes; the writes of a tab closed before it
|
||||
which copies `sessionStorage`, leaves the running tab its writes. A page
|
||||
load that still looked live (or whose close flag storage refused) is
|
||||
checked again every 30 s and taken over once it does not; the writes of a tab closed before it
|
||||
could send them are queued, ahead of its own, by the next re-read in
|
||||
another tab once that tab's record has gone stale; the result replays the
|
||||
closed tab's edits first and this tab's newer ones last. Tabs never clear each other's.
|
||||
|
||||
@@ -60,6 +60,8 @@ 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;
|
||||
/** Closed tabs' page loads this one took over (once each). */
|
||||
private readonly adopted = new Set<string>();
|
||||
/** Page loads taken over whose copy stays until this one saves. */
|
||||
@@ -99,11 +101,11 @@ export class ServerUserDataStorage implements UserDataStorage {
|
||||
writeFailed: onWriteFailure,
|
||||
});
|
||||
claimCache(local, instanceId);
|
||||
const previous = swapPage(tabStore, instanceId, this.tabId);
|
||||
// A duplicated tab copies the page load of a tab that still runs.
|
||||
if (previous && !pageIsLive(local, instanceId, previous)) {
|
||||
this.takeOver(previous);
|
||||
}
|
||||
// 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`
|
||||
// takes it over once it no longer looks live.
|
||||
this.awaited = swapPage(tabStore, instanceId, this.tabId);
|
||||
this.touch();
|
||||
}
|
||||
|
||||
/** The page unloads: a reload of this tab may take its writes over. */
|
||||
@@ -183,6 +185,11 @@ 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)) {
|
||||
this.awaited = null;
|
||||
this.takeOver(page);
|
||||
}
|
||||
if (this.hasUnsentWrites()) {
|
||||
this.publishUnconfirmed();
|
||||
}
|
||||
@@ -208,30 +215,27 @@ export class ServerUserDataStorage implements UserDataStorage {
|
||||
}
|
||||
|
||||
setItem(key: string, value: string): void {
|
||||
if (isServerUserStateNamespace(key)) {
|
||||
this.write(key, value);
|
||||
} else {
|
||||
this.local.setItem(key, value);
|
||||
}
|
||||
this.write(key, value);
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
if (isServerUserStateNamespace(key)) {
|
||||
this.write(key, null);
|
||||
} else {
|
||||
this.local.removeItem(key);
|
||||
}
|
||||
this.write(key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A caller's new map. When the cache changed since the caller read it
|
||||
* (another tab, a re-read), only the entries it changed are applied to
|
||||
* the cache as it is now; the rest stays.
|
||||
* the cache as it is now; the rest stays. Other keys use plain storage.
|
||||
*/
|
||||
private write(
|
||||
namespace: ServerUserStateNamespace,
|
||||
value: string | null
|
||||
): void {
|
||||
private write(namespace: string, value: string | null): void {
|
||||
if (!isServerUserStateNamespace(namespace)) {
|
||||
if (value === null) {
|
||||
this.local.removeItem(namespace);
|
||||
} else {
|
||||
this.local.setItem(namespace, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const current = this.cache.read(namespace);
|
||||
const before = parseMap(current ?? '{}');
|
||||
const read = this.handedOut.has(namespace)
|
||||
|
||||
@@ -1017,4 +1017,54 @@ describe('ServerUserDataStorage with other devices', () => {
|
||||
)
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('takes over a reload whose close flag was lost once it goes quiet', async () => {
|
||||
const server = fakeServer({ 'xtream-favorites': { p1: ['S'] } });
|
||||
const local = memoryStorage();
|
||||
const tabStore = memoryStorage();
|
||||
const clock = jest.spyOn(Date, 'now').mockReturnValue(1_000);
|
||||
const before = new ServerUserDataStorage(
|
||||
{
|
||||
...server.api,
|
||||
sendJson: jest
|
||||
.fn()
|
||||
.mockRejectedValue(
|
||||
new ServerApiError(0, 'network', 'offline')
|
||||
),
|
||||
} as never,
|
||||
local,
|
||||
jest.fn(),
|
||||
'default',
|
||||
tabStore
|
||||
);
|
||||
await before.hydrate();
|
||||
before.setItem('xtream-favorites', JSON.stringify({ p1: ['S', 'X'] }));
|
||||
await before.flush();
|
||||
// Storage is full when the page unloads: the close flag is lost.
|
||||
const setItem = local.setItem;
|
||||
local.setItem = () => {
|
||||
throw new DOMException('full', 'QuotaExceededError');
|
||||
};
|
||||
before.close();
|
||||
local.setItem = setItem;
|
||||
|
||||
const reloaded = new ServerUserDataStorage(
|
||||
server.api as never,
|
||||
local,
|
||||
jest.fn(),
|
||||
'default',
|
||||
tabStore
|
||||
);
|
||||
// It looks like a duplicated tab of a running one: nothing taken.
|
||||
expect(reloaded.hasUnsentWrites()).toBe(false);
|
||||
|
||||
clock.mockReturnValue(1_000 + UNCONFIRMED_TTL_MS + 1);
|
||||
reloaded.touch();
|
||||
expect(reloaded.hasUnsentWrites()).toBe(true);
|
||||
await reloaded.hydrate();
|
||||
await reloaded.flush();
|
||||
clock.mockRestore();
|
||||
|
||||
expect(server.state['xtream-favorites']['p1']).toEqual(['S', 'X']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user