Skip to content

Commit cc07e2c

Browse files
SisyphusZhengDevBot
andauthored
fix(fullstack): re-seed on a bound in tier-2 realtime recovery checks (#1078)
* fix(fullstack): re-seed on a bound in tier-2 realtime recovery checks The 1s settle (#1074) was not sufficient: postgres_changes has no backfill, and a single seed that lands while the re-subscribed channel's server-side binding is still activating is never delivered — 3/3 CI runs timed out at the recovery assertion while local runs (higher latency, larger natural slack) always passed. seedNoteUntilDelivered re-seeds up to 3 times with a 7s delivery assertion per attempt: recovery means delivery resumes, and each attempt still asserts real end-to-end delivery. Verified locally against the real project: 14/14 green. * fix(fullstack): unblock ci gates for the realtime reseed branch - regenerate the public interface snapshot (sha-only drift for ./open-dropdown from the hygiene sweep's anchor sync; no public declaration changes) - fullstack:cloudflare-config-check: grant --allow-read to the test invocation; the new queue-name parity test reads the workflow file --------- Co-authored-by: DevBot <devbot@openelement.dev>
1 parent ca0aa15 commit cc07e2c

3 files changed

Lines changed: 40 additions & 54 deletions

File tree

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"fullstack:boundary-check": "deno run --allow-read --allow-run tools/check-fullstack-boundary.ts",
6363
"fullstack:migrations-check": "deno run --allow-read tools/check-supabase-migrations.ts",
6464
"fullstack:evidence-freshness": "deno run --allow-env --allow-net=api.github.com tools/check-evidence-freshness.ts",
65-
"fullstack:cloudflare-config-check": "deno test tools/render-cloudflare-async-config.test.ts && deno task --cwd examples/supabase-cloudflare-starter build && deno task --cwd examples/supabase-cloudflare-starter nitro:build && deno run --allow-read --allow-write tools/render-cloudflare-async-config.ts examples/supabase-cloudflare-starter/wrangler.jsonc examples/supabase-cloudflare-starter/.wrangler-async.generated.json && deno run -A npm:wrangler@4.123.0 deploy --dry-run --config examples/supabase-cloudflare-starter/.wrangler-async.generated.json && rm examples/supabase-cloudflare-starter/.wrangler-async.generated.json",
65+
"fullstack:cloudflare-config-check": "deno test --allow-read tools/render-cloudflare-async-config.test.ts && deno task --cwd examples/supabase-cloudflare-starter build && deno task --cwd examples/supabase-cloudflare-starter nitro:build && deno run --allow-read --allow-write tools/render-cloudflare-async-config.ts examples/supabase-cloudflare-starter/wrangler.jsonc examples/supabase-cloudflare-starter/.wrangler-async.generated.json && deno run -A npm:wrangler@4.123.0 deploy --dry-run --config examples/supabase-cloudflare-starter/.wrangler-async.generated.json && rm examples/supabase-cloudflare-starter/.wrangler-async.generated.json",
6666
"arch:check": "deno run --allow-read --allow-run tools/check-architecture-contract.ts",
6767
"type-safety:check": "deno run --allow-read tools/check-type-safety.ts",
6868
"deno-api:check": "deno run --allow-read --allow-env tools/check-deno-api-free.ts",

docs/release/public-interface-snapshot.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@
438438
"reExportedNames": []
439439
},
440440
"./open-dropdown": {
441-
"sha256": "ca5847021381c99054b74d11d24de01a5fdf818e5654bf22f66edc76de2f3238",
441+
"sha256": "ee57f9a630f32d695b7869060e1132435d4f8b2e7caf7a326cc28e0360a84d7a",
442442
"publicDeclarations": [
443443
"export const tagName = 'open-dropdown';",
444444
"export class OpenDropdown extends OpenElement {"

tools/smoke-supabase-browser.ts

Lines changed: 38 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* secrets through the runner environment. This script never prints or writes
66
* those values; its report contains check names only.
77
*/
8-
import { chromium } from 'npm:playwright@1.59.1';
8+
import { chromium, type Locator } from 'npm:playwright@1.59.1';
99

1010
function required(name: string): string {
1111
const value = Deno.env.get(name);
@@ -32,6 +32,41 @@ async function record(check: string): Promise<void> {
3232
);
3333
}
3434

35+
/**
36+
* Seed a note via the service role until the subscribed island renders it.
37+
* postgres_changes has no backfill: an insert that lands while the
38+
* server-side binding is still activating (the join is already acked) is
39+
* never delivered, so a single zero-notice seed false-fails on low-latency
40+
* networks (CI runners near the provider). Recovery means delivery resumes —
41+
* re-seed on a bound while keeping each attempt's delivery assertion intact.
42+
*/
43+
async function seedNoteUntilDelivered(
44+
live: Locator,
45+
marker: string,
46+
title: string,
47+
): Promise<void> {
48+
for (let attempt = 1; attempt <= 3; attempt++) {
49+
const insert = await fetch(`${supabaseUrl}/rest/v1/notes`, {
50+
method: 'POST',
51+
headers: {
52+
apikey: serviceRoleKey,
53+
authorization: `Bearer ${serviceRoleKey}`,
54+
'content-type': 'application/json',
55+
prefer: 'return=minimal',
56+
},
57+
body: JSON.stringify({ user_id: userId, title, body: marker }),
58+
});
59+
if (!insert.ok) throw new Error(`Realtime seed failed with HTTP ${insert.status}`);
60+
const delivered = await live
61+
.locator('#live-events')
62+
.getByText(marker, { exact: true })
63+
.waitFor({ state: 'visible', timeout: 7_000 })
64+
.then(() => true, () => false);
65+
if (delivered) return;
66+
}
67+
throw new Error(`Realtime seed "${marker}" was not delivered after 3 bounded attempts`);
68+
}
69+
3570
const browser = await chromium.launch();
3671
try {
3772
const context = await browser.newContext();
@@ -206,33 +241,8 @@ try {
206241
state: 'visible',
207242
timeout: 20_000,
208243
});
209-
// Hosted Realtime acknowledges the join before the postgres_changes binding
210-
// finishes activating server-side; a zero-slack insert can land inside that
211-
// window and never be delivered. Low-latency CI runners hit the window,
212-
// high-latency dev machines do not — give the binding a beat before seeding.
213-
await page.waitForTimeout(1_000);
214244
const recoveredMarker = `browser-realtime-recovered-${runId}`;
215-
const recoveredInsert = await fetch(`${supabaseUrl}/rest/v1/notes`, {
216-
method: 'POST',
217-
headers: {
218-
apikey: serviceRoleKey,
219-
authorization: `Bearer ${serviceRoleKey}`,
220-
'content-type': 'application/json',
221-
prefer: 'return=minimal',
222-
},
223-
body: JSON.stringify({
224-
user_id: userId,
225-
title: 'realtime recovery smoke',
226-
body: recoveredMarker,
227-
}),
228-
});
229-
if (!recoveredInsert.ok) {
230-
throw new Error(`Recovered Realtime seed failed with HTTP ${recoveredInsert.status}`);
231-
}
232-
await live.locator('#live-events').getByText(recoveredMarker, { exact: true }).waitFor({
233-
state: 'visible',
234-
timeout: 20_000,
235-
});
245+
await seedNoteUntilDelivered(live, recoveredMarker, 'realtime recovery smoke');
236246
await record('browser-realtime-offline-online-recovery-delivers');
237247

238248
const refreshedSession = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=password`, {
@@ -250,32 +260,8 @@ try {
250260
(element, token) => element.setAttribute('data-access-token', token),
251261
refreshedAccessToken,
252262
);
253-
// Same settle as the reconnect case above: the server must apply the new
254-
// access token before the next insert is evaluated under RLS.
255-
await page.waitForTimeout(1_000);
256-
257263
const refreshedMarker = `browser-realtime-refreshed-${runId}`;
258-
const refreshedInsert = await fetch(`${supabaseUrl}/rest/v1/notes`, {
259-
method: 'POST',
260-
headers: {
261-
apikey: serviceRoleKey,
262-
authorization: `Bearer ${serviceRoleKey}`,
263-
'content-type': 'application/json',
264-
prefer: 'return=minimal',
265-
},
266-
body: JSON.stringify({
267-
user_id: userId,
268-
title: 'realtime refreshed token smoke',
269-
body: refreshedMarker,
270-
}),
271-
});
272-
if (!refreshedInsert.ok) {
273-
throw new Error(`Refreshed-token Realtime seed failed with HTTP ${refreshedInsert.status}`);
274-
}
275-
await live.locator('#live-events').getByText(refreshedMarker, { exact: true }).waitFor({
276-
state: 'visible',
277-
timeout: 20_000,
278-
});
264+
await seedNoteUntilDelivered(live, refreshedMarker, 'realtime refreshed token smoke');
279265
await record('browser-realtime-refreshed-jwt-delivers');
280266

281267
await live.evaluate((element) => element.remove());

0 commit comments

Comments
 (0)