feedback7 G-TOOLS: a reconnect token cannot be claimed twice by two tabs started apart (PR #299 by @siruimei07)

Applied as-is. A tab that is still choosing a reconnect token now answers another tab's query for the same candidate hashes, so a tab started during the 150 ms window cannot adopt a token the earlier tab is also considering. The smaller tab id still wins, and a tab that already holds the token keeps it.

Tests: node --test test/client-identity-session.test.js test/client-static.test.js — 319 pass, 0 fail.
This commit is contained in:
siruimei07 authored and sganggs committed 2026-10-08 18:41:33 +08:00
1 parent ae22335cfe
commit 7c307d8613
3 files changed
+204 -4

No files matched your search

+52
View File
@@ -790,6 +790,58 @@ describe('identity (reconnect-token selection across tabs)', () => {
assert.equal(ty, null);
});
for (const [firstId, secondId, expected] of [
['a-tab', 'b-tab', ['tokShared', null]],
['b-tab', 'a-tab', [null, 'tokShared']],
]) {
test(`staggered channels: ${firstId} starts before ${secondId}`, async () => {
const { createIdentity, CLAIM_QUERY_MS } = await mod('net.js');
const hub = channelHub(), timers = fakeTimers(), local = memStorage();
local.setItem('sp.tokens', JSON.stringify(['tokShared']));
const firstSession = memStorage(), secondSession = memStorage();
const first = createIdentity({ local, session: firstSession, tabId: firstId,
channel: hub.create(), setTimeout: timers.setTimeout });
const firstPick = first.init();
timers.advance(30);
// This channel did not exist when the first tab broadcast its query.
const second = createIdentity({ local, session: secondSession, tabId: secondId,
channel: hub.create(), setTimeout: timers.setTimeout });
const secondPick = second.init();
await new Promise((r) => setImmediate(r));
timers.advance(CLAIM_QUERY_MS - 30);
assert.equal(await firstPick, expected[0]);
timers.advance(30);
assert.equal(await secondPick, expected[1]);
assert.deepEqual([first.getToken(), second.getToken()], expected);
assert.deepEqual([firstSession.getItem('sp.token'), secondSession.getItem('sp.token')], expected);
});
}
test('staggered elections reserve fallback candidates while a live owner keeps its token', async () => {
const { createIdentity, CLAIM_QUERY_MS } = await mod('net.js');
const hub = channelHub(), timers = fakeTimers(), local = memStorage();
const owner = createIdentity({ local, session: memStorage(), tabId: 'z-owner', channel: hub.create() });
await owner.init();
owner.saveToken('tokOwned');
local.setItem('sp.tokens', JSON.stringify(['tokOwned', 'tokFree']));
const first = createIdentity({ local, session: memStorage(), tabId: 'a-tab',
channel: hub.create(), setTimeout: timers.setTimeout });
const firstPick = first.init();
timers.advance(30);
const duplicateSession = memStorage();
duplicateSession.setItem('sp.token', 'tokOwned');
const second = createIdentity({ local, session: duplicateSession, tabId: 'b-tab',
channel: hub.create(), setTimeout: timers.setTimeout });
const secondPick = second.init();
await new Promise((r) => setImmediate(r));
timers.advance(CLAIM_QUERY_MS - 30);
assert.equal(await firstPick, 'tokFree');
timers.advance(30);
assert.equal(await secondPick, null);
assert.equal(duplicateSession.getItem('sp.token'), null, 'the copied live token is discarded');
assert.equal(owner.getToken(), 'tokOwned', 'a live owner outranks even a smaller tab id');
});
test('without BroadcastChannel only the own token is used; init is idempotent; welcome during init wins', async () => {
const { createIdentity } = await mod('net.js');
const local = memStorage();