-
Notifications
You must be signed in to change notification settings - Fork 1
fix(playground): wait for browser error reports #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -135,6 +135,12 @@ const sseConnections = new Map<string, Set<WritableStreamDefaultWriter<Uint8Arra | |
| // Screenshot wait queue: when an update is posted, the API waits for the | ||
| // browser to send a screenshot back. This map stores resolve callbacks. | ||
| const screenshotWaiters = new Map<string, Array<(base64: string | null) => void>>() | ||
| const errorReportWaiters = new Map<string, Array<(report: ErrorReportPayload | null) => void>>() | ||
|
|
||
| export type ErrorReportPayload = { | ||
| errors: string[] | ||
| structuredErrors: PlaygroundError[] | ||
| } | ||
|
|
||
| export function addSSEConnection(sessionId: string, writer: WritableStreamDefaultWriter<Uint8Array>) { | ||
| let set = sseConnections.get(sessionId) | ||
|
|
@@ -203,6 +209,85 @@ export function resolveScreenshotWaiters(sessionId: string, base64: string) { | |
| } | ||
| } | ||
|
|
||
| export function waitForErrorReport(sessionId: string, timeoutMs: number): Promise<ErrorReportPayload | null> { | ||
| return new Promise((resolve) => { | ||
| const list = errorReportWaiters.get(sessionId) ?? [] | ||
| list.push(resolve) | ||
| errorReportWaiters.set(sessionId, list) | ||
| setTimeout(() => { | ||
| const current = errorReportWaiters.get(sessionId) | ||
| if (current) { | ||
| const idx = current.indexOf(resolve) | ||
| if (idx !== -1) { | ||
| current.splice(idx, 1) | ||
| if (current.length === 0) errorReportWaiters.delete(sessionId) | ||
| } | ||
| } | ||
| resolve(null) | ||
| }, timeoutMs) | ||
| }) | ||
| } | ||
|
|
||
| export function resolveErrorReportWaiters(sessionId: string, report: ErrorReportPayload) { | ||
| const list = errorReportWaiters.get(sessionId) | ||
| if (!list || list.length === 0) return | ||
| errorReportWaiters.delete(sessionId) | ||
| for (const resolve of list) { | ||
| resolve(report) | ||
| } | ||
| } | ||
|
|
||
| export async function waitForBrowserSyncResult(sessionId: string, timeoutMs: number): Promise<{ | ||
| screenshotBase64: string | null | ||
| errorReport: ErrorReportPayload | null | ||
| }> { | ||
| const screenshotPromise = waitForScreenshot(sessionId, timeoutMs).then((base64) => ({ | ||
| type: 'screenshot' as const, | ||
| base64, | ||
| })) | ||
| const errorPromise = waitForErrorReport(sessionId, timeoutMs).then((report) => ({ | ||
| type: 'errorReport' as const, | ||
| report, | ||
| })) | ||
|
|
||
| let waitForScreenshotEvent = true | ||
| let waitForErrorEvent = true | ||
| let screenshotBase64: string | null = null | ||
| let errorReport: ErrorReportPayload | null = null | ||
|
|
||
| while (waitForScreenshotEvent || waitForErrorEvent) { | ||
| const pending: Array< | ||
| Promise< | ||
| | { type: 'screenshot'; base64: string | null } | ||
| | { type: 'errorReport'; report: ErrorReportPayload | null } | ||
| > | ||
| > = [] | ||
|
|
||
| if (waitForScreenshotEvent) pending.push(screenshotPromise) | ||
| if (waitForErrorEvent) pending.push(errorPromise) | ||
|
|
||
| const next = await Promise.race(pending) | ||
|
|
||
| if (next.type === 'screenshot') { | ||
| waitForScreenshotEvent = false | ||
| screenshotBase64 = next.base64 | ||
| } else { | ||
| waitForErrorEvent = false | ||
| errorReport = next.report | ||
| } | ||
|
|
||
| const hasCompilationErrors = !!errorReport | ||
| && (errorReport.errors.length > 0 || errorReport.structuredErrors.length > 0) | ||
| const hasSuccessfulSync = screenshotBase64 !== null && errorReport !== null | ||
|
|
||
| if (hasCompilationErrors || hasSuccessfulSync) { | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return { screenshotBase64, errorReport } | ||
|
Comment on lines
+240
to
+288
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The state machine here is solid — I traced through all five scenarios:
One thing to note: when breaking early on compilation errors, the screenshot waiter remains registered until the 5s timeout fires. Functionally harmless (promise resolves to Also: |
||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Row type from SQLite | ||
| // --------------------------------------------------------------------------- | ||
|
|
@@ -354,6 +439,12 @@ export function setStructuredErrors(id: string, errors: PlaygroundError[]): void | |
| ).run(JSON.stringify(errors), id) | ||
| } | ||
|
|
||
| export function recordErrorReport(id: string, report: ErrorReportPayload): void { | ||
| setErrors(id, report.errors) | ||
| setStructuredErrors(id, report.structuredErrors) | ||
| resolveErrorReportWaiters(id, report) | ||
| } | ||
|
|
||
| export function setUniformValues(id: string, values: Record<string, unknown>): void { | ||
| db.prepare( | ||
| `UPDATE playground_sessions SET uniform_values_json = ?, updated_at = datetime('now') WHERE id = ?`, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good coverage of the key scenarios — error-first, screenshot-first, and the interleaved orderings.
Two optional gaps if you want full coverage:
{ screenshotBase64: null, errorReport: null }structuredErrorsonly: error report with emptyerrorsbut non-emptystructuredErrors— thehasCompilationErrorscheck handles it correctly but there's no test exercising that branchnit / optional