Skip to content

Commit 98da381

Browse files
committed
fix: settle docs tracking during frame replacement
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
1 parent 20c6e34 commit 98da381

2 files changed

Lines changed: 68 additions & 9 deletions

File tree

‎docs/site/scripts/shared-tracking-client.test.mjs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,59 @@ test('an authoritative newer push acknowledges its matching choice before the ol
177177
f.client.close();
178178
});
179179

180+
test('a validated generation change settles a readiness-waiting read before it is sent', async (t) => {
181+
const f = fixture();
182+
t.after(() => f.client.close());
183+
const initial = await nextRequest(() => f.client.read());
184+
f.reply(accepted);
185+
await initial.promise;
186+
const oldFrame = f.frame;
187+
const sentBeforeRead = f.sent.length;
188+
189+
const waitingRead = f.client.read();
190+
f.message({ ...envelope, type: 'changed', state: rejected });
191+
192+
assert.deepEqual(await waitingRead, rejected);
193+
await new Promise(resolve => setImmediate(resolve));
194+
assert.equal(f.sent.length, sentBeforeRead);
195+
assert.equal(oldFrame.removed, true);
196+
assert.deepEqual(f.client.state, rejected);
197+
assert.equal(f.failures.length, 0);
198+
});
199+
200+
test('a validated generation change settles only its matching readiness-waiting choice', async (t) => {
201+
const matching = fixture();
202+
t.after(() => matching.client.close());
203+
const initial = await nextRequest(() => matching.client.read());
204+
matching.reply(accepted);
205+
await initial.promise;
206+
207+
const matchingSentBeforeChoice = matching.sent.length;
208+
const refusal = matching.client.choose('rejected');
209+
matching.message({ ...envelope, type: 'changed', state: rejected });
210+
assert.deepEqual(await refusal, rejected);
211+
await new Promise(resolve => setImmediate(resolve));
212+
assert.equal(matching.sent.length, matchingSentBeforeChoice);
213+
assert.equal(matching.values.has(localRefusalKey), false);
214+
assert.equal(matching.failures.length, 0);
215+
});
216+
217+
test('a readiness-waiting acceptance cannot override a validated rejection', async (t) => {
218+
const conflicting = fixture();
219+
t.after(() => conflicting.client.close());
220+
const initial = await nextRequest(() => conflicting.client.read());
221+
conflicting.reply(accepted);
222+
await initial.promise;
223+
const conflictingSentBeforeChoice = conflicting.sent.length;
224+
const acceptance = conflicting.client.choose('accepted');
225+
conflicting.message({ ...envelope, type: 'changed', state: rejected });
226+
await assert.rejects(acceptance, /tracking/i);
227+
await new Promise(resolve => setImmediate(resolve));
228+
assert.equal(conflicting.sent.length, conflictingSentBeforeChoice);
229+
assert.deepEqual(conflicting.client.state, rejected);
230+
assert.equal(conflicting.failures.length, 0);
231+
});
232+
180233
test('a visible rejected tab recreates its essential bridge and observes another tab reaccepting', async () => {
181234
const f = fixture();
182235
const initial = await nextRequest(() => f.client.read());

‎docs/site/src/lib/shared-tracking-client.mjs‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,14 @@ export function createSharedTrackingClient(options = {}) {
273273
const resolveOnce = value => {
274274
if (settled) return;
275275
settled = true;
276+
pending.delete(message.id);
276277
clearTimeout(timer);
277278
resolve(value);
278279
};
279280
const rejectOnce = error => {
280281
if (settled) return;
281282
settled = true;
283+
pending.delete(message.id);
282284
clearTimeout(timer);
283285
reject(error);
284286
};
@@ -302,6 +304,17 @@ export function createSharedTrackingClient(options = {}) {
302304
rejectOnce(requestOptions.recoverUnavailable ? recoverableFailure() : fail());
303305
return;
304306
}
307+
// Generation replacement must also settle operations awaiting readiness.
308+
const item = {
309+
resolve: resolveOnce,
310+
reject: rejectOnce,
311+
timer,
312+
type,
313+
frame,
314+
recoverUnavailable: requestOptions.recoverUnavailable === true,
315+
...payload,
316+
};
317+
pending.set(message.id, item);
305318
readiness.then(readyFrame => {
306319
if (settled) return;
307320
if (closed || readyFrame !== frame) {
@@ -310,15 +323,8 @@ export function createSharedTrackingClient(options = {}) {
310323
}
311324
requestFrame = readyFrame;
312325
if (requestOptions.timeoutMs === undefined) timer = startTimer();
313-
pending.set(message.id, {
314-
resolve: resolveOnce,
315-
reject: rejectOnce,
316-
timer,
317-
type,
318-
frame: readyFrame,
319-
recoverUnavailable: requestOptions.recoverUnavailable === true,
320-
...payload,
321-
});
326+
item.frame = readyFrame;
327+
item.timer = timer;
322328
try {
323329
readyFrame.contentWindow.postMessage(message, bridge.origin);
324330
} catch {

0 commit comments

Comments
 (0)