mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
fix(server-storage): adopted writes without a known version conflict instead of overwriting
A page that never read the server has no version tags, so an adopted write went out unconditionally and replaced newer server data with its cached value. Adopted edits now use the never-matching version when this page knows none (and for a queued write without a tag), so the server answers 412 and the edits are replayed on its value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2320b64e01
commit
586ba74ea7
3 files changed
+45
-3
No files matched your search
@@ -24,6 +24,12 @@ export function adoptEdits(
|
||||
const key = `${namespace}|${playlistId}`;
|
||||
const adopted = uncertain(steps);
|
||||
const queued = writes.isQueued(key) ? writes.unsent(key) : undefined;
|
||||
// Without a server version this page read, the write must conflict and
|
||||
// be replayed on the server's value, never overwrite it blindly.
|
||||
const unknown =
|
||||
foreign ||
|
||||
(queued ? queued.tag : writes.tags.of(namespace, playlistId)) ===
|
||||
undefined;
|
||||
if (queued) {
|
||||
// This tab's queued edits are newer: they replay last.
|
||||
const all = [...adopted, ...queued.steps];
|
||||
@@ -32,12 +38,12 @@ export function adoptEdits(
|
||||
...queued,
|
||||
value,
|
||||
steps: all,
|
||||
tag: foreign ? UNKNOWN_TAG : queued.tag,
|
||||
tag: unknown ? UNKNOWN_TAG : queued.tag,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
const value = replaySteps(current, steps);
|
||||
writes.queue(namespace, playlistId, value, value, { foreign });
|
||||
writes.queue(namespace, playlistId, value, value, { foreign: unknown });
|
||||
const entry = writes.unsent(key);
|
||||
if (entry) {
|
||||
writes.put({ ...entry, steps: [...adopted, ...entry.steps] });
|
||||
|
||||
@@ -1182,5 +1182,39 @@ describe('ServerUserDataStorage with other devices', () => {
|
||||
|
||||
expect(server.state['xtream-favorites']['p1']).toEqual(['S', 'X']);
|
||||
});
|
||||
|
||||
it("replays its writes on the server's value when this page never read it", async () => {
|
||||
const server = fakeServer({ 'xtream-favorites': { p1: ['S'] } });
|
||||
const local = memoryStorage();
|
||||
const tabStore = memoryStorage();
|
||||
const clock = jest.spyOn(Date, 'now').mockReturnValue(1_000);
|
||||
await lastPageLoad(server, local, tabStore, async (page) => {
|
||||
await page.hydrate();
|
||||
page.setItem(
|
||||
'xtream-favorites',
|
||||
JSON.stringify({ p1: ['S', 'X'] })
|
||||
);
|
||||
await page.flush();
|
||||
});
|
||||
// Another device saves Z; this page's reads keep failing.
|
||||
server.state['xtream-favorites']['p1'] = ['S', 'Z'];
|
||||
const reloaded = new ServerUserDataStorage(
|
||||
{ ...server.api, getJson: jest.fn(offline) } as never,
|
||||
local,
|
||||
jest.fn(),
|
||||
'default',
|
||||
tabStore
|
||||
);
|
||||
|
||||
clock.mockReturnValue(1_000 + UNCONFIRMED_TTL_MS + 1);
|
||||
reloaded.touch();
|
||||
await reloaded.flush();
|
||||
await reloaded.flush();
|
||||
clock.mockRestore();
|
||||
|
||||
expect(
|
||||
[...(server.state['xtream-favorites']['p1'] as string[])].sort()
|
||||
).toEqual(['S', 'X', 'Z']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user