Skip to content

Commit 010e269

Browse files
authored
Merge pull request #2 from contember/fixes
fix: fire-and-forget query execution + bugfixes
2 parents 1c6223e + 63b3d5d commit 010e269

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2507
-635
lines changed

bun.lock

Lines changed: 36 additions & 78 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

electrobun.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@ export default {
2424
bundleCEF: true,
2525
defaultRenderer: 'cef',
2626
icon: 'assets/icon.png',
27+
chromiumFlags: {
28+
'disable-gpu': false,
29+
'disable-gpu-compositing': false,
30+
'disable-gpu-sandbox': false,
31+
'enable-software-rasterizer': false,
32+
'force-software-rasterizer': false,
33+
'disable-accelerated-2d-canvas': false,
34+
'disable-accelerated-video-decode': false,
35+
'disable-accelerated-video-encode': false,
36+
'disable-gpu-memory-buffer-video-frames': false,
37+
},
2738
},
2839
win: {
2940
bundleCEF: false,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@sqlite.org/sqlite-wasm": "^3.51.2-build6",
4646
"@tanstack/solid-virtual": "^3.13.6",
4747
"codemirror": "^6.0.1",
48-
"electrobun": "1.14.4",
48+
"electrobun": "1.16.0",
4949
"lucide-solid": "^0.575.0",
5050
"simple-icons": "^16.10.0"
5151
},

src/backend-desktop/index.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,15 @@ emitToFrontend = (channel: string, payload: unknown) => {
204204
}
205205

206206
// Wire up BE→FE notifications after window creation
207-
connectionManager.onStatusChanged((event) => {
207+
connectionManager.onSessionDead((event) => {
208+
sessionManager.handleSessionDead(event.sessionId)
209+
emitToFrontend!('session.changed', {
210+
connectionId: event.connectionId,
211+
sessions: sessionManager.listSessions(event.connectionId),
212+
})
213+
})
214+
215+
connectionManager.onStatusChanged(async (event) => {
208216
emitToFrontend!('connections.statusChanged', {
209217
connectionId: event.connectionId,
210218
state: event.state,
@@ -224,16 +232,17 @@ connectionManager.onStatusChanged((event) => {
224232

225233
// Restore sessions after successful reconnect
226234
if (event.state === 'connected') {
227-
sessionManager.handleConnectionRestored(event.connectionId).then((restored) => {
235+
try {
236+
const restored = await sessionManager.handleConnectionRestored(event.connectionId)
228237
if (restored.length > 0) {
229238
emitToFrontend!('session.changed', {
230239
connectionId: event.connectionId,
231240
sessions: restored,
232241
})
233242
}
234-
}).catch((err) => {
243+
} catch (err) {
235244
console.warn('Session restoration failed:', err instanceof Error ? err.message : err)
236-
})
245+
}
237246
}
238247
})
239248

src/backend-shared/db/driver.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export interface DatabaseDriver extends SqlDialect {
1515
getSessionIds(): string[]
1616

1717
// Query execution
18-
execute(sql: string, params?: unknown[], sessionId?: string): Promise<QueryResult>
19-
cancel(sessionId?: string): Promise<void>
18+
execute(sql: string, params?: unknown[], sessionId?: string, poolQueryKey?: symbol): Promise<QueryResult>
19+
cancel(sessionId?: string, poolQueryKey?: symbol): Promise<void>
2020

2121
// Streaming iteration — yields batches of rows from a query
2222
iterate(
@@ -46,4 +46,6 @@ export interface DatabaseDriver extends SqlDialect {
4646
commit(sessionId?: string): Promise<void>
4747
rollback(sessionId?: string): Promise<void>
4848
inTransaction(sessionId?: string): boolean
49+
isTxAborted(sessionId?: string): boolean
50+
isIterating(sessionId?: string): boolean
4951
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { DatabaseDriver } from './driver'
2+
3+
/**
4+
* Run a callback within an ephemeral (auto-created, auto-released) session.
5+
* Handles reserve → try/fn → cancel → release lifecycle.
6+
*/
7+
export async function withEphemeralSession<T>(
8+
driver: DatabaseDriver,
9+
fn: (sessionId: string) => Promise<T>,
10+
): Promise<T> {
11+
const sessionId = `__ephemeral_${crypto.randomUUID()}`
12+
await driver.reserveSession(sessionId)
13+
try {
14+
return await fn(sessionId)
15+
} finally {
16+
try {
17+
await driver.cancel(sessionId)
18+
} catch { /* best effort */ }
19+
try {
20+
await driver.releaseSession(sessionId)
21+
} catch { /* best effort */ }
22+
}
23+
}

src/backend-shared/db/error-mapping.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { AuthenticationError, ConnectionError, ConstraintError, DatabaseError, Q
55
/** Map a PostgreSQL error to a domain error. PostgreSQL errors carry a `code` property (SQLSTATE). */
66
export function mapPostgresError(err: unknown): DatabaseError {
77
const message = err instanceof Error ? err.message : String(err)
8-
const pgCode = (err as any)?.code as string | undefined
8+
// Bun SQL stores SQLSTATE in errno, postgres.js uses code
9+
const pgCode = ((err as any)?.errno ?? (err as any)?.code) as string | undefined
910

1011
// Connection errors
1112
if (/ECONNREFUSED|connection refused/i.test(message)) {

src/backend-shared/db/logging-driver.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,18 @@ export class LoggingDriver implements DatabaseDriver {
9393
ping(): Promise<void> {
9494
return this.inner.ping()
9595
}
96-
loadSchema(sessionId?: string): Promise<SchemaData> {
97-
return this.inner.loadSchema(sessionId)
96+
async loadSchema(sessionId?: string): Promise<SchemaData> {
97+
console.debug('[SQL] loadSchema started' + (sessionId ? ` (session: ${sessionId})` : ''))
98+
const start = performance.now()
99+
try {
100+
const result = await this.inner.loadSchema(sessionId)
101+
const tables = Object.values(result.tables).reduce((sum, t) => sum + t.length, 0)
102+
console.debug(`[SQL] loadSchema completed — ${result.schemas.length} schemas, ${tables} tables (${Math.round(performance.now() - start)}ms)`)
103+
return result
104+
} catch (err) {
105+
console.debug(`[SQL ERROR] loadSchema failed (${Math.round(performance.now() - start)}ms)`, err)
106+
throw err
107+
}
98108
}
99109
beginTransaction(sessionId?: string): Promise<void> {
100110
return this.inner.beginTransaction(sessionId)
@@ -108,6 +118,12 @@ export class LoggingDriver implements DatabaseDriver {
108118
inTransaction(sessionId?: string): boolean {
109119
return this.inner.inTransaction(sessionId)
110120
}
121+
isTxAborted(sessionId?: string): boolean {
122+
return this.inner.isTxAborted(sessionId)
123+
}
124+
isIterating(sessionId?: string): boolean {
125+
return this.inner.isIterating(sessionId)
126+
}
111127
getDriverType(): 'postgresql' | 'sqlite' | 'mysql' {
112128
return (this.inner as any).getDriverType()
113129
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { stripLiteralsAndComments } from '@dotaz/shared/sql'
2+
import type { ReservedSQL } from 'bun'
3+
4+
/** Minimal interface for transaction state tracking. */
5+
interface TxTrackable {
6+
txActive: boolean
7+
txAborted?: boolean
8+
}
9+
10+
/** Detect raw transaction-control statements and sync txActive/txAborted flags. */
11+
export function syncTxActive(session: TxTrackable, sql: string): void {
12+
const upper = stripLiteralsAndComments(sql).trim().toUpperCase()
13+
if (/^(BEGIN|START\s+TRANSACTION)\b/.test(upper)) {
14+
session.txActive = true
15+
if ('txAborted' in session) session.txAborted = false
16+
} else if (/^(COMMIT|END)\b/.test(upper)) {
17+
session.txActive = false
18+
if ('txAborted' in session) session.txAborted = false
19+
} else if (/^ROLLBACK\b/.test(upper) && !/^ROLLBACK\s+TO\b/.test(upper)) {
20+
session.txActive = false
21+
if ('txAborted' in session) session.txAborted = false
22+
}
23+
}
24+
25+
/** Detect connection-level errors (TCP drop, reset, etc.) as opposed to protocol errors. */
26+
export function isConnectionLevelError(err: unknown): boolean {
27+
const code = (err as any)?.code
28+
if (typeof code === 'string' && /^(ECONNRESET|ECONNREFUSED|EPIPE|ETIMEDOUT|ENOTCONN)$/.test(code)) {
29+
return true
30+
}
31+
// fallback for errors without .code (Bun-specific, string messages, etc.)
32+
const message = err instanceof Error ? err.message : String(err)
33+
return /ECONNRESET|ECONNREFUSED|EPIPE|ETIMEDOUT|connection (terminated|ended|closed|lost|reset)|socket.*(closed|hang up|end)|write after end|broken pipe|network/i
34+
.test(message)
35+
}
36+
37+
/**
38+
* Safely release a reserved connection back to the pool.
39+
* Optionally rolls back, then runs the reset function (e.g. DISCARD ALL or RESET CONNECTION),
40+
* releases the connection, or closes it if reset/release fails.
41+
*/
42+
export async function safeReleaseConnection(
43+
conn: ReservedSQL,
44+
resetFn: (conn: ReservedSQL) => Promise<void>,
45+
options?: { rollback?: boolean },
46+
): Promise<void> {
47+
if (options?.rollback) {
48+
try {
49+
await conn.unsafe('ROLLBACK')
50+
} catch { /* ignore — no tx is fine */ }
51+
}
52+
try {
53+
await resetFn(conn)
54+
try {
55+
conn.release()
56+
} catch { /* broken connection */ }
57+
} catch {
58+
try {
59+
conn.close({ timeout: 0 })
60+
} catch { /* already dead */ }
61+
}
62+
}

0 commit comments

Comments
 (0)