Skip to content

Commit db02cc8

Browse files
committed
fix: give docs consent frame loading its own startup budget
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
1 parent 3b8d5b6 commit db02cc8

2 files changed

Lines changed: 77 additions & 12 deletions

File tree

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

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ test('an essential read retries one failed frame without exposing the transient
390390
assert.throws(() => { f.client.readRetryReason = 'send-failed'; }, TypeError);
391391
});
392392

393-
test('an essential read retries one frame-load timeout within the same call', async (t) => {
393+
test('an explicit timeout overrides the frame-load budget', async (t) => {
394394
t.mock.timers.enable({ apis: ['setTimeout'] });
395395
const f = fixture({ autoLoad: false, timeoutMs: 80 });
396396
t.after(() => f.client.close());
@@ -413,20 +413,78 @@ test('an essential read retries one frame-load timeout within the same call', as
413413
assert.equal(f.failures.length, 0);
414414
});
415415

416-
test('iframe startup does not consume the broker response timeout', async (t) => {
416+
test('the default 30-second frame-load budget still permits only two attempts', async (t) => {
417417
t.mock.timers.enable({ apis: ['setTimeout'] });
418-
const f = fixture({ autoLoad: false, timeoutMs: 80 });
418+
const f = fixture({ autoLoad: false, timeoutMs: undefined });
419+
t.after(() => f.client.close());
420+
const read = f.client.read();
421+
const outcome = read.then(value => ({ value }), error => ({ error }));
422+
const firstFrame = f.frame;
423+
424+
t.mock.timers.tick(6_500);
425+
await new Promise(resolve => setImmediate(resolve));
426+
assert.equal(f.frame, firstFrame);
427+
assert.equal(firstFrame.removed, undefined);
428+
assert.equal(f.client.readRetryReason, null);
429+
430+
t.mock.timers.tick(23_500);
431+
await new Promise(resolve => setImmediate(resolve));
432+
const retryFrame = f.frame;
433+
assert.notEqual(retryFrame, firstFrame);
434+
assert.equal(firstFrame.removed, true);
435+
assert.equal(f.client.readRetryReason, 'frame-load-timeout');
436+
437+
t.mock.timers.tick(30_000);
438+
const result = await outcome;
439+
assert.equal(result.value, undefined);
440+
assert.equal(result.error?.code, 'frame-load-timeout');
441+
assert.equal(retryFrame.removed, true);
442+
assert.equal(f.frames.length, 2);
443+
assert.equal(f.sent.length, 0);
444+
assert.deepEqual(f.failures, [result.error]);
445+
});
446+
447+
test('the default broker response budget remains 6.5 seconds after the frame loads', async (t) => {
448+
t.mock.timers.enable({ apis: ['setTimeout'] });
449+
const f = fixture({ timeoutMs: undefined });
450+
t.after(() => f.client.close());
451+
const read = f.client.read();
452+
const outcome = read.then(value => ({ value }), error => ({ error }));
453+
await new Promise(resolve => setImmediate(resolve));
454+
const firstFrame = f.frame;
455+
456+
t.mock.timers.tick(6_499);
457+
await new Promise(resolve => setImmediate(resolve));
458+
assert.equal(f.frame, firstFrame);
459+
assert.equal(firstFrame.removed, undefined);
460+
461+
t.mock.timers.tick(1);
462+
await new Promise(resolve => setImmediate(resolve));
463+
assert.notEqual(f.frame, firstFrame);
464+
assert.equal(firstFrame.removed, true);
465+
assert.equal(f.client.readRetryReason, 'response-timeout');
466+
f.reply(unknown);
467+
assert.deepEqual(await outcome, { value: unknown });
468+
assert.equal(f.failures.length, 0);
469+
});
470+
471+
test('a separate frame-load budget lets startup wait without consuming the response timeout', async (t) => {
472+
t.mock.timers.enable({ apis: ['setTimeout'] });
473+
const f = fixture({ autoLoad: false, timeoutMs: 80, frameLoadTimeoutMs: 240 });
419474
t.after(() => f.client.close());
420475
const pending = f.client.read();
421476
const outcome = pending.then(value => ({ value }), error => ({ error }));
422477
const requestFrame = f.frame;
423478

424-
t.mock.timers.tick(50);
479+
t.mock.timers.tick(160);
480+
assert.equal(f.frame, requestFrame);
481+
assert.equal(requestFrame.removed, undefined);
482+
assert.equal(f.sent.length, 0);
425483
requestFrame.fireLoad();
426484
await new Promise(resolve => setImmediate(resolve));
427485
const request = f.sent.at(-1).data;
428486

429-
t.mock.timers.tick(50);
487+
t.mock.timers.tick(79);
430488
f.message({ ...envelope, id: request.id, ok: true, state: unknown }, {
431489
source: requestFrame.contentWindow,
432490
});
@@ -435,7 +493,7 @@ test('iframe startup does not consume the broker response timeout', async (t) =>
435493
assert.deepEqual(result.value, unknown);
436494
});
437495

438-
test('an essential read retries one response timeout with a fresh frame', async (t) => {
496+
test('an explicit timeout overrides the broker response budget', async (t) => {
439497
t.mock.timers.enable({ apis: ['setTimeout'] });
440498
const f = fixture({ timeoutMs: 80 });
441499
t.after(() => f.client.close());
@@ -658,8 +716,10 @@ test('a retained-refusal read superseded by a newer accepted choice resolves wit
658716
assert.equal(f.failures.length, 0);
659717
});
660718

661-
test('association timeout bounds frame readiness and prevents a late identifier request', async () => {
662-
const f = fixture({ timeoutMs: 100 });
719+
test('association keeps its 750ms total deadline despite a longer frame-load budget', async (t) => {
720+
t.mock.timers.enable({ apis: ['setTimeout'] });
721+
const f = fixture({ timeoutMs: 100, frameLoadTimeoutMs: 30_000 });
722+
t.after(() => f.client.close());
663723
const initial = await nextRequest(() => f.client.read());
664724
f.reply(accepted);
665725
await initial.promise;
@@ -668,9 +728,13 @@ test('association timeout bounds frame readiness and prevents a late identifier
668728
f.message({ ...envelope, type: 'changed', state: renewed });
669729

670730
let outcome = 'pending';
671-
const association = f.client.associate(renewed.generation, { timeoutMs: 5 });
731+
const association = f.client.associate(renewed.generation, { timeoutMs: 750 });
672732
association.then(() => { outcome = 'resolved'; }, () => { outcome = 'rejected'; });
673-
await new Promise(resolve => setTimeout(resolve, 20));
733+
t.mock.timers.tick(749);
734+
await new Promise(resolve => setImmediate(resolve));
735+
assert.equal(outcome, 'pending');
736+
t.mock.timers.tick(1);
737+
await new Promise(resolve => setImmediate(resolve));
674738
assert.equal(outcome, 'rejected');
675739
const lateFrame = f.frame;
676740
lateFrame.fireLoad();
@@ -679,7 +743,6 @@ test('association timeout bounds frame readiness and prevents a late identifier
679743
await assert.rejects(association, /tracking/i);
680744
assert.deepEqual(f.client.state, renewed);
681745
assert.equal(f.failures.length, 0);
682-
f.client.close();
683746
});
684747

685748
test('an unavailable association falls back without suspending accepted consent', async () => {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export function createSharedTrackingClient(options = {}) {
7373
const currentNavigator = options.navigator ?? currentWindow?.navigator ?? globalThis.navigator;
7474
const now = options.now ?? Date.now;
7575
const timeoutMs = options.timeoutMs ?? 6500;
76+
// Loading includes the bridge's module graph before responses can begin.
77+
const frameLoadTimeoutMs = options.frameLoadTimeoutMs ?? options.timeoutMs ?? 30_000;
7678
const bridge = new URL(options.bridgeUrl ?? SHARED_TRACKING_BRIDGE_URL);
7779
if (bridge.protocol !== 'https:' || bridge.username || bridge.password ||
7880
bridge.pathname !== '/tracking-bridge/' || bridge.search || bridge.hash) {
@@ -256,7 +258,7 @@ export function createSharedTrackingClient(options = {}) {
256258
candidate.remove();
257259
rejectReady(trackingError(code));
258260
};
259-
const timer = setTimeout(() => reset('frame-load-timeout'), timeoutMs);
261+
const timer = setTimeout(() => reset('frame-load-timeout'), frameLoadTimeoutMs);
260262
frameReadyCancel = reset;
261263
candidate.addEventListener('load', () => {
262264
if (settled) return;

0 commit comments

Comments
 (0)