mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
fix(server-storage): forget the token for another server, listen before sign-in, adopt only stale records
- A server that answers as another backend URL or instance id than the remembered one makes the browser forget the stored token, so server A's token never reaches server B. - The session installs its page-hide and heartbeat listeners before it returns for sign-in, so a page holding another page load's writes still marks itself closed on unload. - Closed tabs are found by stale records only again: saved writes without a record may belong to a running tab whose record storage refused, and are left to that tab's own reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf715a7573
commit
4908803ceb
7 files changed
+94
-41
No files matched your search
@@ -91,6 +91,16 @@ describe('ServerStorageSession', () => {
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('marks the page closed on page hide also while sign-in is pending', async () => {
|
||||
const { session } = setup('token');
|
||||
const close = jest.spyOn(session.userData, 'close');
|
||||
|
||||
await session.start();
|
||||
window.dispatchEvent(new Event('pagehide'));
|
||||
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('starts with a stored token', async () => {
|
||||
localStorage.setItem(SERVER_ACCESS_TOKEN_STORAGE_KEY, 't');
|
||||
const { session, status } = setup('token');
|
||||
|
||||
@@ -76,6 +76,9 @@ export class ServerStorageSession {
|
||||
async start(): Promise<void> {
|
||||
this.backend.useServerStore(this.documentStore);
|
||||
this.status.activate(this.config.instance, () => void this.refresh());
|
||||
// Also while sign-in is pending: this page may already hold another
|
||||
// page load's writes, so it must mark itself closed on page hide.
|
||||
this.listen();
|
||||
if (this.config.instance.auth === 'token' && !readServerAccessToken()) {
|
||||
this.status.reportUnauthorized();
|
||||
return;
|
||||
@@ -86,7 +89,6 @@ export class ServerStorageSession {
|
||||
setTimeout(resolve, STARTUP_HYDRATE_WAIT_MS)
|
||||
),
|
||||
]);
|
||||
this.listen();
|
||||
}
|
||||
|
||||
/** Re-reads synced user data; also the offline retry. */
|
||||
|
||||
@@ -120,8 +120,9 @@ export function swapPage(
|
||||
}
|
||||
|
||||
/**
|
||||
* Page loads with saved writes or a record but no fresh record: closed
|
||||
* tabs nobody sends for (also when their record was lost).
|
||||
* Page loads whose record went stale: closed tabs nobody sends for. Saved
|
||||
* writes without any record are left alone: a tab that could save them but
|
||||
* not its record may still run, and only its own reload takes them over.
|
||||
*/
|
||||
export function orphanedPages(
|
||||
local: UserDataStorage,
|
||||
@@ -129,20 +130,13 @@ export function orphanedPages(
|
||||
page: string,
|
||||
now = Date.now()
|
||||
): string[] {
|
||||
const registry = readRegistry(local, instanceId);
|
||||
const pages = new Set<string>();
|
||||
for (const prefix of PAGE_PREFIXES) {
|
||||
for (const owner of pagesUnder(local, `${prefix}:${instanceId}:`)) {
|
||||
pages.add(owner);
|
||||
}
|
||||
}
|
||||
return [...pages].filter((owner) => {
|
||||
const record = registry[owner];
|
||||
return (
|
||||
owner !== page &&
|
||||
!(record && now - Number(record.at) < UNCONFIRMED_TTL_MS)
|
||||
);
|
||||
});
|
||||
return Object.entries(readRegistry(local, instanceId))
|
||||
.filter(
|
||||
([owner, record]) =>
|
||||
owner !== page &&
|
||||
!(now - Number(record.at) < UNCONFIRMED_TTL_MS)
|
||||
)
|
||||
.map(([owner]) => owner);
|
||||
}
|
||||
|
||||
/** Forgets a page load's saved writes, early edits and record. */
|
||||
|
||||
@@ -1284,43 +1284,54 @@ describe('ServerUserDataStorage with other devices', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a closed tab's saved writes whose record was lost", async () => {
|
||||
it('leaves saved writes without a record to the tab that saved them', async () => {
|
||||
const server = fakeServer({ 'xtream-favorites': { p1: ['S'] } });
|
||||
const shared = memoryStorage();
|
||||
const closed = new ServerUserDataStorage(
|
||||
{
|
||||
...server.api,
|
||||
sendJson: jest
|
||||
.fn()
|
||||
.mockRejectedValue(
|
||||
new ServerApiError(0, 'network', 'offline')
|
||||
),
|
||||
} as never,
|
||||
const tabStore = memoryStorage();
|
||||
const offline = {
|
||||
...server.api,
|
||||
sendJson: jest
|
||||
.fn()
|
||||
.mockRejectedValue(new ServerApiError(0, 'network', 'offline')),
|
||||
};
|
||||
const open = new ServerUserDataStorage(
|
||||
offline as never,
|
||||
shared,
|
||||
jest.fn(),
|
||||
'default',
|
||||
memoryStorage()
|
||||
tabStore
|
||||
);
|
||||
await closed.hydrate();
|
||||
closed.setItem('xtream-favorites', JSON.stringify({ p1: ['S', 'X'] }));
|
||||
await closed.flush();
|
||||
// Another tab's save once replaced the shared record: now it is gone.
|
||||
await open.hydrate();
|
||||
open.setItem('xtream-favorites', JSON.stringify({ p1: ['S', 'X'] }));
|
||||
await open.flush();
|
||||
// Storage refused the record but kept the saved writes.
|
||||
for (const key of [...shared.values.keys()]) {
|
||||
if (key.startsWith('iptvnator-server:unconfirmed:')) {
|
||||
shared.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
const next = new ServerUserDataStorage(
|
||||
const other = new ServerUserDataStorage(
|
||||
server.api as never,
|
||||
shared,
|
||||
jest.fn(),
|
||||
'default',
|
||||
memoryStorage()
|
||||
);
|
||||
await next.hydrate();
|
||||
await next.flush();
|
||||
await other.hydrate();
|
||||
await other.flush();
|
||||
expect(server.state['xtream-favorites']['p1']).toEqual(['S']);
|
||||
|
||||
// The tab's own reload takes them over.
|
||||
const reloaded = new ServerUserDataStorage(
|
||||
server.api as never,
|
||||
shared,
|
||||
jest.fn(),
|
||||
'default',
|
||||
tabStore
|
||||
);
|
||||
await reloaded.hydrate();
|
||||
await reloaded.flush();
|
||||
expect(server.state['xtream-favorites']['p1']).toEqual(['S', 'X']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user